Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
1.3 MiB
Plaintext

{"repo": "agnt-gg/agnt", "files": {"backend/src/services/auth/envKeyMap.js": {"a63f8ebdeada20f4125d4a2bf40a6caf724fc152": {"t": 1785745541.632489, "n": 5448, "h": "7e44b48ad3b729a2", "c": "/**\n * ENV_KEY_MAP — provider key → process.env variable name.\n *\n * Single source of truth for which env var seeds which provider's credential.\n * Consumed by AuthManager.getValidAccessToken and AuthManager.getConnectedApps\n * so that setting e.g. OPENAI_API_KEY=sk-... or SLACK_BOT_TOKEN=xoxb-... in\n * .env / docker-compose results in a working, \"connected\" provider without\n * any UI action.\n *\n * Naming convention: follow each upstream provider's own env-var name where\n * one exists (XAI_API_KEY for Grok, TOGETHER_API_KEY for Together, GITHUB_TOKEN\n * for GitHub, SLACK_BOT_TOKEN for Slack, NOTION_API_KEY for Notion, etc.) so\n * users coming from upstream docs don't have to re-learn the variable name.\n * Where no upstream convention exists, `<PROVIDER>_API_KEY` is the default.\n *\n * Scope: every provider that supports a single-string credential (API key,\n * bot token, personal access token, app password). Pure-OAuth-only providers\n * with no PAT form (sign-in-with-apple, google-login flow, AWS multi-credential\n * pairs, MetaMask wallet) are intentionally omitted — their auth doesn't fit\n * a single env-var slot. CLI providers (openai-codex, claude-code, gemini-cli,\n * kimi-code, grok-build, cursor-cli, ollama, lm-studio) are also omitted; they\n * read filesystem creds (or, for cursor-cli, the CLI owns the session outright).\n */\nconst ENV_KEY_MAP = Object.freeze({\n // ── AI / LLM providers ─────────────────────────────────────────────\n openai: 'OPENAI_API_KEY',\n anthropic: 'ANTHROPIC_API_KEY',\n gemini: 'GEMINI_API_KEY',\n grokai: 'XAI_API_KEY',\n groq: 'GROQ_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n openrouter: 'OPENROUTER_API_KEY',\n togetherai: 'TOGETHER_API_KEY',\n cerebras: 'CEREBRAS_API_KEY',\n kimi: 'MOONSHOT_API_KEY',\n minimax: 'MINIMAX_API_KEY',\n zai: 'ZAI_API_KEY',\n chutes: 'CHUTES_API_KEY',\n mistral: 'MISTRAL_API_KEY',\n fireworks: 'FIREWORKS_API_KEY',\n deepinfra: 'DEEPINFRA_API_KEY',\n perplexity: 'PERPLEXITY_API_KEY',\n sambanova: 'SAMBANOVA_API_KEY',\n novita: 'NOVITA_API_KEY',\n nebius: 'NEBIUS_API_KEY',\n 'nvidia-nim': 'NVIDIA_API_KEY',\n scaleway: 'SCALEWAY_API_KEY',\n hyperbolic: 'HYPERBOLIC_API_KEY',\n 'meta-llama': 'LLAMA_API_KEY',\n cohere: 'COHERE_API_KEY',\n lambda: 'LAMBDA_API_KEY',\n lepton: 'LEPTON_API_KEY',\n vllm: 'VLLM_API_KEY',\n jan: 'JAN_API_KEY',\n\n // ── Voice / media ──────────────────────────────────────────────────\n elevenlabs: 'ELEVENLABS_API_KEY',\n unsplash: 'UNSPLASH_ACCESS_KEY',\n canva: 'CANVA_API_KEY',\n\n // ── Communication / chat ───────────────────────────────────────────\n slack: 'SLACK_BOT_TOKEN',\n discord: 'DISCORD_BOT_TOKEN',\n telegram: 'TELEGRAM_BOT_TOKEN',\n intercom: 'INTERCOM_ACCESS_TOKEN',\n zendesk: 'ZENDESK_API_TOKEN',\n zoom: 'ZOOM_ACCESS_TOKEN',\n\n // ── Productivity / docs / notes ────────────────────────────────────\n notion: 'NOTION_API_KEY',\n airtable: 'AIRTABLE_API_KEY',\n evernote: 'EVERNOTE_DEVELOPER_TOKEN',\n obsidian: 'OBSIDIAN_API_KEY',\n todoist: 'TODOIST_API_TOKEN',\n monday: 'MONDAY_API_KEY',\n clickup: 'CLICKUP_API_TOKEN',\n asana: 'ASANA_PERSONAL_ACCESS_TOKEN',\n trello: 'TRELLO_API_TOKEN',\n calendly: 'CALENDLY_API_KEY',\n\n // ── Engineering / dev ──────────────────────────────────────────────\n github: 'GITHUB_TOKEN',\n atlassian: 'ATLASSIAN_API_TOKEN',\n jira: 'JIRA_API_TOKEN',\n figma: 'FIGMA_ACCESS_TOKEN',\n firecrawl: 'FIRECRAWL_API_KEY',\n 'atlas-cloud': 'MONGODB_ATLAS_API_KEY',\n\n // ── Marketing / CRM / sales ────────────────────────────────────────\n hubspot: 'HUBSPOT_ACCESS_TOKEN',\n salesforce: 'SALESFORCE_ACCESS_TOKEN',\n mailchimp: 'MAILCHIMP_API_KEY',\n zapier: 'ZAPIER_NLA_API_KEY',\n\n // ── Finance / commerce ─────────────────────────────────────────────\n stripe: 'STRIPE_SECRET_KEY',\n quickbooks: 'QUICKBOOKS_ACCESS_TOKEN',\n xero: 'XERO_ACCESS_TOKEN',\n ebay: 'EBAY_API_KEY',\n docusign: 'DOCUSIGN_API_KEY',\n bankr: 'BANKR_API_KEY',\n\n // ── Social ─────────────────────────────────────────────────────────\n linkedin: 'LINKEDIN_ACCESS_TOKEN',\n facebook: 'FACEBOOK_ACCESS_TOKEN',\n\n // ── Storage / Microsoft ecosystem ──────────────────────────────────\n dropbox: 'DROPBOX_ACCESS_TOKEN',\n microsoft: 'MICROSOFT_ACCESS_TOKEN',\n\n // ── Analytics / weather / misc ─────────────────────────────────────\n openweathermap: 'OPENWEATHERMAP_API_KEY',\n analytics: 'GOOGLE_ANALYTICS_API_KEY',\n tableau: 'TABLEAU_ACCESS_TOKEN',\n\n // ── Note: deliberately omitted ─────────────────────────────────────\n // - agnt : internal AGNT auth, not a third-party provider\n // - google : umbrella for the Google Workspace suite (Gmail, Drive,\n // YouTube, Sheets, Slides, Calendar, etc.) — OAuth-only,\n // no single-string PAT covers all those scopes. Use the\n // OAuth connect flow. `gemini` above handles the AI API.\n // - google-login : OAuth login flow, no single-string equivalent\n // - apple : sign-in-with-apple, no PAT form\n // - aws : requires AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY\n // (plus optional SESSION_TOKEN) — multi-credential, can't\n // collapse to one env var. Handle in AWS-specific tool.\n // - metamask : wallet signing, not a server-side credential\n // - dickbutt : test/placeholder entry on remote\n // - kimi-code : CLI provider, uses filesystem creds\n});\n\nexport default ENV_KEY_MAP;\nexport { ENV_KEY_MAP };\n"}, "36fd95bab613e9c0aedfdbba042587405e385034": {"t": 1785754035.762013, "n": 5448, "h": "7e44b48ad3b729a2", "c": "/**\n * ENV_KEY_MAP — provider key → process.env variable name.\n *\n * Single source of truth for which env var seeds which provider's credential.\n * Consumed by AuthManager.getValidAccessToken and AuthManager.getConnectedApps\n * so that setting e.g. OPENAI_API_KEY=sk-... or SLACK_BOT_TOKEN=xoxb-... in\n * .env / docker-compose results in a working, \"connected\" provider without\n * any UI action.\n *\n * Naming convention: follow each upstream provider's own env-var name where\n * one exists (XAI_API_KEY for Grok, TOGETHER_API_KEY for Together, GITHUB_TOKEN\n * for GitHub, SLACK_BOT_TOKEN for Slack, NOTION_API_KEY for Notion, etc.) so\n * users coming from upstream docs don't have to re-learn the variable name.\n * Where no upstream convention exists, `<PROVIDER>_API_KEY` is the default.\n *\n * Scope: every provider that supports a single-string credential (API key,\n * bot token, personal access token, app password). Pure-OAuth-only providers\n * with no PAT form (sign-in-with-apple, google-login flow, AWS multi-credential\n * pairs, MetaMask wallet) are intentionally omitted — their auth doesn't fit\n * a single env-var slot. CLI providers (openai-codex, claude-code, gemini-cli,\n * kimi-code, grok-build, cursor-cli, ollama, lm-studio) are also omitted; they\n * read filesystem creds (or, for cursor-cli, the CLI owns the session outright).\n */\nconst ENV_KEY_MAP = Object.freeze({\n // ── AI / LLM providers ─────────────────────────────────────────────\n openai: 'OPENAI_API_KEY',\n anthropic: 'ANTHROPIC_API_KEY',\n gemini: 'GEMINI_API_KEY',\n grokai: 'XAI_API_KEY',\n groq: 'GROQ_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n openrouter: 'OPENROUTER_API_KEY',\n togetherai: 'TOGETHER_API_KEY',\n cerebras: 'CEREBRAS_API_KEY',\n kimi: 'MOONSHOT_API_KEY',\n minimax: 'MINIMAX_API_KEY',\n zai: 'ZAI_API_KEY',\n chutes: 'CHUTES_API_KEY',\n mistral: 'MISTRAL_API_KEY',\n fireworks: 'FIREWORKS_API_KEY',\n deepinfra: 'DEEPINFRA_API_KEY',\n perplexity: 'PERPLEXITY_API_KEY',\n sambanova: 'SAMBANOVA_API_KEY',\n novita: 'NOVITA_API_KEY',\n nebius: 'NEBIUS_API_KEY',\n 'nvidia-nim': 'NVIDIA_API_KEY',\n scaleway: 'SCALEWAY_API_KEY',\n hyperbolic: 'HYPERBOLIC_API_KEY',\n 'meta-llama': 'LLAMA_API_KEY',\n cohere: 'COHERE_API_KEY',\n lambda: 'LAMBDA_API_KEY',\n lepton: 'LEPTON_API_KEY',\n vllm: 'VLLM_API_KEY',\n jan: 'JAN_API_KEY',\n\n // ── Voice / media ──────────────────────────────────────────────────\n elevenlabs: 'ELEVENLABS_API_KEY',\n unsplash: 'UNSPLASH_ACCESS_KEY',\n canva: 'CANVA_API_KEY',\n\n // ── Communication / chat ───────────────────────────────────────────\n slack: 'SLACK_BOT_TOKEN',\n discord: 'DISCORD_BOT_TOKEN',\n telegram: 'TELEGRAM_BOT_TOKEN',\n intercom: 'INTERCOM_ACCESS_TOKEN',\n zendesk: 'ZENDESK_API_TOKEN',\n zoom: 'ZOOM_ACCESS_TOKEN',\n\n // ── Productivity / docs / notes ────────────────────────────────────\n notion: 'NOTION_API_KEY',\n airtable: 'AIRTABLE_API_KEY',\n evernote: 'EVERNOTE_DEVELOPER_TOKEN',\n obsidian: 'OBSIDIAN_API_KEY',\n todoist: 'TODOIST_API_TOKEN',\n monday: 'MONDAY_API_KEY',\n clickup: 'CLICKUP_API_TOKEN',\n asana: 'ASANA_PERSONAL_ACCESS_TOKEN',\n trello: 'TRELLO_API_TOKEN',\n calendly: 'CALENDLY_API_KEY',\n\n // ── Engineering / dev ──────────────────────────────────────────────\n github: 'GITHUB_TOKEN',\n atlassian: 'ATLASSIAN_API_TOKEN',\n jira: 'JIRA_API_TOKEN',\n figma: 'FIGMA_ACCESS_TOKEN',\n firecrawl: 'FIRECRAWL_API_KEY',\n 'atlas-cloud': 'MONGODB_ATLAS_API_KEY',\n\n // ── Marketing / CRM / sales ────────────────────────────────────────\n hubspot: 'HUBSPOT_ACCESS_TOKEN',\n salesforce: 'SALESFORCE_ACCESS_TOKEN',\n mailchimp: 'MAILCHIMP_API_KEY',\n zapier: 'ZAPIER_NLA_API_KEY',\n\n // ── Finance / commerce ─────────────────────────────────────────────\n stripe: 'STRIPE_SECRET_KEY',\n quickbooks: 'QUICKBOOKS_ACCESS_TOKEN',\n xero: 'XERO_ACCESS_TOKEN',\n ebay: 'EBAY_API_KEY',\n docusign: 'DOCUSIGN_API_KEY',\n bankr: 'BANKR_API_KEY',\n\n // ── Social ─────────────────────────────────────────────────────────\n linkedin: 'LINKEDIN_ACCESS_TOKEN',\n facebook: 'FACEBOOK_ACCESS_TOKEN',\n\n // ── Storage / Microsoft ecosystem ──────────────────────────────────\n dropbox: 'DROPBOX_ACCESS_TOKEN',\n microsoft: 'MICROSOFT_ACCESS_TOKEN',\n\n // ── Analytics / weather / misc ─────────────────────────────────────\n openweathermap: 'OPENWEATHERMAP_API_KEY',\n analytics: 'GOOGLE_ANALYTICS_API_KEY',\n tableau: 'TABLEAU_ACCESS_TOKEN',\n\n // ── Note: deliberately omitted ─────────────────────────────────────\n // - agnt : internal AGNT auth, not a third-party provider\n // - google : umbrella for the Google Workspace suite (Gmail, Drive,\n // YouTube, Sheets, Slides, Calendar, etc.) — OAuth-only,\n // no single-string PAT covers all those scopes. Use the\n // OAuth connect flow. `gemini` above handles the AI API.\n // - google-login : OAuth login flow, no single-string equivalent\n // - apple : sign-in-with-apple, no PAT form\n // - aws : requires AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY\n // (plus optional SESSION_TOKEN) — multi-credential, can't\n // collapse to one env var. Handle in AWS-specific tool.\n // - metamask : wallet signing, not a server-side credential\n // - dickbutt : test/placeholder entry on remote\n // - kimi-code : CLI provider, uses filesystem creds\n});\n\nexport default ENV_KEY_MAP;\nexport { ENV_KEY_MAP };\n"}, "7f4fbbe8ccda7ecae582c93beee8f60fa5a186ba": {"t": 1785754037.0157166, "n": 5365, "h": "a3a0816c8036ee7e", "c": "/**\n * ENV_KEY_MAP — provider key → process.env variable name.\n *\n * Single source of truth for which env var seeds which provider's credential.\n * Consumed by AuthManager.getValidAccessToken and AuthManager.getConnectedApps\n * so that setting e.g. OPENAI_API_KEY=sk-... or SLACK_BOT_TOKEN=xoxb-... in\n * .env / docker-compose results in a working, \"connected\" provider without\n * any UI action.\n *\n * Naming convention: follow each upstream provider's own env-var name where\n * one exists (XAI_API_KEY for Grok, TOGETHER_API_KEY for Together, GITHUB_TOKEN\n * for GitHub, SLACK_BOT_TOKEN for Slack, NOTION_API_KEY for Notion, etc.) so\n * users coming from upstream docs don't have to re-learn the variable name.\n * Where no upstream convention exists, `<PROVIDER>_API_KEY` is the default.\n *\n * Scope: every provider that supports a single-string credential (API key,\n * bot token, personal access token, app password). Pure-OAuth-only providers\n * with no PAT form (sign-in-with-apple, google-login flow, AWS multi-credential\n * pairs, MetaMask wallet) are intentionally omitted — their auth doesn't fit\n * a single env-var slot. CLI providers (openai-codex, claude-code, gemini-cli,\n * kimi-code, ollama, lm-studio) are also omitted; they read filesystem creds.\n */\nconst ENV_KEY_MAP = Object.freeze({\n // ── AI / LLM providers ─────────────────────────────────────────────\n openai: 'OPENAI_API_KEY',\n anthropic: 'ANTHROPIC_API_KEY',\n gemini: 'GEMINI_API_KEY',\n grokai: 'XAI_API_KEY',\n groq: 'GROQ_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n openrouter: 'OPENROUTER_API_KEY',\n togetherai: 'TOGETHER_API_KEY',\n cerebras: 'CEREBRAS_API_KEY',\n kimi: 'MOONSHOT_API_KEY',\n minimax: 'MINIMAX_API_KEY',\n zai: 'ZAI_API_KEY',\n chutes: 'CHUTES_API_KEY',\n mistral: 'MISTRAL_API_KEY',\n fireworks: 'FIREWORKS_API_KEY',\n deepinfra: 'DEEPINFRA_API_KEY',\n perplexity: 'PERPLEXITY_API_KEY',\n sambanova: 'SAMBANOVA_API_KEY',\n novita: 'NOVITA_API_KEY',\n nebius: 'NEBIUS_API_KEY',\n 'nvidia-nim': 'NVIDIA_API_KEY',\n scaleway: 'SCALEWAY_API_KEY',\n hyperbolic: 'HYPERBOLIC_API_KEY',\n 'meta-llama': 'LLAMA_API_KEY',\n cohere: 'COHERE_API_KEY',\n lambda: 'LAMBDA_API_KEY',\n lepton: 'LEPTON_API_KEY',\n vllm: 'VLLM_API_KEY',\n jan: 'JAN_API_KEY',\n\n // ── Voice / media ──────────────────────────────────────────────────\n elevenlabs: 'ELEVENLABS_API_KEY',\n unsplash: 'UNSPLASH_ACCESS_KEY',\n canva: 'CANVA_API_KEY',\n\n // ── Communication / chat ───────────────────────────────────────────\n slack: 'SLACK_BOT_TOKEN',\n discord: 'DISCORD_BOT_TOKEN',\n telegram: 'TELEGRAM_BOT_TOKEN',\n intercom: 'INTERCOM_ACCESS_TOKEN',\n zendesk: 'ZENDESK_API_TOKEN',\n zoom: 'ZOOM_ACCESS_TOKEN',\n\n // ── Productivity / docs / notes ────────────────────────────────────\n notion: 'NOTION_API_KEY',\n airtable: 'AIRTABLE_API_KEY',\n evernote: 'EVERNOTE_DEVELOPER_TOKEN',\n obsidian: 'OBSIDIAN_API_KEY',\n todoist: 'TODOIST_API_TOKEN',\n monday: 'MONDAY_API_KEY',\n clickup: 'CLICKUP_API_TOKEN',\n asana: 'ASANA_PERSONAL_ACCESS_TOKEN',\n trello: 'TRELLO_API_TOKEN',\n calendly: 'CALENDLY_API_KEY',\n\n // ── Engineering / dev ──────────────────────────────────────────────\n github: 'GITHUB_TOKEN',\n atlassian: 'ATLASSIAN_API_TOKEN',\n jira: 'JIRA_API_TOKEN',\n figma: 'FIGMA_ACCESS_TOKEN',\n firecrawl: 'FIRECRAWL_API_KEY',\n 'atlas-cloud': 'MONGODB_ATLAS_API_KEY',\n\n // ── Marketing / CRM / sales ────────────────────────────────────────\n hubspot: 'HUBSPOT_ACCESS_TOKEN',\n salesforce: 'SALESFORCE_ACCESS_TOKEN',\n mailchimp: 'MAILCHIMP_API_KEY',\n zapier: 'ZAPIER_NLA_API_KEY',\n\n // ── Finance / commerce ─────────────────────────────────────────────\n stripe: 'STRIPE_SECRET_KEY',\n quickbooks: 'QUICKBOOKS_ACCESS_TOKEN',\n xero: 'XERO_ACCESS_TOKEN',\n ebay: 'EBAY_API_KEY',\n docusign: 'DOCUSIGN_API_KEY',\n bankr: 'BANKR_API_KEY',\n\n // ── Social ─────────────────────────────────────────────────────────\n linkedin: 'LINKEDIN_ACCESS_TOKEN',\n facebook: 'FACEBOOK_ACCESS_TOKEN',\n\n // ── Storage / Microsoft ecosystem ──────────────────────────────────\n dropbox: 'DROPBOX_ACCESS_TOKEN',\n microsoft: 'MICROSOFT_ACCESS_TOKEN',\n\n // ── Analytics / weather / misc ─────────────────────────────────────\n openweathermap: 'OPENWEATHERMAP_API_KEY',\n analytics: 'GOOGLE_ANALYTICS_API_KEY',\n tableau: 'TABLEAU_ACCESS_TOKEN',\n\n // ── Note: deliberately omitted ─────────────────────────────────────\n // - agnt : internal AGNT auth, not a third-party provider\n // - google : umbrella for the Google Workspace suite (Gmail, Drive,\n // YouTube, Sheets, Slides, Calendar, etc.) — OAuth-only,\n // no single-string PAT covers all those scopes. Use the\n // OAuth connect flow. `gemini` above handles the AI API.\n // - google-login : OAuth login flow, no single-string equivalent\n // - apple : sign-in-with-apple, no PAT form\n // - aws : requires AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY\n // (plus optional SESSION_TOKEN) — multi-credential, can't\n // collapse to one env var. Handle in AWS-specific tool.\n // - metamask : wallet signing, not a server-side credential\n // - dickbutt : test/placeholder entry on remote\n // - kimi-code : CLI provider, uses filesystem creds\n});\n\nexport default ENV_KEY_MAP;\nexport { ENV_KEY_MAP };\n"}, "6fdecedb35300903d1a2a60faeeb356d27c2c4ac": {"t": 1786067400.535519, "n": 5448, "h": "7e44b48ad3b729a2", "c": "/**\n * ENV_KEY_MAP — provider key → process.env variable name.\n *\n * Single source of truth for which env var seeds which provider's credential.\n * Consumed by AuthManager.getValidAccessToken and AuthManager.getConnectedApps\n * so that setting e.g. OPENAI_API_KEY=sk-... or SLACK_BOT_TOKEN=xoxb-... in\n * .env / docker-compose results in a working, \"connected\" provider without\n * any UI action.\n *\n * Naming convention: follow each upstream provider's own env-var name where\n * one exists (XAI_API_KEY for Grok, TOGETHER_API_KEY for Together, GITHUB_TOKEN\n * for GitHub, SLACK_BOT_TOKEN for Slack, NOTION_API_KEY for Notion, etc.) so\n * users coming from upstream docs don't have to re-learn the variable name.\n * Where no upstream convention exists, `<PROVIDER>_API_KEY` is the default.\n *\n * Scope: every provider that supports a single-string credential (API key,\n * bot token, personal access token, app password). Pure-OAuth-only providers\n * with no PAT form (sign-in-with-apple, google-login flow, AWS multi-credential\n * pairs, MetaMask wallet) are intentionally omitted — their auth doesn't fit\n * a single env-var slot. CLI providers (openai-codex, claude-code, gemini-cli,\n * kimi-code, grok-build, cursor-cli, ollama, lm-studio) are also omitted; they\n * read filesystem creds (or, for cursor-cli, the CLI owns the session outright).\n */\nconst ENV_KEY_MAP = Object.freeze({\n // ── AI / LLM providers ─────────────────────────────────────────────\n openai: 'OPENAI_API_KEY',\n anthropic: 'ANTHROPIC_API_KEY',\n gemini: 'GEMINI_API_KEY',\n grokai: 'XAI_API_KEY',\n groq: 'GROQ_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n openrouter: 'OPENROUTER_API_KEY',\n togetherai: 'TOGETHER_API_KEY',\n cerebras: 'CEREBRAS_API_KEY',\n kimi: 'MOONSHOT_API_KEY',\n minimax: 'MINIMAX_API_KEY',\n zai: 'ZAI_API_KEY',\n chutes: 'CHUTES_API_KEY',\n mistral: 'MISTRAL_API_KEY',\n fireworks: 'FIREWORKS_API_KEY',\n deepinfra: 'DEEPINFRA_API_KEY',\n perplexity: 'PERPLEXITY_API_KEY',\n sambanova: 'SAMBANOVA_API_KEY',\n novita: 'NOVITA_API_KEY',\n nebius: 'NEBIUS_API_KEY',\n 'nvidia-nim': 'NVIDIA_API_KEY',\n scaleway: 'SCALEWAY_API_KEY',\n hyperbolic: 'HYPERBOLIC_API_KEY',\n 'meta-llama': 'LLAMA_API_KEY',\n cohere: 'COHERE_API_KEY',\n lambda: 'LAMBDA_API_KEY',\n lepton: 'LEPTON_API_KEY',\n vllm: 'VLLM_API_KEY',\n jan: 'JAN_API_KEY',\n\n // ── Voice / media ──────────────────────────────────────────────────\n elevenlabs: 'ELEVENLABS_API_KEY',\n unsplash: 'UNSPLASH_ACCESS_KEY',\n canva: 'CANVA_API_KEY',\n\n // ── Communication / chat ───────────────────────────────────────────\n slack: 'SLACK_BOT_TOKEN',\n discord: 'DISCORD_BOT_TOKEN',\n telegram: 'TELEGRAM_BOT_TOKEN',\n intercom: 'INTERCOM_ACCESS_TOKEN',\n zendesk: 'ZENDESK_API_TOKEN',\n zoom: 'ZOOM_ACCESS_TOKEN',\n\n // ── Productivity / docs / notes ────────────────────────────────────\n notion: 'NOTION_API_KEY',\n airtable: 'AIRTABLE_API_KEY',\n evernote: 'EVERNOTE_DEVELOPER_TOKEN',\n obsidian: 'OBSIDIAN_API_KEY',\n todoist: 'TODOIST_API_TOKEN',\n monday: 'MONDAY_API_KEY',\n clickup: 'CLICKUP_API_TOKEN',\n asana: 'ASANA_PERSONAL_ACCESS_TOKEN',\n trello: 'TRELLO_API_TOKEN',\n calendly: 'CALENDLY_API_KEY',\n\n // ── Engineering / dev ──────────────────────────────────────────────\n github: 'GITHUB_TOKEN',\n atlassian: 'ATLASSIAN_API_TOKEN',\n jira: 'JIRA_API_TOKEN',\n figma: 'FIGMA_ACCESS_TOKEN',\n firecrawl: 'FIRECRAWL_API_KEY',\n 'atlas-cloud': 'MONGODB_ATLAS_API_KEY',\n\n // ── Marketing / CRM / sales ────────────────────────────────────────\n hubspot: 'HUBSPOT_ACCESS_TOKEN',\n salesforce: 'SALESFORCE_ACCESS_TOKEN',\n mailchimp: 'MAILCHIMP_API_KEY',\n zapier: 'ZAPIER_NLA_API_KEY',\n\n // ── Finance / commerce ─────────────────────────────────────────────\n stripe: 'STRIPE_SECRET_KEY',\n quickbooks: 'QUICKBOOKS_ACCESS_TOKEN',\n xero: 'XERO_ACCESS_TOKEN',\n ebay: 'EBAY_API_KEY',\n docusign: 'DOCUSIGN_API_KEY',\n bankr: 'BANKR_API_KEY',\n\n // ── Social ─────────────────────────────────────────────────────────\n linkedin: 'LINKEDIN_ACCESS_TOKEN',\n facebook: 'FACEBOOK_ACCESS_TOKEN',\n\n // ── Storage / Microsoft ecosystem ──────────────────────────────────\n dropbox: 'DROPBOX_ACCESS_TOKEN',\n microsoft: 'MICROSOFT_ACCESS_TOKEN',\n\n // ── Analytics / weather / misc ─────────────────────────────────────\n openweathermap: 'OPENWEATHERMAP_API_KEY',\n analytics: 'GOOGLE_ANALYTICS_API_KEY',\n tableau: 'TABLEAU_ACCESS_TOKEN',\n\n // ── Note: deliberately omitted ─────────────────────────────────────\n // - agnt : internal AGNT auth, not a third-party provider\n // - google : umbrella for the Google Workspace suite (Gmail, Drive,\n // YouTube, Sheets, Slides, Calendar, etc.) — OAuth-only,\n // no single-string PAT covers all those scopes. Use the\n // OAuth connect flow. `gemini` above handles the AI API.\n // - google-login : OAuth login flow, no single-string equivalent\n // - apple : sign-in-with-apple, no PAT form\n // - aws : requires AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY\n // (plus optional SESSION_TOKEN) — multi-credential, can't\n // collapse to one env var. Handle in AWS-specific tool.\n // - metamask : wallet signing, not a server-side credential\n // - dickbutt : test/placeholder entry on remote\n // - kimi-code : CLI provider, uses filesystem creds\n});\n\nexport default ENV_KEY_MAP;\nexport { ENV_KEY_MAP };\n"}, "1fc0c1168401536185ef53e8f3800e69b5cbaca8": {"t": 1786068261.6064098, "n": 5448, "h": "7e44b48ad3b729a2", "c": "/**\n * ENV_KEY_MAP — provider key → process.env variable name.\n *\n * Single source of truth for which env var seeds which provider's credential.\n * Consumed by AuthManager.getValidAccessToken and AuthManager.getConnectedApps\n * so that setting e.g. OPENAI_API_KEY=sk-... or SLACK_BOT_TOKEN=xoxb-... in\n * .env / docker-compose results in a working, \"connected\" provider without\n * any UI action.\n *\n * Naming convention: follow each upstream provider's own env-var name where\n * one exists (XAI_API_KEY for Grok, TOGETHER_API_KEY for Together, GITHUB_TOKEN\n * for GitHub, SLACK_BOT_TOKEN for Slack, NOTION_API_KEY for Notion, etc.) so\n * users coming from upstream docs don't have to re-learn the variable name.\n * Where no upstream convention exists, `<PROVIDER>_API_KEY` is the default.\n *\n * Scope: every provider that supports a single-string credential (API key,\n * bot token, personal access token, app password). Pure-OAuth-only providers\n * with no PAT form (sign-in-with-apple, google-login flow, AWS multi-credential\n * pairs, MetaMask wallet) are intentionally omitted — their auth doesn't fit\n * a single env-var slot. CLI providers (openai-codex, claude-code, gemini-cli,\n * kimi-code, grok-build, cursor-cli, ollama, lm-studio) are also omitted; they\n * read filesystem creds (or, for cursor-cli, the CLI owns the session outright).\n */\nconst ENV_KEY_MAP = Object.freeze({\n // ── AI / LLM providers ─────────────────────────────────────────────\n openai: 'OPENAI_API_KEY',\n anthropic: 'ANTHROPIC_API_KEY',\n gemini: 'GEMINI_API_KEY',\n grokai: 'XAI_API_KEY',\n groq: 'GROQ_API_KEY',\n deepseek: 'DEEPSEEK_API_KEY',\n openrouter: 'OPENROUTER_API_KEY',\n togetherai: 'TOGETHER_API_KEY',\n cerebras: 'CEREBRAS_API_KEY',\n kimi: 'MOONSHOT_API_KEY',\n minimax: 'MINIMAX_API_KEY',\n zai: 'ZAI_API_KEY',\n chutes: 'CHUTES_API_KEY',\n mistral: 'MISTRAL_API_KEY',\n fireworks: 'FIREWORKS_API_KEY',\n deepinfra: 'DEEPINFRA_API_KEY',\n perplexity: 'PERPLEXITY_API_KEY',\n sambanova: 'SAMBANOVA_API_KEY',\n novita: 'NOVITA_API_KEY',\n nebius: 'NEBIUS_API_KEY',\n 'nvidia-nim': 'NVIDIA_API_KEY',\n scaleway: 'SCALEWAY_API_KEY',\n hyperbolic: 'HYPERBOLIC_API_KEY',\n 'meta-llama': 'LLAMA_API_KEY',\n cohere: 'COHERE_API_KEY',\n lambda: 'LAMBDA_API_KEY',\n lepton: 'LEPTON_API_KEY',\n vllm: 'VLLM_API_KEY',\n jan: 'JAN_API_KEY',\n\n // ── Voice / media ──────────────────────────────────────────────────\n elevenlabs: 'ELEVENLABS_API_KEY',\n unsplash: 'UNSPLASH_ACCESS_KEY',\n canva: 'CANVA_API_KEY',\n\n // ── Communication / chat ───────────────────────────────────────────\n slack: 'SLACK_BOT_TOKEN',\n discord: 'DISCORD_BOT_TOKEN',\n telegram: 'TELEGRAM_BOT_TOKEN',\n intercom: 'INTERCOM_ACCESS_TOKEN',\n zendesk: 'ZENDESK_API_TOKEN',\n zoom: 'ZOOM_ACCESS_TOKEN',\n\n // ── Productivity / docs / notes ────────────────────────────────────\n notion: 'NOTION_API_KEY',\n airtable: 'AIRTABLE_API_KEY',\n evernote: 'EVERNOTE_DEVELOPER_TOKEN',\n obsidian: 'OBSIDIAN_API_KEY',\n todoist: 'TODOIST_API_TOKEN',\n monday: 'MONDAY_API_KEY',\n clickup: 'CLICKUP_API_TOKEN',\n asana: 'ASANA_PERSONAL_ACCESS_TOKEN',\n trello: 'TRELLO_API_TOKEN',\n calendly: 'CALENDLY_API_KEY',\n\n // ── Engineering / dev ──────────────────────────────────────────────\n github: 'GITHUB_TOKEN',\n atlassian: 'ATLASSIAN_API_TOKEN',\n jira: 'JIRA_API_TOKEN',\n figma: 'FIGMA_ACCESS_TOKEN',\n firecrawl: 'FIRECRAWL_API_KEY',\n 'atlas-cloud': 'MONGODB_ATLAS_API_KEY',\n\n // ── Marketing / CRM / sales ────────────────────────────────────────\n hubspot: 'HUBSPOT_ACCESS_TOKEN',\n salesforce: 'SALESFORCE_ACCESS_TOKEN',\n mailchimp: 'MAILCHIMP_API_KEY',\n zapier: 'ZAPIER_NLA_API_KEY',\n\n // ── Finance / commerce ─────────────────────────────────────────────\n stripe: 'STRIPE_SECRET_KEY',\n quickbooks: 'QUICKBOOKS_ACCESS_TOKEN',\n xero: 'XERO_ACCESS_TOKEN',\n ebay: 'EBAY_API_KEY',\n docusign: 'DOCUSIGN_API_KEY',\n bankr: 'BANKR_API_KEY',\n\n // ── Social ─────────────────────────────────────────────────────────\n linkedin: 'LINKEDIN_ACCESS_TOKEN',\n facebook: 'FACEBOOK_ACCESS_TOKEN',\n\n // ── Storage / Microsoft ecosystem ──────────────────────────────────\n dropbox: 'DROPBOX_ACCESS_TOKEN',\n microsoft: 'MICROSOFT_ACCESS_TOKEN',\n\n // ── Analytics / weather / misc ─────────────────────────────────────\n openweathermap: 'OPENWEATHERMAP_API_KEY',\n analytics: 'GOOGLE_ANALYTICS_API_KEY',\n tableau: 'TABLEAU_ACCESS_TOKEN',\n\n // ── Note: deliberately omitted ─────────────────────────────────────\n // - agnt : internal AGNT auth, not a third-party provider\n // - google : umbrella for the Google Workspace suite (Gmail, Drive,\n // YouTube, Sheets, Slides, Calendar, etc.) — OAuth-only,\n // no single-string PAT covers all those scopes. Use the\n // OAuth connect flow. `gemini` above handles the AI API.\n // - google-login : OAuth login flow, no single-string equivalent\n // - apple : sign-in-with-apple, no PAT form\n // - aws : requires AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY\n // (plus optional SESSION_TOKEN) — multi-credential, can't\n // collapse to one env var. Handle in AWS-specific tool.\n // - metamask : wallet signing, not a server-side credential\n // - dickbutt : test/placeholder entry on remote\n // - kimi-code : CLI provider, uses filesystem creds\n});\n\nexport default ENV_KEY_MAP;\nexport { ENV_KEY_MAP };\n"}}, "frontend/src/store/app/aiProvider.js": {"a63f8ebdeada20f4125d4a2bf40a6caf724fc152": {"t": 1785746495.6715968, "n": 55411, "h": "21ed483fe3539fcc", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\nimport { withFreshness } from '../_utils/withFreshness.js';\nimport { TTL } from '../_utils/freshnessConfig.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version fallback — used only if the backend's schema-version endpoint\n// is unreachable on boot (offline, backend not started). The real invalidator\n// is the schema hash returned by GET /api/models/schema-version, fetched on\n// module load and compared against the stored value. Any change to metadata\n// shape, recommended-model order, or fallback list auto-invalidates all\n// per-provider localStorage caches. Kills the manual bump-the-integer footgun.\nconst MODEL_CACHE_VERSION_FALLBACK = 12;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION_FALLBACK)) {\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION_FALLBACK));\n console.log('[aiProvider] Cleared stale model caches (fallback version upgrade)');\n }\n})();\n\n// Auto cache-bust via backend schema hash. Fetched once on module load and\n// again on every explicit refresh action. Silent failure = keep whatever was\n// cached; the 5-min SWR path below still keeps things fresh.\n(async () => {\n try {\n const res = await fetch(`${API_CONFIG.BASE_URL}/models/schema-version`);\n if (!res.ok) return;\n const data = await res.json();\n if (!data?.success || !data.version) return;\n const stored = localStorage.getItem('model_schema_hash');\n if (stored !== data.version) {\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_schema_hash', data.version);\n console.log(`[aiProvider] Cleared stale model caches (backend schema hash changed to ${data.version})`);\n }\n } catch {\n // Backend not reachable on module load — fine, fallback version handles it.\n }\n})();\n\n// Display order is ENFORCED here, not by where entries are typed. The literal\n// below was hand-alphabetized, so every new provider silently landed wherever\n// its author appended it (PR #50 put Cursor between Grok-Build and Groq — the\n// \"Cursor is in the G's\" bug). Sorting at the boundary makes insertion position\n// irrelevant. localeCompare ignores punctuation at the primary strength, so\n// Grok-Build/GrokAI order by their letters rather than by the hyphen.\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'antigravity', displayName: 'Antigravity' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'grok-build', displayName: 'Grok-Build' },\n { key: 'cursor-cli', displayName: 'Cursor' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n].sort((a, b) => a.displayName.localeCompare(b.displayName, 'en', { sensitivity: 'base' }));\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\n// MIRROR of backend/src/services/ai/reasoningModels.js — keep regexes in sync.\n// Matches claude-opus-4-6+, claude-sonnet-4-6+ (auto-covers 4.7 / 4.8 / 4.9 / 4.10+),\n// plus the always-on Fable / Mythos family.\nconst ANTHROPIC_VERSIONED_REASONING_RE = /^claude-(opus|sonnet)-4-([6-9]|[1-9]\\d{1,2})(?:-|$)/;\nconst ANTHROPIC_FAMILY_REASONING_RE = /^claude-(fable|mythos)-/;\nconst ANTHROPIC_XHIGH_RE = /^claude-(opus-4-([7-9]|[1-9]\\d{1,2})(?:-|$)|fable-|mythos-)/;\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return ANTHROPIC_VERSIONED_REASONING_RE.test(lower) || ANTHROPIC_FAMILY_REASONING_RE.test(lower);\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n if (!lower.startsWith('anthropic/')) return false;\n return isAnthropicReasoningModel(lower.slice('anthropic/'.length));\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (ANTHROPIC_XHIGH_RE.test(lowerModel)) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli' || lowerProvider === 'antigravity') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n // Per-user \"Async tool execution\" toggle. Default FALSE — async tool\n // execution is currently an experimental opt-in feature. Users enable\n // it in Settings → AI Provider. localStorage is the source of truth on\n // the frontend so the toggle survives reloads; loadUserSettings()\n // reconciles it with the backend value on session start.\n asyncToolsEnabled: localStorage.getItem('asyncToolsEnabled') === 'true',\n // Per-user hard cap on tool result size returned to the LLM. Default\n // 100000 matches the historical OrchestratorService MAX_TOOL_RESULT_CHARS.\n // localStorage is the source of truth on the frontend so the value\n // survives reloads; loadUserSettings() reconciles with the backend.\n toolOutputCap: (() => {\n const raw = Number(localStorage.getItem('toolOutputCap'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100000;\n })(),\n // Per-user cap on tool-loop rounds per chat turn. Default 100 matches the\n // historical orchestrator/agent/tool/widget/goal cap. When set, overrides\n // every chat surface uniformly (including workflow/artifact which default\n // to 25 server-side).\n maxToolRounds: (() => {\n const raw = Number(localStorage.getItem('maxToolRounds'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100;\n })(),\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n SET_ASYNC_TOOLS_ENABLED(state, enabled) {\n const value = Boolean(enabled);\n state.asyncToolsEnabled = value;\n // Persist explicitly even when true, so a deliberate \"leave it on\"\n // setting is preserved across reloads (rather than relying on the\n // localStorage-missing-key default and tripping over future default\n // changes).\n localStorage.setItem('asyncToolsEnabled', value ? 'true' : 'false');\n },\n SET_TOOL_OUTPUT_CAP(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100000;\n state.toolOutputCap = clean;\n localStorage.setItem('toolOutputCap', String(clean));\n },\n SET_MAX_TOOL_ROUNDS(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100;\n state.maxToolRounds = clean;\n localStorage.setItem('maxToolRounds', String(clean));\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setAsyncToolsEnabled({ commit }, enabled) {\n const value = Boolean(enabled);\n commit('SET_ASYNC_TOOLS_ENABLED', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n asyncToolsEnabled: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync async tools toggle with backend:', error);\n }\n },\n\n async setToolOutputCap({ commit }, newCap) {\n // Clamp to the same window the backend validates against — keeps the\n // PUT from 400-ing on out-of-range values.\n const raw = Number(newCap);\n const clamped = Number.isFinite(raw)\n ? Math.max(25000, Math.min(500000, Math.round(raw)))\n : 100000;\n commit('SET_TOOL_OUTPUT_CAP', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n toolOutputCap: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync tool output cap with backend:', error);\n }\n },\n\n async setMaxToolRounds({ commit }, newRounds) {\n // Clamp to the backend-validated [1, 999999] window.\n const raw = Number(newRounds);\n const clamped = Number.isFinite(raw)\n ? Math.max(1, Math.min(999999, Math.round(raw)))\n : 100;\n commit('SET_MAX_TOOL_ROUNDS', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n maxToolRounds: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync max tool rounds with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (settings.asyncToolsEnabled !== undefined) {\n commit('SET_ASYNC_TOOLS_ENABLED', settings.asyncToolsEnabled === true);\n }\n\n if (settings.toolOutputCap !== undefined && Number.isFinite(Number(settings.toolOutputCap))) {\n commit('SET_TOOL_OUTPUT_CAP', Number(settings.toolOutputCap));\n }\n\n if (settings.maxToolRounds !== undefined && Number.isFinite(Number(settings.maxToolRounds))) {\n commit('SET_MAX_TOOL_ROUNDS', Number(settings.maxToolRounds));\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider.\n //\n // Stale-while-revalidate: if cache exists, commit it INSTANTLY for zero\n // paint latency and simultaneously fire a background network refresh.\n // If the fresh result diverges from cache, commit the update and overwrite\n // localStorage. Frontend cache is now display-only — never authoritative,\n // never blocks new-model discovery.\n //\n // forceRefresh: true — caller (RefreshModelsButton, provider reconnect)\n // awaits the network round-trip and skips the SWR early-return.\n async fetchProviderModels({ commit, state, dispatch }, { provider, forceRefresh = false } = {}) {\n if (!provider) return [];\n\n // Route non-built-in providers to their dedicated fetchers. Without this,\n // callers (ModelSelector, AgentForge) that pass 'Local' or a custom-provider\n // UUID hit /api/models/<id>/models, which only knows built-in keys and 400s.\n if (String(provider).toLowerCase() === 'local') {\n return dispatch('fetchLocalModels', { forceRefresh });\n }\n if (state.customProviders.some((cp) => cp.id === provider)) {\n return dispatch('fetchCustomProviderModels', provider);\n }\n\n if (state.loadingModels[provider] && !forceRefresh) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n\n // Step 1: Instant paint from cache if available. No TTL gate — cache is\n // for first-paint speed only, revalidation below always runs.\n let cachedModels = null;\n const rawCached = localStorage.getItem(cacheKey);\n if (rawCached) {\n try {\n const parsed = JSON.parse(rawCached);\n cachedModels = parsed.models;\n if (Array.isArray(cachedModels)) {\n commit('SET_PROVIDER_MODELS', { provider, models: cachedModels });\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch { /* ignore malformed meta */ }\n }\n }\n } catch {\n // Malformed cache — nuke it, treat as no cache\n localStorage.removeItem(cacheKey);\n }\n }\n\n // Step 2: revalidate. If we already painted from cache and forceRefresh\n // is false, run in background and return cached immediately. Otherwise\n // await the network.\n const providerLower = resolveProviderKey(provider);\n\n const revalidate = async () => {\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli' || providerLower === 'antigravity' || providerLower === 'grok-build' || providerLower === 'cursor-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, { headers });\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n // Only mutate Vuex + localStorage if the list actually changed. Prevents\n // pointless renders and lets model-selector components diff cleanly.\n const changed = !cachedModels\n || cachedModels.length !== models.length\n || JSON.stringify(cachedModels) !== JSON.stringify(models);\n\n if (changed) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n localStorage.setItem(cacheKey, JSON.stringify({ models, timestamp: Date.now() }));\n if (cachedModels) {\n console.log(`[aiProvider] ${provider} model list changed on revalidate (${cachedModels.length} → ${models.length})`);\n }\n }\n\n // Metadata revalidation runs alongside models.\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch { /* non-critical */ }\n\n return models;\n };\n\n // Fast-path: we have cache AND forceRefresh is false. Return cached\n // instantly, revalidate in background. Loading indicator not shown —\n // the user already sees a populated dropdown.\n if (cachedModels && !forceRefresh) {\n revalidate().catch((err) => {\n console.warn(`[aiProvider] Background revalidate failed for ${provider}:`, err.message);\n });\n return cachedModels;\n }\n\n // Slow-path: no cache OR forceRefresh. Await the network.\n commit('SET_LOADING_MODELS', { provider, loading: true });\n try {\n const models = await revalidate();\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n // Last-resort fallback: any cached data is better than empty.\n if (cachedModels) return cachedModels;\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n //\n // With SWR in fetchProviderModels this is now an emergency escape hatch,\n // not a routine action — normal use ships fresh models within one refresh\n // cycle without any manual button press.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for BOTH the display-name and\n // lowercase spellings so no split-brain survives. Providers can be\n // referenced as 'OpenAI-Codex' (display) or 'openai-codex' (key) at\n // different call sites; belt-and-suspenders coverage kills that class.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n localStorage.removeItem(`${providerLower}_models`);\n localStorage.removeItem(`${providerLower}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code', 'grok-build', 'cursor-cli'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n fetchCustomProviders: withFreshness('aiProvider.fetchCustomProviders', async ({ commit, state }) => {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n }, { staleAfter: TTL.aiProviderFetchCustomProviders }),\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "073e60dea31c3c980f23d29fe976da4dc6188a52": {"t": 1785754035.519341, "n": 55411, "h": "21ed483fe3539fcc", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\nimport { withFreshness } from '../_utils/withFreshness.js';\nimport { TTL } from '../_utils/freshnessConfig.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version fallback — used only if the backend's schema-version endpoint\n// is unreachable on boot (offline, backend not started). The real invalidator\n// is the schema hash returned by GET /api/models/schema-version, fetched on\n// module load and compared against the stored value. Any change to metadata\n// shape, recommended-model order, or fallback list auto-invalidates all\n// per-provider localStorage caches. Kills the manual bump-the-integer footgun.\nconst MODEL_CACHE_VERSION_FALLBACK = 12;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION_FALLBACK)) {\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION_FALLBACK));\n console.log('[aiProvider] Cleared stale model caches (fallback version upgrade)');\n }\n})();\n\n// Auto cache-bust via backend schema hash. Fetched once on module load and\n// again on every explicit refresh action. Silent failure = keep whatever was\n// cached; the 5-min SWR path below still keeps things fresh.\n(async () => {\n try {\n const res = await fetch(`${API_CONFIG.BASE_URL}/models/schema-version`);\n if (!res.ok) return;\n const data = await res.json();\n if (!data?.success || !data.version) return;\n const stored = localStorage.getItem('model_schema_hash');\n if (stored !== data.version) {\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_schema_hash', data.version);\n console.log(`[aiProvider] Cleared stale model caches (backend schema hash changed to ${data.version})`);\n }\n } catch {\n // Backend not reachable on module load — fine, fallback version handles it.\n }\n})();\n\n// Display order is ENFORCED here, not by where entries are typed. The literal\n// below was hand-alphabetized, so every new provider silently landed wherever\n// its author appended it (PR #50 put Cursor between Grok-Build and Groq — the\n// \"Cursor is in the G's\" bug). Sorting at the boundary makes insertion position\n// irrelevant. localeCompare ignores punctuation at the primary strength, so\n// Grok-Build/GrokAI order by their letters rather than by the hyphen.\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'antigravity', displayName: 'Antigravity' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'grok-build', displayName: 'Grok-Build' },\n { key: 'cursor-cli', displayName: 'Cursor' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n].sort((a, b) => a.displayName.localeCompare(b.displayName, 'en', { sensitivity: 'base' }));\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\n// MIRROR of backend/src/services/ai/reasoningModels.js — keep regexes in sync.\n// Matches claude-opus-4-6+, claude-sonnet-4-6+ (auto-covers 4.7 / 4.8 / 4.9 / 4.10+),\n// plus the always-on Fable / Mythos family.\nconst ANTHROPIC_VERSIONED_REASONING_RE = /^claude-(opus|sonnet)-4-([6-9]|[1-9]\\d{1,2})(?:-|$)/;\nconst ANTHROPIC_FAMILY_REASONING_RE = /^claude-(fable|mythos)-/;\nconst ANTHROPIC_XHIGH_RE = /^claude-(opus-4-([7-9]|[1-9]\\d{1,2})(?:-|$)|fable-|mythos-)/;\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return ANTHROPIC_VERSIONED_REASONING_RE.test(lower) || ANTHROPIC_FAMILY_REASONING_RE.test(lower);\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n if (!lower.startsWith('anthropic/')) return false;\n return isAnthropicReasoningModel(lower.slice('anthropic/'.length));\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (ANTHROPIC_XHIGH_RE.test(lowerModel)) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli' || lowerProvider === 'antigravity') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n // Per-user \"Async tool execution\" toggle. Default FALSE — async tool\n // execution is currently an experimental opt-in feature. Users enable\n // it in Settings → AI Provider. localStorage is the source of truth on\n // the frontend so the toggle survives reloads; loadUserSettings()\n // reconciles it with the backend value on session start.\n asyncToolsEnabled: localStorage.getItem('asyncToolsEnabled') === 'true',\n // Per-user hard cap on tool result size returned to the LLM. Default\n // 100000 matches the historical OrchestratorService MAX_TOOL_RESULT_CHARS.\n // localStorage is the source of truth on the frontend so the value\n // survives reloads; loadUserSettings() reconciles with the backend.\n toolOutputCap: (() => {\n const raw = Number(localStorage.getItem('toolOutputCap'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100000;\n })(),\n // Per-user cap on tool-loop rounds per chat turn. Default 100 matches the\n // historical orchestrator/agent/tool/widget/goal cap. When set, overrides\n // every chat surface uniformly (including workflow/artifact which default\n // to 25 server-side).\n maxToolRounds: (() => {\n const raw = Number(localStorage.getItem('maxToolRounds'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100;\n })(),\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n SET_ASYNC_TOOLS_ENABLED(state, enabled) {\n const value = Boolean(enabled);\n state.asyncToolsEnabled = value;\n // Persist explicitly even when true, so a deliberate \"leave it on\"\n // setting is preserved across reloads (rather than relying on the\n // localStorage-missing-key default and tripping over future default\n // changes).\n localStorage.setItem('asyncToolsEnabled', value ? 'true' : 'false');\n },\n SET_TOOL_OUTPUT_CAP(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100000;\n state.toolOutputCap = clean;\n localStorage.setItem('toolOutputCap', String(clean));\n },\n SET_MAX_TOOL_ROUNDS(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100;\n state.maxToolRounds = clean;\n localStorage.setItem('maxToolRounds', String(clean));\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setAsyncToolsEnabled({ commit }, enabled) {\n const value = Boolean(enabled);\n commit('SET_ASYNC_TOOLS_ENABLED', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n asyncToolsEnabled: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync async tools toggle with backend:', error);\n }\n },\n\n async setToolOutputCap({ commit }, newCap) {\n // Clamp to the same window the backend validates against — keeps the\n // PUT from 400-ing on out-of-range values.\n const raw = Number(newCap);\n const clamped = Number.isFinite(raw)\n ? Math.max(25000, Math.min(500000, Math.round(raw)))\n : 100000;\n commit('SET_TOOL_OUTPUT_CAP', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n toolOutputCap: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync tool output cap with backend:', error);\n }\n },\n\n async setMaxToolRounds({ commit }, newRounds) {\n // Clamp to the backend-validated [1, 999999] window.\n const raw = Number(newRounds);\n const clamped = Number.isFinite(raw)\n ? Math.max(1, Math.min(999999, Math.round(raw)))\n : 100;\n commit('SET_MAX_TOOL_ROUNDS', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n maxToolRounds: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync max tool rounds with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (settings.asyncToolsEnabled !== undefined) {\n commit('SET_ASYNC_TOOLS_ENABLED', settings.asyncToolsEnabled === true);\n }\n\n if (settings.toolOutputCap !== undefined && Number.isFinite(Number(settings.toolOutputCap))) {\n commit('SET_TOOL_OUTPUT_CAP', Number(settings.toolOutputCap));\n }\n\n if (settings.maxToolRounds !== undefined && Number.isFinite(Number(settings.maxToolRounds))) {\n commit('SET_MAX_TOOL_ROUNDS', Number(settings.maxToolRounds));\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider.\n //\n // Stale-while-revalidate: if cache exists, commit it INSTANTLY for zero\n // paint latency and simultaneously fire a background network refresh.\n // If the fresh result diverges from cache, commit the update and overwrite\n // localStorage. Frontend cache is now display-only — never authoritative,\n // never blocks new-model discovery.\n //\n // forceRefresh: true — caller (RefreshModelsButton, provider reconnect)\n // awaits the network round-trip and skips the SWR early-return.\n async fetchProviderModels({ commit, state, dispatch }, { provider, forceRefresh = false } = {}) {\n if (!provider) return [];\n\n // Route non-built-in providers to their dedicated fetchers. Without this,\n // callers (ModelSelector, AgentForge) that pass 'Local' or a custom-provider\n // UUID hit /api/models/<id>/models, which only knows built-in keys and 400s.\n if (String(provider).toLowerCase() === 'local') {\n return dispatch('fetchLocalModels', { forceRefresh });\n }\n if (state.customProviders.some((cp) => cp.id === provider)) {\n return dispatch('fetchCustomProviderModels', provider);\n }\n\n if (state.loadingModels[provider] && !forceRefresh) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n\n // Step 1: Instant paint from cache if available. No TTL gate — cache is\n // for first-paint speed only, revalidation below always runs.\n let cachedModels = null;\n const rawCached = localStorage.getItem(cacheKey);\n if (rawCached) {\n try {\n const parsed = JSON.parse(rawCached);\n cachedModels = parsed.models;\n if (Array.isArray(cachedModels)) {\n commit('SET_PROVIDER_MODELS', { provider, models: cachedModels });\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch { /* ignore malformed meta */ }\n }\n }\n } catch {\n // Malformed cache — nuke it, treat as no cache\n localStorage.removeItem(cacheKey);\n }\n }\n\n // Step 2: revalidate. If we already painted from cache and forceRefresh\n // is false, run in background and return cached immediately. Otherwise\n // await the network.\n const providerLower = resolveProviderKey(provider);\n\n const revalidate = async () => {\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli' || providerLower === 'antigravity' || providerLower === 'grok-build' || providerLower === 'cursor-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, { headers });\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n // Only mutate Vuex + localStorage if the list actually changed. Prevents\n // pointless renders and lets model-selector components diff cleanly.\n const changed = !cachedModels\n || cachedModels.length !== models.length\n || JSON.stringify(cachedModels) !== JSON.stringify(models);\n\n if (changed) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n localStorage.setItem(cacheKey, JSON.stringify({ models, timestamp: Date.now() }));\n if (cachedModels) {\n console.log(`[aiProvider] ${provider} model list changed on revalidate (${cachedModels.length} → ${models.length})`);\n }\n }\n\n // Metadata revalidation runs alongside models.\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch { /* non-critical */ }\n\n return models;\n };\n\n // Fast-path: we have cache AND forceRefresh is false. Return cached\n // instantly, revalidate in background. Loading indicator not shown —\n // the user already sees a populated dropdown.\n if (cachedModels && !forceRefresh) {\n revalidate().catch((err) => {\n console.warn(`[aiProvider] Background revalidate failed for ${provider}:`, err.message);\n });\n return cachedModels;\n }\n\n // Slow-path: no cache OR forceRefresh. Await the network.\n commit('SET_LOADING_MODELS', { provider, loading: true });\n try {\n const models = await revalidate();\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n // Last-resort fallback: any cached data is better than empty.\n if (cachedModels) return cachedModels;\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n //\n // With SWR in fetchProviderModels this is now an emergency escape hatch,\n // not a routine action — normal use ships fresh models within one refresh\n // cycle without any manual button press.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for BOTH the display-name and\n // lowercase spellings so no split-brain survives. Providers can be\n // referenced as 'OpenAI-Codex' (display) or 'openai-codex' (key) at\n // different call sites; belt-and-suspenders coverage kills that class.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n localStorage.removeItem(`${providerLower}_models`);\n localStorage.removeItem(`${providerLower}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code', 'grok-build', 'cursor-cli'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n fetchCustomProviders: withFreshness('aiProvider.fetchCustomProviders', async ({ commit, state }) => {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n }, { staleAfter: TTL.aiProviderFetchCustomProviders }),\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "7733a4974997cceb36a618bcf8aa59df7201f98d": {"t": 1785754037.0136433, "n": 54855, "h": "85e6bc195e5e06be", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\nimport { withFreshness } from '../_utils/withFreshness.js';\nimport { TTL } from '../_utils/freshnessConfig.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version fallback — used only if the backend's schema-version endpoint\n// is unreachable on boot (offline, backend not started). The real invalidator\n// is the schema hash returned by GET /api/models/schema-version, fetched on\n// module load and compared against the stored value. Any change to metadata\n// shape, recommended-model order, or fallback list auto-invalidates all\n// per-provider localStorage caches. Kills the manual bump-the-integer footgun.\nconst MODEL_CACHE_VERSION_FALLBACK = 12;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION_FALLBACK)) {\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION_FALLBACK));\n console.log('[aiProvider] Cleared stale model caches (fallback version upgrade)');\n }\n})();\n\n// Auto cache-bust via backend schema hash. Fetched once on module load and\n// again on every explicit refresh action. Silent failure = keep whatever was\n// cached; the 5-min SWR path below still keeps things fresh.\n(async () => {\n try {\n const res = await fetch(`${API_CONFIG.BASE_URL}/models/schema-version`);\n if (!res.ok) return;\n const data = await res.json();\n if (!data?.success || !data.version) return;\n const stored = localStorage.getItem('model_schema_hash');\n if (stored !== data.version) {\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_schema_hash', data.version);\n console.log(`[aiProvider] Cleared stale model caches (backend schema hash changed to ${data.version})`);\n }\n } catch {\n // Backend not reachable on module load — fine, fallback version handles it.\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'antigravity', displayName: 'Antigravity' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'grok-build', displayName: 'Grok-Build' },\n { key: 'cursor-cli', displayName: 'Cursor' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\n// MIRROR of backend/src/services/ai/reasoningModels.js — keep regexes in sync.\n// Matches claude-opus-4-6+, claude-sonnet-4-6+ (auto-covers 4.7 / 4.8 / 4.9 / 4.10+),\n// plus the always-on Fable / Mythos family.\nconst ANTHROPIC_VERSIONED_REASONING_RE = /^claude-(opus|sonnet)-4-([6-9]|[1-9]\\d{1,2})(?:-|$)/;\nconst ANTHROPIC_FAMILY_REASONING_RE = /^claude-(fable|mythos)-/;\nconst ANTHROPIC_XHIGH_RE = /^claude-(opus-4-([7-9]|[1-9]\\d{1,2})(?:-|$)|fable-|mythos-)/;\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return ANTHROPIC_VERSIONED_REASONING_RE.test(lower) || ANTHROPIC_FAMILY_REASONING_RE.test(lower);\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n if (!lower.startsWith('anthropic/')) return false;\n return isAnthropicReasoningModel(lower.slice('anthropic/'.length));\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (ANTHROPIC_XHIGH_RE.test(lowerModel)) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli' || lowerProvider === 'antigravity') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n // Per-user \"Async tool execution\" toggle. Default FALSE — async tool\n // execution is currently an experimental opt-in feature. Users enable\n // it in Settings → AI Provider. localStorage is the source of truth on\n // the frontend so the toggle survives reloads; loadUserSettings()\n // reconciles it with the backend value on session start.\n asyncToolsEnabled: localStorage.getItem('asyncToolsEnabled') === 'true',\n // Per-user hard cap on tool result size returned to the LLM. Default\n // 100000 matches the historical OrchestratorService MAX_TOOL_RESULT_CHARS.\n // localStorage is the source of truth on the frontend so the value\n // survives reloads; loadUserSettings() reconciles with the backend.\n toolOutputCap: (() => {\n const raw = Number(localStorage.getItem('toolOutputCap'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100000;\n })(),\n // Per-user cap on tool-loop rounds per chat turn. Default 100 matches the\n // historical orchestrator/agent/tool/widget/goal cap. When set, overrides\n // every chat surface uniformly (including workflow/artifact which default\n // to 25 server-side).\n maxToolRounds: (() => {\n const raw = Number(localStorage.getItem('maxToolRounds'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100;\n })(),\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n SET_ASYNC_TOOLS_ENABLED(state, enabled) {\n const value = Boolean(enabled);\n state.asyncToolsEnabled = value;\n // Persist explicitly even when true, so a deliberate \"leave it on\"\n // setting is preserved across reloads (rather than relying on the\n // localStorage-missing-key default and tripping over future default\n // changes).\n localStorage.setItem('asyncToolsEnabled', value ? 'true' : 'false');\n },\n SET_TOOL_OUTPUT_CAP(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100000;\n state.toolOutputCap = clean;\n localStorage.setItem('toolOutputCap', String(clean));\n },\n SET_MAX_TOOL_ROUNDS(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100;\n state.maxToolRounds = clean;\n localStorage.setItem('maxToolRounds', String(clean));\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setAsyncToolsEnabled({ commit }, enabled) {\n const value = Boolean(enabled);\n commit('SET_ASYNC_TOOLS_ENABLED', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n asyncToolsEnabled: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync async tools toggle with backend:', error);\n }\n },\n\n async setToolOutputCap({ commit }, newCap) {\n // Clamp to the same window the backend validates against — keeps the\n // PUT from 400-ing on out-of-range values.\n const raw = Number(newCap);\n const clamped = Number.isFinite(raw)\n ? Math.max(25000, Math.min(500000, Math.round(raw)))\n : 100000;\n commit('SET_TOOL_OUTPUT_CAP', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n toolOutputCap: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync tool output cap with backend:', error);\n }\n },\n\n async setMaxToolRounds({ commit }, newRounds) {\n // Clamp to the backend-validated [1, 999999] window.\n const raw = Number(newRounds);\n const clamped = Number.isFinite(raw)\n ? Math.max(1, Math.min(999999, Math.round(raw)))\n : 100;\n commit('SET_MAX_TOOL_ROUNDS', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n maxToolRounds: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync max tool rounds with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (settings.asyncToolsEnabled !== undefined) {\n commit('SET_ASYNC_TOOLS_ENABLED', settings.asyncToolsEnabled === true);\n }\n\n if (settings.toolOutputCap !== undefined && Number.isFinite(Number(settings.toolOutputCap))) {\n commit('SET_TOOL_OUTPUT_CAP', Number(settings.toolOutputCap));\n }\n\n if (settings.maxToolRounds !== undefined && Number.isFinite(Number(settings.maxToolRounds))) {\n commit('SET_MAX_TOOL_ROUNDS', Number(settings.maxToolRounds));\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider.\n //\n // Stale-while-revalidate: if cache exists, commit it INSTANTLY for zero\n // paint latency and simultaneously fire a background network refresh.\n // If the fresh result diverges from cache, commit the update and overwrite\n // localStorage. Frontend cache is now display-only — never authoritative,\n // never blocks new-model discovery.\n //\n // forceRefresh: true — caller (RefreshModelsButton, provider reconnect)\n // awaits the network round-trip and skips the SWR early-return.\n async fetchProviderModels({ commit, state, dispatch }, { provider, forceRefresh = false } = {}) {\n if (!provider) return [];\n\n // Route non-built-in providers to their dedicated fetchers. Without this,\n // callers (ModelSelector, AgentForge) that pass 'Local' or a custom-provider\n // UUID hit /api/models/<id>/models, which only knows built-in keys and 400s.\n if (String(provider).toLowerCase() === 'local') {\n return dispatch('fetchLocalModels', { forceRefresh });\n }\n if (state.customProviders.some((cp) => cp.id === provider)) {\n return dispatch('fetchCustomProviderModels', provider);\n }\n\n if (state.loadingModels[provider] && !forceRefresh) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n\n // Step 1: Instant paint from cache if available. No TTL gate — cache is\n // for first-paint speed only, revalidation below always runs.\n let cachedModels = null;\n const rawCached = localStorage.getItem(cacheKey);\n if (rawCached) {\n try {\n const parsed = JSON.parse(rawCached);\n cachedModels = parsed.models;\n if (Array.isArray(cachedModels)) {\n commit('SET_PROVIDER_MODELS', { provider, models: cachedModels });\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch { /* ignore malformed meta */ }\n }\n }\n } catch {\n // Malformed cache — nuke it, treat as no cache\n localStorage.removeItem(cacheKey);\n }\n }\n\n // Step 2: revalidate. If we already painted from cache and forceRefresh\n // is false, run in background and return cached immediately. Otherwise\n // await the network.\n const providerLower = resolveProviderKey(provider);\n\n const revalidate = async () => {\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli' || providerLower === 'antigravity' || providerLower === 'grok-build' || providerLower === 'cursor-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, { headers });\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n // Only mutate Vuex + localStorage if the list actually changed. Prevents\n // pointless renders and lets model-selector components diff cleanly.\n const changed = !cachedModels\n || cachedModels.length !== models.length\n || JSON.stringify(cachedModels) !== JSON.stringify(models);\n\n if (changed) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n localStorage.setItem(cacheKey, JSON.stringify({ models, timestamp: Date.now() }));\n if (cachedModels) {\n console.log(`[aiProvider] ${provider} model list changed on revalidate (${cachedModels.length} → ${models.length})`);\n }\n }\n\n // Metadata revalidation runs alongside models.\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch { /* non-critical */ }\n\n return models;\n };\n\n // Fast-path: we have cache AND forceRefresh is false. Return cached\n // instantly, revalidate in background. Loading indicator not shown —\n // the user already sees a populated dropdown.\n if (cachedModels && !forceRefresh) {\n revalidate().catch((err) => {\n console.warn(`[aiProvider] Background revalidate failed for ${provider}:`, err.message);\n });\n return cachedModels;\n }\n\n // Slow-path: no cache OR forceRefresh. Await the network.\n commit('SET_LOADING_MODELS', { provider, loading: true });\n try {\n const models = await revalidate();\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n // Last-resort fallback: any cached data is better than empty.\n if (cachedModels) return cachedModels;\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n //\n // With SWR in fetchProviderModels this is now an emergency escape hatch,\n // not a routine action — normal use ships fresh models within one refresh\n // cycle without any manual button press.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for BOTH the display-name and\n // lowercase spellings so no split-brain survives. Providers can be\n // referenced as 'OpenAI-Codex' (display) or 'openai-codex' (key) at\n // different call sites; belt-and-suspenders coverage kills that class.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n localStorage.removeItem(`${providerLower}_models`);\n localStorage.removeItem(`${providerLower}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code', 'grok-build', 'cursor-cli'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n fetchCustomProviders: withFreshness('aiProvider.fetchCustomProviders', async ({ commit, state }) => {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n }, { staleAfter: TTL.aiProviderFetchCustomProviders }),\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "ac022984cdd51cdf816b37b81ef3fe21497ff8e3": {"t": 1785754038.792294, "n": 54759, "h": "8e4c7b578806cc35", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\nimport { withFreshness } from '../_utils/withFreshness.js';\nimport { TTL } from '../_utils/freshnessConfig.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version fallback — used only if the backend's schema-version endpoint\n// is unreachable on boot (offline, backend not started). The real invalidator\n// is the schema hash returned by GET /api/models/schema-version, fetched on\n// module load and compared against the stored value. Any change to metadata\n// shape, recommended-model order, or fallback list auto-invalidates all\n// per-provider localStorage caches. Kills the manual bump-the-integer footgun.\nconst MODEL_CACHE_VERSION_FALLBACK = 12;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION_FALLBACK)) {\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION_FALLBACK));\n console.log('[aiProvider] Cleared stale model caches (fallback version upgrade)');\n }\n})();\n\n// Auto cache-bust via backend schema hash. Fetched once on module load and\n// again on every explicit refresh action. Silent failure = keep whatever was\n// cached; the 5-min SWR path below still keeps things fresh.\n(async () => {\n try {\n const res = await fetch(`${API_CONFIG.BASE_URL}/models/schema-version`);\n if (!res.ok) return;\n const data = await res.json();\n if (!data?.success || !data.version) return;\n const stored = localStorage.getItem('model_schema_hash');\n if (stored !== data.version) {\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_schema_hash', data.version);\n console.log(`[aiProvider] Cleared stale model caches (backend schema hash changed to ${data.version})`);\n }\n } catch {\n // Backend not reachable on module load — fine, fallback version handles it.\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'antigravity', displayName: 'Antigravity' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'grok-build', displayName: 'Grok-Build' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\n// MIRROR of backend/src/services/ai/reasoningModels.js — keep regexes in sync.\n// Matches claude-opus-4-6+, claude-sonnet-4-6+ (auto-covers 4.7 / 4.8 / 4.9 / 4.10+),\n// plus the always-on Fable / Mythos family.\nconst ANTHROPIC_VERSIONED_REASONING_RE = /^claude-(opus|sonnet)-4-([6-9]|[1-9]\\d{1,2})(?:-|$)/;\nconst ANTHROPIC_FAMILY_REASONING_RE = /^claude-(fable|mythos)-/;\nconst ANTHROPIC_XHIGH_RE = /^claude-(opus-4-([7-9]|[1-9]\\d{1,2})(?:-|$)|fable-|mythos-)/;\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return ANTHROPIC_VERSIONED_REASONING_RE.test(lower) || ANTHROPIC_FAMILY_REASONING_RE.test(lower);\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n if (!lower.startsWith('anthropic/')) return false;\n return isAnthropicReasoningModel(lower.slice('anthropic/'.length));\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (ANTHROPIC_XHIGH_RE.test(lowerModel)) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli' || lowerProvider === 'antigravity') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n // Per-user \"Async tool execution\" toggle. Default FALSE — async tool\n // execution is currently an experimental opt-in feature. Users enable\n // it in Settings → AI Provider. localStorage is the source of truth on\n // the frontend so the toggle survives reloads; loadUserSettings()\n // reconciles it with the backend value on session start.\n asyncToolsEnabled: localStorage.getItem('asyncToolsEnabled') === 'true',\n // Per-user hard cap on tool result size returned to the LLM. Default\n // 100000 matches the historical OrchestratorService MAX_TOOL_RESULT_CHARS.\n // localStorage is the source of truth on the frontend so the value\n // survives reloads; loadUserSettings() reconciles with the backend.\n toolOutputCap: (() => {\n const raw = Number(localStorage.getItem('toolOutputCap'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100000;\n })(),\n // Per-user cap on tool-loop rounds per chat turn. Default 100 matches the\n // historical orchestrator/agent/tool/widget/goal cap. When set, overrides\n // every chat surface uniformly (including workflow/artifact which default\n // to 25 server-side).\n maxToolRounds: (() => {\n const raw = Number(localStorage.getItem('maxToolRounds'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100;\n })(),\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n SET_ASYNC_TOOLS_ENABLED(state, enabled) {\n const value = Boolean(enabled);\n state.asyncToolsEnabled = value;\n // Persist explicitly even when true, so a deliberate \"leave it on\"\n // setting is preserved across reloads (rather than relying on the\n // localStorage-missing-key default and tripping over future default\n // changes).\n localStorage.setItem('asyncToolsEnabled', value ? 'true' : 'false');\n },\n SET_TOOL_OUTPUT_CAP(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100000;\n state.toolOutputCap = clean;\n localStorage.setItem('toolOutputCap', String(clean));\n },\n SET_MAX_TOOL_ROUNDS(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100;\n state.maxToolRounds = clean;\n localStorage.setItem('maxToolRounds', String(clean));\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setAsyncToolsEnabled({ commit }, enabled) {\n const value = Boolean(enabled);\n commit('SET_ASYNC_TOOLS_ENABLED', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n asyncToolsEnabled: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync async tools toggle with backend:', error);\n }\n },\n\n async setToolOutputCap({ commit }, newCap) {\n // Clamp to the same window the backend validates against — keeps the\n // PUT from 400-ing on out-of-range values.\n const raw = Number(newCap);\n const clamped = Number.isFinite(raw)\n ? Math.max(25000, Math.min(500000, Math.round(raw)))\n : 100000;\n commit('SET_TOOL_OUTPUT_CAP', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n toolOutputCap: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync tool output cap with backend:', error);\n }\n },\n\n async setMaxToolRounds({ commit }, newRounds) {\n // Clamp to the backend-validated [1, 999999] window.\n const raw = Number(newRounds);\n const clamped = Number.isFinite(raw)\n ? Math.max(1, Math.min(999999, Math.round(raw)))\n : 100;\n commit('SET_MAX_TOOL_ROUNDS', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n maxToolRounds: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync max tool rounds with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (settings.asyncToolsEnabled !== undefined) {\n commit('SET_ASYNC_TOOLS_ENABLED', settings.asyncToolsEnabled === true);\n }\n\n if (settings.toolOutputCap !== undefined && Number.isFinite(Number(settings.toolOutputCap))) {\n commit('SET_TOOL_OUTPUT_CAP', Number(settings.toolOutputCap));\n }\n\n if (settings.maxToolRounds !== undefined && Number.isFinite(Number(settings.maxToolRounds))) {\n commit('SET_MAX_TOOL_ROUNDS', Number(settings.maxToolRounds));\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider.\n //\n // Stale-while-revalidate: if cache exists, commit it INSTANTLY for zero\n // paint latency and simultaneously fire a background network refresh.\n // If the fresh result diverges from cache, commit the update and overwrite\n // localStorage. Frontend cache is now display-only — never authoritative,\n // never blocks new-model discovery.\n //\n // forceRefresh: true — caller (RefreshModelsButton, provider reconnect)\n // awaits the network round-trip and skips the SWR early-return.\n async fetchProviderModels({ commit, state, dispatch }, { provider, forceRefresh = false } = {}) {\n if (!provider) return [];\n\n // Route non-built-in providers to their dedicated fetchers. Without this,\n // callers (ModelSelector, AgentForge) that pass 'Local' or a custom-provider\n // UUID hit /api/models/<id>/models, which only knows built-in keys and 400s.\n if (String(provider).toLowerCase() === 'local') {\n return dispatch('fetchLocalModels', { forceRefresh });\n }\n if (state.customProviders.some((cp) => cp.id === provider)) {\n return dispatch('fetchCustomProviderModels', provider);\n }\n\n if (state.loadingModels[provider] && !forceRefresh) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n\n // Step 1: Instant paint from cache if available. No TTL gate — cache is\n // for first-paint speed only, revalidation below always runs.\n let cachedModels = null;\n const rawCached = localStorage.getItem(cacheKey);\n if (rawCached) {\n try {\n const parsed = JSON.parse(rawCached);\n cachedModels = parsed.models;\n if (Array.isArray(cachedModels)) {\n commit('SET_PROVIDER_MODELS', { provider, models: cachedModels });\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch { /* ignore malformed meta */ }\n }\n }\n } catch {\n // Malformed cache — nuke it, treat as no cache\n localStorage.removeItem(cacheKey);\n }\n }\n\n // Step 2: revalidate. If we already painted from cache and forceRefresh\n // is false, run in background and return cached immediately. Otherwise\n // await the network.\n const providerLower = resolveProviderKey(provider);\n\n const revalidate = async () => {\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli' || providerLower === 'antigravity' || providerLower === 'grok-build';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, { headers });\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n // Only mutate Vuex + localStorage if the list actually changed. Prevents\n // pointless renders and lets model-selector components diff cleanly.\n const changed = !cachedModels\n || cachedModels.length !== models.length\n || JSON.stringify(cachedModels) !== JSON.stringify(models);\n\n if (changed) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n localStorage.setItem(cacheKey, JSON.stringify({ models, timestamp: Date.now() }));\n if (cachedModels) {\n console.log(`[aiProvider] ${provider} model list changed on revalidate (${cachedModels.length} → ${models.length})`);\n }\n }\n\n // Metadata revalidation runs alongside models.\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch { /* non-critical */ }\n\n return models;\n };\n\n // Fast-path: we have cache AND forceRefresh is false. Return cached\n // instantly, revalidate in background. Loading indicator not shown —\n // the user already sees a populated dropdown.\n if (cachedModels && !forceRefresh) {\n revalidate().catch((err) => {\n console.warn(`[aiProvider] Background revalidate failed for ${provider}:`, err.message);\n });\n return cachedModels;\n }\n\n // Slow-path: no cache OR forceRefresh. Await the network.\n commit('SET_LOADING_MODELS', { provider, loading: true });\n try {\n const models = await revalidate();\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n // Last-resort fallback: any cached data is better than empty.\n if (cachedModels) return cachedModels;\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n //\n // With SWR in fetchProviderModels this is now an emergency escape hatch,\n // not a routine action — normal use ships fresh models within one refresh\n // cycle without any manual button press.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for BOTH the display-name and\n // lowercase spellings so no split-brain survives. Providers can be\n // referenced as 'OpenAI-Codex' (display) or 'openai-codex' (key) at\n // different call sites; belt-and-suspenders coverage kills that class.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n localStorage.removeItem(`${providerLower}_models`);\n localStorage.removeItem(`${providerLower}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code', 'grok-build'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n fetchCustomProviders: withFreshness('aiProvider.fetchCustomProviders', async ({ commit, state }) => {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n }, { staleAfter: TTL.aiProviderFetchCustomProviders }),\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "fc40f2247898d4c59bcfa3a545e1a93e1fea2f22": {"t": 1785754041.328521, "n": 54659, "h": "ade3b3d167717589", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\nimport { withFreshness } from '../_utils/withFreshness.js';\nimport { TTL } from '../_utils/freshnessConfig.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version fallback — used only if the backend's schema-version endpoint\n// is unreachable on boot (offline, backend not started). The real invalidator\n// is the schema hash returned by GET /api/models/schema-version, fetched on\n// module load and compared against the stored value. Any change to metadata\n// shape, recommended-model order, or fallback list auto-invalidates all\n// per-provider localStorage caches. Kills the manual bump-the-integer footgun.\nconst MODEL_CACHE_VERSION_FALLBACK = 12;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION_FALLBACK)) {\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION_FALLBACK));\n console.log('[aiProvider] Cleared stale model caches (fallback version upgrade)');\n }\n})();\n\n// Auto cache-bust via backend schema hash. Fetched once on module load and\n// again on every explicit refresh action. Silent failure = keep whatever was\n// cached; the 5-min SWR path below still keeps things fresh.\n(async () => {\n try {\n const res = await fetch(`${API_CONFIG.BASE_URL}/models/schema-version`);\n if (!res.ok) return;\n const data = await res.json();\n if (!data?.success || !data.version) return;\n const stored = localStorage.getItem('model_schema_hash');\n if (stored !== data.version) {\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_schema_hash', data.version);\n console.log(`[aiProvider] Cleared stale model caches (backend schema hash changed to ${data.version})`);\n }\n } catch {\n // Backend not reachable on module load — fine, fallback version handles it.\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'antigravity', displayName: 'Antigravity' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\n// MIRROR of backend/src/services/ai/reasoningModels.js — keep regexes in sync.\n// Matches claude-opus-4-6+, claude-sonnet-4-6+ (auto-covers 4.7 / 4.8 / 4.9 / 4.10+),\n// plus the always-on Fable / Mythos family.\nconst ANTHROPIC_VERSIONED_REASONING_RE = /^claude-(opus|sonnet)-4-([6-9]|[1-9]\\d{1,2})(?:-|$)/;\nconst ANTHROPIC_FAMILY_REASONING_RE = /^claude-(fable|mythos)-/;\nconst ANTHROPIC_XHIGH_RE = /^claude-(opus-4-([7-9]|[1-9]\\d{1,2})(?:-|$)|fable-|mythos-)/;\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return ANTHROPIC_VERSIONED_REASONING_RE.test(lower) || ANTHROPIC_FAMILY_REASONING_RE.test(lower);\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n if (!lower.startsWith('anthropic/')) return false;\n return isAnthropicReasoningModel(lower.slice('anthropic/'.length));\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (ANTHROPIC_XHIGH_RE.test(lowerModel)) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli' || lowerProvider === 'antigravity') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n // Per-user \"Async tool execution\" toggle. Default FALSE — async tool\n // execution is currently an experimental opt-in feature. Users enable\n // it in Settings → AI Provider. localStorage is the source of truth on\n // the frontend so the toggle survives reloads; loadUserSettings()\n // reconciles it with the backend value on session start.\n asyncToolsEnabled: localStorage.getItem('asyncToolsEnabled') === 'true',\n // Per-user hard cap on tool result size returned to the LLM. Default\n // 100000 matches the historical OrchestratorService MAX_TOOL_RESULT_CHARS.\n // localStorage is the source of truth on the frontend so the value\n // survives reloads; loadUserSettings() reconciles with the backend.\n toolOutputCap: (() => {\n const raw = Number(localStorage.getItem('toolOutputCap'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100000;\n })(),\n // Per-user cap on tool-loop rounds per chat turn. Default 100 matches the\n // historical orchestrator/agent/tool/widget/goal cap. When set, overrides\n // every chat surface uniformly (including workflow/artifact which default\n // to 25 server-side).\n maxToolRounds: (() => {\n const raw = Number(localStorage.getItem('maxToolRounds'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100;\n })(),\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n SET_ASYNC_TOOLS_ENABLED(state, enabled) {\n const value = Boolean(enabled);\n state.asyncToolsEnabled = value;\n // Persist explicitly even when true, so a deliberate \"leave it on\"\n // setting is preserved across reloads (rather than relying on the\n // localStorage-missing-key default and tripping over future default\n // changes).\n localStorage.setItem('asyncToolsEnabled', value ? 'true' : 'false');\n },\n SET_TOOL_OUTPUT_CAP(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100000;\n state.toolOutputCap = clean;\n localStorage.setItem('toolOutputCap', String(clean));\n },\n SET_MAX_TOOL_ROUNDS(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100;\n state.maxToolRounds = clean;\n localStorage.setItem('maxToolRounds', String(clean));\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setAsyncToolsEnabled({ commit }, enabled) {\n const value = Boolean(enabled);\n commit('SET_ASYNC_TOOLS_ENABLED', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n asyncToolsEnabled: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync async tools toggle with backend:', error);\n }\n },\n\n async setToolOutputCap({ commit }, newCap) {\n // Clamp to the same window the backend validates against — keeps the\n // PUT from 400-ing on out-of-range values.\n const raw = Number(newCap);\n const clamped = Number.isFinite(raw)\n ? Math.max(25000, Math.min(500000, Math.round(raw)))\n : 100000;\n commit('SET_TOOL_OUTPUT_CAP', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n toolOutputCap: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync tool output cap with backend:', error);\n }\n },\n\n async setMaxToolRounds({ commit }, newRounds) {\n // Clamp to the backend-validated [1, 999999] window.\n const raw = Number(newRounds);\n const clamped = Number.isFinite(raw)\n ? Math.max(1, Math.min(999999, Math.round(raw)))\n : 100;\n commit('SET_MAX_TOOL_ROUNDS', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n maxToolRounds: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync max tool rounds with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (settings.asyncToolsEnabled !== undefined) {\n commit('SET_ASYNC_TOOLS_ENABLED', settings.asyncToolsEnabled === true);\n }\n\n if (settings.toolOutputCap !== undefined && Number.isFinite(Number(settings.toolOutputCap))) {\n commit('SET_TOOL_OUTPUT_CAP', Number(settings.toolOutputCap));\n }\n\n if (settings.maxToolRounds !== undefined && Number.isFinite(Number(settings.maxToolRounds))) {\n commit('SET_MAX_TOOL_ROUNDS', Number(settings.maxToolRounds));\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider.\n //\n // Stale-while-revalidate: if cache exists, commit it INSTANTLY for zero\n // paint latency and simultaneously fire a background network refresh.\n // If the fresh result diverges from cache, commit the update and overwrite\n // localStorage. Frontend cache is now display-only — never authoritative,\n // never blocks new-model discovery.\n //\n // forceRefresh: true — caller (RefreshModelsButton, provider reconnect)\n // awaits the network round-trip and skips the SWR early-return.\n async fetchProviderModels({ commit, state, dispatch }, { provider, forceRefresh = false } = {}) {\n if (!provider) return [];\n\n // Route non-built-in providers to their dedicated fetchers. Without this,\n // callers (ModelSelector, AgentForge) that pass 'Local' or a custom-provider\n // UUID hit /api/models/<id>/models, which only knows built-in keys and 400s.\n if (String(provider).toLowerCase() === 'local') {\n return dispatch('fetchLocalModels', { forceRefresh });\n }\n if (state.customProviders.some((cp) => cp.id === provider)) {\n return dispatch('fetchCustomProviderModels', provider);\n }\n\n if (state.loadingModels[provider] && !forceRefresh) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n\n // Step 1: Instant paint from cache if available. No TTL gate — cache is\n // for first-paint speed only, revalidation below always runs.\n let cachedModels = null;\n const rawCached = localStorage.getItem(cacheKey);\n if (rawCached) {\n try {\n const parsed = JSON.parse(rawCached);\n cachedModels = parsed.models;\n if (Array.isArray(cachedModels)) {\n commit('SET_PROVIDER_MODELS', { provider, models: cachedModels });\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch { /* ignore malformed meta */ }\n }\n }\n } catch {\n // Malformed cache — nuke it, treat as no cache\n localStorage.removeItem(cacheKey);\n }\n }\n\n // Step 2: revalidate. If we already painted from cache and forceRefresh\n // is false, run in background and return cached immediately. Otherwise\n // await the network.\n const providerLower = resolveProviderKey(provider);\n\n const revalidate = async () => {\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli' || providerLower === 'antigravity';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, { headers });\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n // Only mutate Vuex + localStorage if the list actually changed. Prevents\n // pointless renders and lets model-selector components diff cleanly.\n const changed = !cachedModels\n || cachedModels.length !== models.length\n || JSON.stringify(cachedModels) !== JSON.stringify(models);\n\n if (changed) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n localStorage.setItem(cacheKey, JSON.stringify({ models, timestamp: Date.now() }));\n if (cachedModels) {\n console.log(`[aiProvider] ${provider} model list changed on revalidate (${cachedModels.length} → ${models.length})`);\n }\n }\n\n // Metadata revalidation runs alongside models.\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch { /* non-critical */ }\n\n return models;\n };\n\n // Fast-path: we have cache AND forceRefresh is false. Return cached\n // instantly, revalidate in background. Loading indicator not shown —\n // the user already sees a populated dropdown.\n if (cachedModels && !forceRefresh) {\n revalidate().catch((err) => {\n console.warn(`[aiProvider] Background revalidate failed for ${provider}:`, err.message);\n });\n return cachedModels;\n }\n\n // Slow-path: no cache OR forceRefresh. Await the network.\n commit('SET_LOADING_MODELS', { provider, loading: true });\n try {\n const models = await revalidate();\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n // Last-resort fallback: any cached data is better than empty.\n if (cachedModels) return cachedModels;\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n //\n // With SWR in fetchProviderModels this is now an emergency escape hatch,\n // not a routine action — normal use ships fresh models within one refresh\n // cycle without any manual button press.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for BOTH the display-name and\n // lowercase spellings so no split-brain survives. Providers can be\n // referenced as 'OpenAI-Codex' (display) or 'openai-codex' (key) at\n // different call sites; belt-and-suspenders coverage kills that class.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n localStorage.removeItem(`${providerLower}_models`);\n localStorage.removeItem(`${providerLower}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n fetchCustomProviders: withFreshness('aiProvider.fetchCustomProviders', async ({ commit, state }) => {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n }, { staleAfter: TTL.aiProviderFetchCustomProviders }),\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "194c55459958da611f917ddd7a76066db12a1346": {"t": 1785754042.7062542, "n": 52159, "h": "715d9149dfba9203", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\nimport { withFreshness } from '../_utils/withFreshness.js';\nimport { TTL } from '../_utils/freshnessConfig.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches.\n// v11: Z.AI dropdown adds GLM-5.2 (1M context) + GLM-5.2 reasoning_effort control (high/max).\n// v10: Anthropic dropdown reordered (Fable 5 first, Opus 4.8 second) + Fable 5 / Opus 4.8 metadata added.\n// v9: chutes reasoning controls added — old cached metadata lacks reasoningControl.\nconst MODEL_CACHE_VERSION = 11;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'antigravity', displayName: 'Antigravity' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\n// MIRROR of backend/src/services/ai/reasoningModels.js — keep regexes in sync.\n// Matches claude-opus-4-6+, claude-sonnet-4-6+ (auto-covers 4.7 / 4.8 / 4.9 / 4.10+),\n// plus the always-on Fable / Mythos family.\nconst ANTHROPIC_VERSIONED_REASONING_RE = /^claude-(opus|sonnet)-4-([6-9]|[1-9]\\d{1,2})(?:-|$)/;\nconst ANTHROPIC_FAMILY_REASONING_RE = /^claude-(fable|mythos)-/;\nconst ANTHROPIC_XHIGH_RE = /^claude-(opus-4-([7-9]|[1-9]\\d{1,2})(?:-|$)|fable-|mythos-)/;\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return ANTHROPIC_VERSIONED_REASONING_RE.test(lower) || ANTHROPIC_FAMILY_REASONING_RE.test(lower);\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n if (!lower.startsWith('anthropic/')) return false;\n return isAnthropicReasoningModel(lower.slice('anthropic/'.length));\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (ANTHROPIC_XHIGH_RE.test(lowerModel)) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli' || lowerProvider === 'antigravity') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n // Per-user \"Async tool execution\" toggle. Default FALSE — async tool\n // execution is currently an experimental opt-in feature. Users enable\n // it in Settings → AI Provider. localStorage is the source of truth on\n // the frontend so the toggle survives reloads; loadUserSettings()\n // reconciles it with the backend value on session start.\n asyncToolsEnabled: localStorage.getItem('asyncToolsEnabled') === 'true',\n // Per-user hard cap on tool result size returned to the LLM. Default\n // 100000 matches the historical OrchestratorService MAX_TOOL_RESULT_CHARS.\n // localStorage is the source of truth on the frontend so the value\n // survives reloads; loadUserSettings() reconciles with the backend.\n toolOutputCap: (() => {\n const raw = Number(localStorage.getItem('toolOutputCap'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100000;\n })(),\n // Per-user cap on tool-loop rounds per chat turn. Default 100 matches the\n // historical orchestrator/agent/tool/widget/goal cap. When set, overrides\n // every chat surface uniformly (including workflow/artifact which default\n // to 25 server-side).\n maxToolRounds: (() => {\n const raw = Number(localStorage.getItem('maxToolRounds'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100;\n })(),\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n SET_ASYNC_TOOLS_ENABLED(state, enabled) {\n const value = Boolean(enabled);\n state.asyncToolsEnabled = value;\n // Persist explicitly even when true, so a deliberate \"leave it on\"\n // setting is preserved across reloads (rather than relying on the\n // localStorage-missing-key default and tripping over future default\n // changes).\n localStorage.setItem('asyncToolsEnabled', value ? 'true' : 'false');\n },\n SET_TOOL_OUTPUT_CAP(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100000;\n state.toolOutputCap = clean;\n localStorage.setItem('toolOutputCap', String(clean));\n },\n SET_MAX_TOOL_ROUNDS(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100;\n state.maxToolRounds = clean;\n localStorage.setItem('maxToolRounds', String(clean));\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setAsyncToolsEnabled({ commit }, enabled) {\n const value = Boolean(enabled);\n commit('SET_ASYNC_TOOLS_ENABLED', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n asyncToolsEnabled: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync async tools toggle with backend:', error);\n }\n },\n\n async setToolOutputCap({ commit }, newCap) {\n // Clamp to the same window the backend validates against — keeps the\n // PUT from 400-ing on out-of-range values.\n const raw = Number(newCap);\n const clamped = Number.isFinite(raw)\n ? Math.max(25000, Math.min(500000, Math.round(raw)))\n : 100000;\n commit('SET_TOOL_OUTPUT_CAP', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n toolOutputCap: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync tool output cap with backend:', error);\n }\n },\n\n async setMaxToolRounds({ commit }, newRounds) {\n // Clamp to the backend-validated [1, 999999] window.\n const raw = Number(newRounds);\n const clamped = Number.isFinite(raw)\n ? Math.max(1, Math.min(999999, Math.round(raw)))\n : 100;\n commit('SET_MAX_TOOL_ROUNDS', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n maxToolRounds: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync max tool rounds with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (settings.asyncToolsEnabled !== undefined) {\n commit('SET_ASYNC_TOOLS_ENABLED', settings.asyncToolsEnabled === true);\n }\n\n if (settings.toolOutputCap !== undefined && Number.isFinite(Number(settings.toolOutputCap))) {\n commit('SET_TOOL_OUTPUT_CAP', Number(settings.toolOutputCap));\n }\n\n if (settings.maxToolRounds !== undefined && Number.isFinite(Number(settings.maxToolRounds))) {\n commit('SET_MAX_TOOL_ROUNDS', Number(settings.maxToolRounds));\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state, dispatch }, { provider, forceRefresh = false } = {}) {\n if (!provider) return [];\n\n // Route non-built-in providers to their dedicated fetchers. Without this,\n // callers (ModelSelector, AgentForge) that pass 'Local' or a custom-provider\n // UUID hit /api/models/<id>/models, which only knows built-in keys and 400s.\n if (String(provider).toLowerCase() === 'local') {\n return dispatch('fetchLocalModels', { forceRefresh });\n }\n if (state.customProviders.some((cp) => cp.id === provider)) {\n return dispatch('fetchCustomProviderModels', provider);\n }\n\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli' || providerLower === 'antigravity';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for this provider.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n fetchCustomProviders: withFreshness('aiProvider.fetchCustomProviders', async ({ commit, state }) => {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n }, { staleAfter: TTL.aiProviderFetchCustomProviders }),\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "15af34f7e68cccbfa5d6bb2744cf5b037975f1a7": {"t": 1785754044.2494948, "n": 52035, "h": "447f066e0fc316ba", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\nimport { withFreshness } from '../_utils/withFreshness.js';\nimport { TTL } from '../_utils/freshnessConfig.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches.\n// v11: Z.AI dropdown adds GLM-5.2 (1M context) + GLM-5.2 reasoning_effort control (high/max).\n// v10: Anthropic dropdown reordered (Fable 5 first, Opus 4.8 second) + Fable 5 / Opus 4.8 metadata added.\n// v9: chutes reasoning controls added — old cached metadata lacks reasoningControl.\nconst MODEL_CACHE_VERSION = 11;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\n// MIRROR of backend/src/services/ai/reasoningModels.js — keep regexes in sync.\n// Matches claude-opus-4-6+, claude-sonnet-4-6+ (auto-covers 4.7 / 4.8 / 4.9 / 4.10+),\n// plus the always-on Fable / Mythos family.\nconst ANTHROPIC_VERSIONED_REASONING_RE = /^claude-(opus|sonnet)-4-([6-9]|[1-9]\\d{1,2})(?:-|$)/;\nconst ANTHROPIC_FAMILY_REASONING_RE = /^claude-(fable|mythos)-/;\nconst ANTHROPIC_XHIGH_RE = /^claude-(opus-4-([7-9]|[1-9]\\d{1,2})(?:-|$)|fable-|mythos-)/;\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return ANTHROPIC_VERSIONED_REASONING_RE.test(lower) || ANTHROPIC_FAMILY_REASONING_RE.test(lower);\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n if (!lower.startsWith('anthropic/')) return false;\n return isAnthropicReasoningModel(lower.slice('anthropic/'.length));\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (ANTHROPIC_XHIGH_RE.test(lowerModel)) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n // Per-user \"Async tool execution\" toggle. Default FALSE — async tool\n // execution is currently an experimental opt-in feature. Users enable\n // it in Settings → AI Provider. localStorage is the source of truth on\n // the frontend so the toggle survives reloads; loadUserSettings()\n // reconciles it with the backend value on session start.\n asyncToolsEnabled: localStorage.getItem('asyncToolsEnabled') === 'true',\n // Per-user hard cap on tool result size returned to the LLM. Default\n // 100000 matches the historical OrchestratorService MAX_TOOL_RESULT_CHARS.\n // localStorage is the source of truth on the frontend so the value\n // survives reloads; loadUserSettings() reconciles with the backend.\n toolOutputCap: (() => {\n const raw = Number(localStorage.getItem('toolOutputCap'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100000;\n })(),\n // Per-user cap on tool-loop rounds per chat turn. Default 100 matches the\n // historical orchestrator/agent/tool/widget/goal cap. When set, overrides\n // every chat surface uniformly (including workflow/artifact which default\n // to 25 server-side).\n maxToolRounds: (() => {\n const raw = Number(localStorage.getItem('maxToolRounds'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100;\n })(),\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n SET_ASYNC_TOOLS_ENABLED(state, enabled) {\n const value = Boolean(enabled);\n state.asyncToolsEnabled = value;\n // Persist explicitly even when true, so a deliberate \"leave it on\"\n // setting is preserved across reloads (rather than relying on the\n // localStorage-missing-key default and tripping over future default\n // changes).\n localStorage.setItem('asyncToolsEnabled', value ? 'true' : 'false');\n },\n SET_TOOL_OUTPUT_CAP(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100000;\n state.toolOutputCap = clean;\n localStorage.setItem('toolOutputCap', String(clean));\n },\n SET_MAX_TOOL_ROUNDS(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100;\n state.maxToolRounds = clean;\n localStorage.setItem('maxToolRounds', String(clean));\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setAsyncToolsEnabled({ commit }, enabled) {\n const value = Boolean(enabled);\n commit('SET_ASYNC_TOOLS_ENABLED', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n asyncToolsEnabled: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync async tools toggle with backend:', error);\n }\n },\n\n async setToolOutputCap({ commit }, newCap) {\n // Clamp to the same window the backend validates against — keeps the\n // PUT from 400-ing on out-of-range values.\n const raw = Number(newCap);\n const clamped = Number.isFinite(raw)\n ? Math.max(25000, Math.min(500000, Math.round(raw)))\n : 100000;\n commit('SET_TOOL_OUTPUT_CAP', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n toolOutputCap: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync tool output cap with backend:', error);\n }\n },\n\n async setMaxToolRounds({ commit }, newRounds) {\n // Clamp to the backend-validated [1, 999999] window.\n const raw = Number(newRounds);\n const clamped = Number.isFinite(raw)\n ? Math.max(1, Math.min(999999, Math.round(raw)))\n : 100;\n commit('SET_MAX_TOOL_ROUNDS', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n maxToolRounds: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync max tool rounds with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (settings.asyncToolsEnabled !== undefined) {\n commit('SET_ASYNC_TOOLS_ENABLED', settings.asyncToolsEnabled === true);\n }\n\n if (settings.toolOutputCap !== undefined && Number.isFinite(Number(settings.toolOutputCap))) {\n commit('SET_TOOL_OUTPUT_CAP', Number(settings.toolOutputCap));\n }\n\n if (settings.maxToolRounds !== undefined && Number.isFinite(Number(settings.maxToolRounds))) {\n commit('SET_MAX_TOOL_ROUNDS', Number(settings.maxToolRounds));\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state, dispatch }, { provider, forceRefresh = false } = {}) {\n if (!provider) return [];\n\n // Route non-built-in providers to their dedicated fetchers. Without this,\n // callers (ModelSelector, AgentForge) that pass 'Local' or a custom-provider\n // UUID hit /api/models/<id>/models, which only knows built-in keys and 400s.\n if (String(provider).toLowerCase() === 'local') {\n return dispatch('fetchLocalModels', { forceRefresh });\n }\n if (state.customProviders.some((cp) => cp.id === provider)) {\n return dispatch('fetchCustomProviderModels', provider);\n }\n\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for this provider.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n fetchCustomProviders: withFreshness('aiProvider.fetchCustomProviders', async ({ commit, state }) => {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n }, { staleAfter: TTL.aiProviderFetchCustomProviders }),\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "2f4cf22af1e09f454025a6a5cbf6d2935518f08b": {"t": 1785754046.1603825, "n": 48079, "h": "8f8aa1ac5a67161c", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\nimport { withFreshness } from '../_utils/withFreshness.js';\nimport { TTL } from '../_utils/freshnessConfig.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches.\n// v11: Z.AI dropdown adds GLM-5.2 (1M context) + GLM-5.2 reasoning_effort control (high/max).\n// v10: Anthropic dropdown reordered (Fable 5 first, Opus 4.8 second) + Fable 5 / Opus 4.8 metadata added.\n// v9: chutes reasoning controls added — old cached metadata lacks reasoningControl.\nconst MODEL_CACHE_VERSION = 11;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\n// MIRROR of backend/src/services/ai/reasoningModels.js — keep regexes in sync.\n// Matches claude-opus-4-6+, claude-sonnet-4-6+ (auto-covers 4.7 / 4.8 / 4.9 / 4.10+),\n// plus the always-on Fable / Mythos family.\nconst ANTHROPIC_VERSIONED_REASONING_RE = /^claude-(opus|sonnet)-4-([6-9]|[1-9]\\d{1,2})(?:-|$)/;\nconst ANTHROPIC_FAMILY_REASONING_RE = /^claude-(fable|mythos)-/;\nconst ANTHROPIC_XHIGH_RE = /^claude-(opus-4-([7-9]|[1-9]\\d{1,2})(?:-|$)|fable-|mythos-)/;\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return ANTHROPIC_VERSIONED_REASONING_RE.test(lower) || ANTHROPIC_FAMILY_REASONING_RE.test(lower);\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n if (!lower.startsWith('anthropic/')) return false;\n return isAnthropicReasoningModel(lower.slice('anthropic/'.length));\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (ANTHROPIC_XHIGH_RE.test(lowerModel)) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n // Per-user \"Async tool execution\" toggle. Default FALSE — async tool\n // execution is currently an experimental opt-in feature. Users enable\n // it in Settings → AI Provider. localStorage is the source of truth on\n // the frontend so the toggle survives reloads; loadUserSettings()\n // reconciles it with the backend value on session start.\n asyncToolsEnabled: localStorage.getItem('asyncToolsEnabled') === 'true',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n SET_ASYNC_TOOLS_ENABLED(state, enabled) {\n const value = Boolean(enabled);\n state.asyncToolsEnabled = value;\n // Persist explicitly even when true, so a deliberate \"leave it on\"\n // setting is preserved across reloads (rather than relying on the\n // localStorage-missing-key default and tripping over future default\n // changes).\n localStorage.setItem('asyncToolsEnabled', value ? 'true' : 'false');\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setAsyncToolsEnabled({ commit }, enabled) {\n const value = Boolean(enabled);\n commit('SET_ASYNC_TOOLS_ENABLED', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n asyncToolsEnabled: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync async tools toggle with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (settings.asyncToolsEnabled !== undefined) {\n commit('SET_ASYNC_TOOLS_ENABLED', settings.asyncToolsEnabled === true);\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state, dispatch }, { provider, forceRefresh = false } = {}) {\n if (!provider) return [];\n\n // Route non-built-in providers to their dedicated fetchers. Without this,\n // callers (ModelSelector, AgentForge) that pass 'Local' or a custom-provider\n // UUID hit /api/models/<id>/models, which only knows built-in keys and 400s.\n if (String(provider).toLowerCase() === 'local') {\n return dispatch('fetchLocalModels', { forceRefresh });\n }\n if (state.customProviders.some((cp) => cp.id === provider)) {\n return dispatch('fetchCustomProviderModels', provider);\n }\n\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for this provider.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n fetchCustomProviders: withFreshness('aiProvider.fetchCustomProviders', async ({ commit, state }) => {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n }, { staleAfter: TTL.aiProviderFetchCustomProviders }),\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "ec5c54b92c7412852b9981a0cfce187c5cb54cba": {"t": 1785754047.7019904, "n": 47679, "h": "f9ff33667ecc6a4f", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\nimport { withFreshness } from '../_utils/withFreshness.js';\nimport { TTL } from '../_utils/freshnessConfig.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches.\n// v11: Z.AI dropdown adds GLM-5.2 (1M context) + GLM-5.2 reasoning_effort control (high/max).\n// v10: Anthropic dropdown reordered (Fable 5 first, Opus 4.8 second) + Fable 5 / Opus 4.8 metadata added.\n// v9: chutes reasoning controls added — old cached metadata lacks reasoningControl.\nconst MODEL_CACHE_VERSION = 11;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('claude-opus-4-7') || lower.startsWith('claude-opus-4-6') || lower.startsWith('claude-sonnet-4-6');\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('anthropic/claude-opus-4-7') || lower.startsWith('anthropic/claude-opus-4-6') || lower.startsWith('anthropic/claude-sonnet-4-6');\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (lowerModel.startsWith('claude-opus-4-7')) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n // Per-user \"Async tool execution\" toggle. Default FALSE — async tool\n // execution is currently an experimental opt-in feature. Users enable\n // it in Settings → AI Provider. localStorage is the source of truth on\n // the frontend so the toggle survives reloads; loadUserSettings()\n // reconciles it with the backend value on session start.\n asyncToolsEnabled: localStorage.getItem('asyncToolsEnabled') === 'true',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n SET_ASYNC_TOOLS_ENABLED(state, enabled) {\n const value = Boolean(enabled);\n state.asyncToolsEnabled = value;\n // Persist explicitly even when true, so a deliberate \"leave it on\"\n // setting is preserved across reloads (rather than relying on the\n // localStorage-missing-key default and tripping over future default\n // changes).\n localStorage.setItem('asyncToolsEnabled', value ? 'true' : 'false');\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setAsyncToolsEnabled({ commit }, enabled) {\n const value = Boolean(enabled);\n commit('SET_ASYNC_TOOLS_ENABLED', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n asyncToolsEnabled: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync async tools toggle with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (settings.asyncToolsEnabled !== undefined) {\n commit('SET_ASYNC_TOOLS_ENABLED', settings.asyncToolsEnabled === true);\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state, dispatch }, { provider, forceRefresh = false } = {}) {\n if (!provider) return [];\n\n // Route non-built-in providers to their dedicated fetchers. Without this,\n // callers (ModelSelector, AgentForge) that pass 'Local' or a custom-provider\n // UUID hit /api/models/<id>/models, which only knows built-in keys and 400s.\n if (String(provider).toLowerCase() === 'local') {\n return dispatch('fetchLocalModels', { forceRefresh });\n }\n if (state.customProviders.some((cp) => cp.id === provider)) {\n return dispatch('fetchCustomProviderModels', provider);\n }\n\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for this provider.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n fetchCustomProviders: withFreshness('aiProvider.fetchCustomProviders', async ({ commit, state }) => {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n }, { staleAfter: TTL.aiProviderFetchCustomProviders }),\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "3ae5b575aa12f00ef39cd1131966c284b1821951": {"t": 1785754049.2560592, "n": 47584, "h": "aabc2ee015394597", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\nimport { withFreshness } from '../_utils/withFreshness.js';\nimport { TTL } from '../_utils/freshnessConfig.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches.\n// v10: Anthropic dropdown reordered (Fable 5 first, Opus 4.8 second) + Fable 5 / Opus 4.8 metadata added.\n// v9: chutes reasoning controls added — old cached metadata lacks reasoningControl.\nconst MODEL_CACHE_VERSION = 10;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('claude-opus-4-7') || lower.startsWith('claude-opus-4-6') || lower.startsWith('claude-sonnet-4-6');\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('anthropic/claude-opus-4-7') || lower.startsWith('anthropic/claude-opus-4-6') || lower.startsWith('anthropic/claude-sonnet-4-6');\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (lowerModel.startsWith('claude-opus-4-7')) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n // Per-user \"Async tool execution\" toggle. Default FALSE — async tool\n // execution is currently an experimental opt-in feature. Users enable\n // it in Settings → AI Provider. localStorage is the source of truth on\n // the frontend so the toggle survives reloads; loadUserSettings()\n // reconciles it with the backend value on session start.\n asyncToolsEnabled: localStorage.getItem('asyncToolsEnabled') === 'true',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n SET_ASYNC_TOOLS_ENABLED(state, enabled) {\n const value = Boolean(enabled);\n state.asyncToolsEnabled = value;\n // Persist explicitly even when true, so a deliberate \"leave it on\"\n // setting is preserved across reloads (rather than relying on the\n // localStorage-missing-key default and tripping over future default\n // changes).\n localStorage.setItem('asyncToolsEnabled', value ? 'true' : 'false');\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setAsyncToolsEnabled({ commit }, enabled) {\n const value = Boolean(enabled);\n commit('SET_ASYNC_TOOLS_ENABLED', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n asyncToolsEnabled: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync async tools toggle with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (settings.asyncToolsEnabled !== undefined) {\n commit('SET_ASYNC_TOOLS_ENABLED', settings.asyncToolsEnabled === true);\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state, dispatch }, { provider, forceRefresh = false } = {}) {\n if (!provider) return [];\n\n // Route non-built-in providers to their dedicated fetchers. Without this,\n // callers (ModelSelector, AgentForge) that pass 'Local' or a custom-provider\n // UUID hit /api/models/<id>/models, which only knows built-in keys and 400s.\n if (String(provider).toLowerCase() === 'local') {\n return dispatch('fetchLocalModels', { forceRefresh });\n }\n if (state.customProviders.some((cp) => cp.id === provider)) {\n return dispatch('fetchCustomProviderModels', provider);\n }\n\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for this provider.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n fetchCustomProviders: withFreshness('aiProvider.fetchCustomProviders', async ({ commit, state }) => {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n }, { staleAfter: TTL.aiProviderFetchCustomProviders }),\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "60e6aeb51e30bf14fe3a4a47578f79723a87ab33": {"t": 1785754050.7454422, "n": 47476, "h": "aec1e54839bee04d", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\nimport { withFreshness } from '../_utils/withFreshness.js';\nimport { TTL } from '../_utils/freshnessConfig.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches.\n// v9: chutes reasoning controls added — old cached metadata lacks reasoningControl.\nconst MODEL_CACHE_VERSION = 9;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('claude-opus-4-7') || lower.startsWith('claude-opus-4-6') || lower.startsWith('claude-sonnet-4-6');\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('anthropic/claude-opus-4-7') || lower.startsWith('anthropic/claude-opus-4-6') || lower.startsWith('anthropic/claude-sonnet-4-6');\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (lowerModel.startsWith('claude-opus-4-7')) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n // Per-user \"Async tool execution\" toggle. Default FALSE — async tool\n // execution is currently an experimental opt-in feature. Users enable\n // it in Settings → AI Provider. localStorage is the source of truth on\n // the frontend so the toggle survives reloads; loadUserSettings()\n // reconciles it with the backend value on session start.\n asyncToolsEnabled: localStorage.getItem('asyncToolsEnabled') === 'true',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n SET_ASYNC_TOOLS_ENABLED(state, enabled) {\n const value = Boolean(enabled);\n state.asyncToolsEnabled = value;\n // Persist explicitly even when true, so a deliberate \"leave it on\"\n // setting is preserved across reloads (rather than relying on the\n // localStorage-missing-key default and tripping over future default\n // changes).\n localStorage.setItem('asyncToolsEnabled', value ? 'true' : 'false');\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setAsyncToolsEnabled({ commit }, enabled) {\n const value = Boolean(enabled);\n commit('SET_ASYNC_TOOLS_ENABLED', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n asyncToolsEnabled: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync async tools toggle with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (settings.asyncToolsEnabled !== undefined) {\n commit('SET_ASYNC_TOOLS_ENABLED', settings.asyncToolsEnabled === true);\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state, dispatch }, { provider, forceRefresh = false } = {}) {\n if (!provider) return [];\n\n // Route non-built-in providers to their dedicated fetchers. Without this,\n // callers (ModelSelector, AgentForge) that pass 'Local' or a custom-provider\n // UUID hit /api/models/<id>/models, which only knows built-in keys and 400s.\n if (String(provider).toLowerCase() === 'local') {\n return dispatch('fetchLocalModels', { forceRefresh });\n }\n if (state.customProviders.some((cp) => cp.id === provider)) {\n return dispatch('fetchCustomProviderModels', provider);\n }\n\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for this provider.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n fetchCustomProviders: withFreshness('aiProvider.fetchCustomProviders', async ({ commit, state }) => {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n }, { staleAfter: TTL.aiProviderFetchCustomProviders }),\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "bf9eebf4f6eb3481cfdb119f6c863da9d16dd3ec": {"t": 1785754054.3909678, "n": 45551, "h": "ef53bc1147e6bbd2", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\nimport { withFreshness } from '../_utils/withFreshness.js';\nimport { TTL } from '../_utils/freshnessConfig.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches.\n// v9: chutes reasoning controls added — old cached metadata lacks reasoningControl.\nconst MODEL_CACHE_VERSION = 9;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('claude-opus-4-7') || lower.startsWith('claude-opus-4-6') || lower.startsWith('claude-sonnet-4-6');\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('anthropic/claude-opus-4-7') || lower.startsWith('anthropic/claude-opus-4-6') || lower.startsWith('anthropic/claude-sonnet-4-6');\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (lowerModel.startsWith('claude-opus-4-7')) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state, dispatch }, { provider, forceRefresh = false } = {}) {\n if (!provider) return [];\n\n // Route non-built-in providers to their dedicated fetchers. Without this,\n // callers (ModelSelector, AgentForge) that pass 'Local' or a custom-provider\n // UUID hit /api/models/<id>/models, which only knows built-in keys and 400s.\n if (String(provider).toLowerCase() === 'local') {\n return dispatch('fetchLocalModels', { forceRefresh });\n }\n if (state.customProviders.some((cp) => cp.id === provider)) {\n return dispatch('fetchCustomProviderModels', provider);\n }\n\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for this provider.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n fetchCustomProviders: withFreshness('aiProvider.fetchCustomProviders', async ({ commit, state }) => {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n }, { staleAfter: TTL.aiProviderFetchCustomProviders }),\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "965ab5a2c624a1f33fb63a976021b76f9bb7a807": {"t": 1785754055.8109848, "n": 44991, "h": "fe5a4db92e16a3c4", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\nimport { withFreshness } from '../_utils/withFreshness.js';\nimport { TTL } from '../_utils/freshnessConfig.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches.\n// v9: chutes reasoning controls added — old cached metadata lacks reasoningControl.\nconst MODEL_CACHE_VERSION = 9;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('claude-opus-4-7') || lower.startsWith('claude-opus-4-6') || lower.startsWith('claude-sonnet-4-6');\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('anthropic/claude-opus-4-7') || lower.startsWith('anthropic/claude-opus-4-6') || lower.startsWith('anthropic/claude-sonnet-4-6');\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (lowerModel.startsWith('claude-opus-4-7')) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for this provider.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n fetchCustomProviders: withFreshness('aiProvider.fetchCustomProviders', async ({ commit, state }) => {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n }, { staleAfter: TTL.aiProviderFetchCustomProviders }),\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "e78c716265715cee9d9944263b11f79ca1b1063e": {"t": 1785754057.5129318, "n": 44772, "h": "0ebbfe976696719a", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches.\n// v9: chutes reasoning controls added — old cached metadata lacks reasoningControl.\nconst MODEL_CACHE_VERSION = 9;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('claude-opus-4-7') || lower.startsWith('claude-opus-4-6') || lower.startsWith('claude-sonnet-4-6');\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('anthropic/claude-opus-4-7') || lower.startsWith('anthropic/claude-opus-4-6') || lower.startsWith('anthropic/claude-sonnet-4-6');\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (lowerModel.startsWith('claude-opus-4-7')) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for this provider.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "4f9370d3d1e5b42cb4fa3251d1a610339ff4d268": {"t": 1785754059.0768847, "n": 43924, "h": "8fdb026a25a29e3e", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches\nconst MODEL_CACHE_VERSION = 8;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('claude-opus-4-7') || lower.startsWith('claude-opus-4-6') || lower.startsWith('claude-sonnet-4-6');\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('anthropic/claude-opus-4-7') || lower.startsWith('anthropic/claude-opus-4-6') || lower.startsWith('anthropic/claude-sonnet-4-6');\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (lowerModel.startsWith('claude-opus-4-7')) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for this provider.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "170f0871e5fb989d24aa3425f2c73be6ddb945d8": {"t": 1785754060.5005352, "n": 43714, "h": "ad38e999b76d80bf", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches\nconst MODEL_CACHE_VERSION = 8;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('claude-opus-4-7') || lower.startsWith('claude-opus-4-6') || lower.startsWith('claude-sonnet-4-6');\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('anthropic/claude-opus-4-7') || lower.startsWith('anthropic/claude-opus-4-6') || lower.startsWith('anthropic/claude-sonnet-4-6');\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (lowerModel.startsWith('claude-opus-4-7')) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for this provider.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "391967e1a6c226fbd10fe24a87cc11cc26d37b5f": {"t": 1785754062.5145772, "n": 33968, "h": "e6fd6e7f92a49b14", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches\nconst MODEL_CACHE_VERSION = 7;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n return state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for this provider.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "3f3ea2dd905708dd5f44ea14c8e56c33869062b5": {"t": 1785754064.4469647, "n": 32573, "h": "d8d690534d8b1bf3", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches\nconst MODEL_CACHE_VERSION = 5;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningEnabled: localStorage.getItem('reasoningEnabled') === 'true',\n customInstructions: localStorage.getItem('customInstructions') || '',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningEnabled = enabled;\n if (enabled) {\n localStorage.setItem('reasoningEnabled', 'true');\n } else {\n localStorage.removeItem('reasoningEnabled');\n }\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n return state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n },\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for this provider.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "ef5e7fac9f5191b5d5450068a87d2fc99086054b": {"t": 1785754066.4873195, "n": 30131, "h": "a98328bce3480a1c", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches\nconst MODEL_CACHE_VERSION = 5;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningEnabled: localStorage.getItem('reasoningEnabled') === 'true',\n customInstructions: localStorage.getItem('customInstructions') || '',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningEnabled = enabled;\n if (enabled) {\n localStorage.setItem('reasoningEnabled', 'true');\n } else {\n localStorage.removeItem('reasoningEnabled');\n }\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n return state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n },\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "7d350b378695f59dd76f8e4d2b81223a0adbdb09": {"t": 1785754067.8615136, "n": 29910, "h": "6700349e14ac553a", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches\nconst MODEL_CACHE_VERSION = 5;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningEnabled: localStorage.getItem('reasoningEnabled') === 'true',\n customInstructions: localStorage.getItem('customInstructions') || '',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningEnabled = enabled;\n if (enabled) {\n localStorage.setItem('reasoningEnabled', 'true');\n } else {\n localStorage.removeItem('reasoningEnabled');\n }\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n return state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n },\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "134eb1880d21d971f2d72af446e9716e3ca88d4a": {"t": 1785754069.3361113, "n": 28412, "h": "88cc78c574b7ad8b", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches\nconst MODEL_CACHE_VERSION = 5;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningEnabled: localStorage.getItem('reasoningEnabled') === 'true',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningEnabled = enabled;\n if (enabled) {\n localStorage.setItem('reasoningEnabled', 'true');\n } else {\n localStorage.removeItem('reasoningEnabled');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n return state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n },\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "c0be2ecd5d83e0174838765f8049c3006902a10d": {"t": 1785754070.8277476, "n": 28412, "h": "351946a9a2a9f44a", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches\nconst MODEL_CACHE_VERSION = 5;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningEnabled: localStorage.getItem('reasoningEnabled') === 'true',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningEnabled = enabled;\n if (enabled) {\n localStorage.setItem('reasoningEnabled', 'true');\n } else {\n localStorage.removeItem('reasoningEnabled');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n return state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n },\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchMinimaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "e2e7e95b209ed714fe06ba4ef7bebf721b9e87cf": {"t": 1785754072.4177887, "n": 29014, "h": "62788f18528435fc", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches\nconst MODEL_CACHE_VERSION = 5;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openai-codex-cli', displayName: 'OpenAI-Codex-CLI' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningEnabled: localStorage.getItem('reasoningEnabled') === 'true',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningEnabled = enabled;\n if (enabled) {\n localStorage.setItem('reasoningEnabled', 'true');\n } else {\n localStorage.removeItem('reasoningEnabled');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n return state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n },\n filteredProviders(state, _getters, rootState) {\n const codexStatus = rootState?.appAuth?.codexStatus || {};\n const shouldHideOpenAICodex = codexStatus.available === true && codexStatus.apiUsable !== true;\n if (!shouldHideOpenAICodex) {\n return state.providers;\n }\n return state.providers.filter((provider) => provider !== 'OpenAI-Codex');\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'openai-codex-cli' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchOpenAICodexCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex-CLI', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchMinimaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "820113af9f0664f8b0d33ea776f37ba921833545": {"t": 1785754073.8139374, "n": 28030, "h": "ace0eca003099087", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches\nconst MODEL_CACHE_VERSION = 5;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openai-codex-cli', displayName: 'OpenAI-Codex-CLI' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningEnabled: localStorage.getItem('reasoningEnabled') === 'true',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningEnabled = enabled;\n if (enabled) {\n localStorage.setItem('reasoningEnabled', 'true');\n } else {\n localStorage.removeItem('reasoningEnabled');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n return state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n },\n filteredProviders(state, _getters, rootState) {\n const codexStatus = rootState?.appAuth?.codexStatus || {};\n const shouldHideOpenAICodex = codexStatus.available === true && codexStatus.apiUsable !== true;\n if (!shouldHideOpenAICodex) {\n return state.providers;\n }\n return state.providers.filter((provider) => provider !== 'OpenAI-Codex');\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'openai-codex-cli' || providerLower === 'claude-code' || providerLower === 'gemini-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchOpenAICodexCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex-CLI', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchMinimaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n },\n\n ensureValidModel({ commit, state }) {\n commit('ENSURE_VALID_MODEL');\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "3c4c27281f0dfcc592531ef89403542a8c54ef27": {"t": 1785754075.2534099, "n": 27771, "h": "647038f2eda079f9", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches\nconst MODEL_CACHE_VERSION = 4;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openai-codex-cli', displayName: 'OpenAI-Codex-CLI' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningEnabled: localStorage.getItem('reasoningEnabled') === 'true',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningEnabled = enabled;\n if (enabled) {\n localStorage.setItem('reasoningEnabled', 'true');\n } else {\n localStorage.removeItem('reasoningEnabled');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n return state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n },\n filteredProviders(state, _getters, rootState) {\n const codexStatus = rootState?.appAuth?.codexStatus || {};\n const shouldHideOpenAICodex = codexStatus.available === true && codexStatus.apiUsable !== true;\n if (!shouldHideOpenAICodex) {\n return state.providers;\n }\n return state.providers.filter((provider) => provider !== 'OpenAI-Codex');\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n // Restore cached metadata too\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch (e) { /* ignore */ }\n } else {\n // Metadata not cached yet — fetch it in background\n const providerLower = resolveProviderKey(provider);\n fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`)\n .then((r) => r.ok ? r.json() : null)\n .then((data) => {\n if (data?.success && data.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: data.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(data.metadata));\n }\n })\n .catch(() => {});\n }\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'openai-codex-cli' || providerLower === 'claude-code';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n // Fetch model metadata (context windows, costs, etc.) alongside models\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch (metaErr) {\n // Non-critical — context monitor just won't show pre-chat values\n }\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchOpenAICodexCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex-CLI', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchMinimaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n },\n\n ensureValidModel({ commit, state }) {\n commit('ENSURE_VALID_MODEL');\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "4ee4888e4a79dea107c83876c75f0d088f74ef08": {"t": 1785754076.5642807, "n": 25777, "h": "5d159d22967f4ab9", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches\nconst MODEL_CACHE_VERSION = 4;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openai-codex-cli', displayName: 'OpenAI-Codex-CLI' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningEnabled: localStorage.getItem('reasoningEnabled') === 'true',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningEnabled = enabled;\n if (enabled) {\n localStorage.setItem('reasoningEnabled', 'true');\n } else {\n localStorage.removeItem('reasoningEnabled');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n filteredProviders(state, _getters, rootState) {\n const codexStatus = rootState?.appAuth?.codexStatus || {};\n const shouldHideOpenAICodex = codexStatus.available === true && codexStatus.apiUsable !== true;\n if (!shouldHideOpenAICodex) {\n return state.providers;\n }\n return state.providers.filter((provider) => provider !== 'OpenAI-Codex');\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'openai-codex-cli' || providerLower === 'claude-code';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchOpenAICodexCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex-CLI', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchMinimaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n },\n\n ensureValidModel({ commit, state }) {\n commit('ENSURE_VALID_MODEL');\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "840d4d743d70c62abf2446b5c2f02053e1a9ddd1": {"t": 1785754077.9699113, "n": 24256, "h": "a97ea2d37b0afa68", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openai-codex-cli', displayName: 'OpenAI-Codex-CLI' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z-AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/-/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n if (state.selectedProvider === provider && !state.selectedModel && models.length > 0) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n filteredProviders(state, _getters, rootState) {\n const codexStatus = rootState?.appAuth?.codexStatus || {};\n const shouldHideOpenAICodex = codexStatus.available === true && codexStatus.apiUsable !== true;\n if (!shouldHideOpenAICodex) {\n return state.providers;\n }\n return state.providers.filter((provider) => provider !== 'OpenAI-Codex');\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = provider.toLowerCase();\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'openai-codex-cli' || providerLower === 'claude-code';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchOpenAICodexCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex-CLI', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchMinimaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z-AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n },\n\n ensureValidModel({ commit, state }) {\n commit('ENSURE_VALID_MODEL');\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "36f5534b6b4bd88dbda88c8b6214506b03927fe0": {"t": 1785754079.4175127, "n": 25521, "h": "e99fa47eeaf5b773", "c": "import { API_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches\nconst MODEL_CACHE_VERSION = 4;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openai-codex-cli', displayName: 'OpenAI-Codex-CLI' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningEnabled: localStorage.getItem('reasoningEnabled') === 'true',\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningEnabled = enabled;\n if (enabled) {\n localStorage.setItem('reasoningEnabled', 'true');\n } else {\n localStorage.removeItem('reasoningEnabled');\n }\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n filteredProviders(state, _getters, rootState) {\n const codexStatus = rootState?.appAuth?.codexStatus || {};\n const shouldHideOpenAICodex = codexStatus.available === true && codexStatus.apiUsable !== true;\n if (!shouldHideOpenAICodex) {\n return state.providers;\n }\n return state.providers.filter((provider) => provider !== 'OpenAI-Codex');\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'openai-codex-cli' || providerLower === 'claude-code';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchOpenAICodexCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex-CLI', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchMinimaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n },\n\n ensureValidModel({ commit, state }) {\n commit('ENSURE_VALID_MODEL');\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "da7cae3c2556c4a8a120d7d103876387e821aa57": {"t": 1785754080.953608, "n": 24938, "h": "40ce9135de06653d", "c": "import { API_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version — bump this to invalidate all provider model caches\nconst MODEL_CACHE_VERSION = 3;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION)) {\n // Clear all provider model caches when version changes\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION));\n console.log('[aiProvider] Cleared stale model caches (version upgrade)');\n }\n})();\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openai-codex-cli', displayName: 'OpenAI-Codex-CLI' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n if (state.selectedProvider === provider && !state.selectedModel && models.length > 0) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n filteredProviders(state, _getters, rootState) {\n const codexStatus = rootState?.appAuth?.codexStatus || {};\n const shouldHideOpenAICodex = codexStatus.available === true && codexStatus.apiUsable !== true;\n if (!shouldHideOpenAICodex) {\n return state.providers;\n }\n return state.providers.filter((provider) => provider !== 'OpenAI-Codex');\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'openai-codex-cli' || providerLower === 'claude-code';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchOpenAICodexCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex-CLI', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchMinimaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n },\n\n ensureValidModel({ commit, state }) {\n commit('ENSURE_VALID_MODEL');\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "7525e2b9e8237460098f3903ea1908af586d5445": {"t": 1785754082.3495116, "n": 24006, "h": "ef078f740a9b7a63", "c": "import { API_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openai-codex-cli', displayName: 'OpenAI-Codex-CLI' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z-AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/-/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n if (state.selectedProvider === provider && !state.selectedModel && models.length > 0) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n filteredProviders(state, _getters, rootState) {\n const codexStatus = rootState?.appAuth?.codexStatus || {};\n const shouldHideOpenAICodex = codexStatus.available === true && codexStatus.apiUsable !== true;\n if (!shouldHideOpenAICodex) {\n return state.providers;\n }\n return state.providers.filter((provider) => provider !== 'OpenAI-Codex');\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = resolveProviderKey(provider);\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'openai-codex-cli' || providerLower === 'claude-code';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchOpenAICodexCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex-CLI', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchMinimaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z-AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n },\n\n ensureValidModel({ commit, state }) {\n commit('ENSURE_VALID_MODEL');\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "ea470d336fb61c32b09ebeedb52906406204cc71": {"t": 1785754083.8070898, "n": 24000, "h": "2a1725c57830042c", "c": "import { API_CONFIG } from '@/tt.config.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openai-codex-cli', displayName: 'OpenAI-Codex-CLI' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z-AI' },\n];\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/-/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n if (state.selectedProvider === provider && !state.selectedModel && models.length > 0) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n filteredProviders(state, _getters, rootState) {\n const codexStatus = rootState?.appAuth?.codexStatus || {};\n const shouldHideOpenAICodex = codexStatus.available === true && codexStatus.apiUsable !== true;\n if (!shouldHideOpenAICodex) {\n return state.providers;\n }\n return state.providers.filter((provider) => provider !== 'OpenAI-Codex');\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider\n async fetchProviderModels({ commit, state }, { provider, forceRefresh = false } = {}) {\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 60 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const providerLower = provider.toLowerCase();\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'openai-codex-cli' || providerLower === 'claude-code';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = {\n 'Content-Type': 'application/json',\n };\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, {\n headers,\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchOpenAICodexCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex-CLI', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchMinimaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z-AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n },\n\n ensureValidModel({ commit, state }) {\n commit('ENSURE_VALID_MODEL');\n },\n\n // Custom provider management actions\n async fetchCustomProviders({ commit, state }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n },\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}, "6fdecedb35300903d1a2a60faeeb356d27c2c4ac": {"t": 1786067795.8119943, "n": 55411, "h": "21ed483fe3539fcc", "c": "import { API_CONFIG, DEPLOYMENT_CONFIG } from '@/tt.config.js';\nimport { withFreshness } from '../_utils/withFreshness.js';\nimport { TTL } from '../_utils/freshnessConfig.js';\n\n// ─────────────────────────── PROVIDER REGISTRY ───────────────────────────\n// Single source of truth for all built-in provider metadata on the frontend.\n// Derived from the same data as backend/src/services/ai/providerConfigs.js.\n\n// Cache version fallback — used only if the backend's schema-version endpoint\n// is unreachable on boot (offline, backend not started). The real invalidator\n// is the schema hash returned by GET /api/models/schema-version, fetched on\n// module load and compared against the stored value. Any change to metadata\n// shape, recommended-model order, or fallback list auto-invalidates all\n// per-provider localStorage caches. Kills the manual bump-the-integer footgun.\nconst MODEL_CACHE_VERSION_FALLBACK = 12;\n(() => {\n const storedVersion = localStorage.getItem('model_cache_version');\n if (storedVersion !== String(MODEL_CACHE_VERSION_FALLBACK)) {\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_cache_version', String(MODEL_CACHE_VERSION_FALLBACK));\n console.log('[aiProvider] Cleared stale model caches (fallback version upgrade)');\n }\n})();\n\n// Auto cache-bust via backend schema hash. Fetched once on module load and\n// again on every explicit refresh action. Silent failure = keep whatever was\n// cached; the 5-min SWR path below still keeps things fresh.\n(async () => {\n try {\n const res = await fetch(`${API_CONFIG.BASE_URL}/models/schema-version`);\n if (!res.ok) return;\n const data = await res.json();\n if (!data?.success || !data.version) return;\n const stored = localStorage.getItem('model_schema_hash');\n if (stored !== data.version) {\n for (const key of Object.keys(localStorage)) {\n if (key.endsWith('_models') || key.endsWith('_metadata')) {\n localStorage.removeItem(key);\n }\n }\n localStorage.setItem('model_schema_hash', data.version);\n console.log(`[aiProvider] Cleared stale model caches (backend schema hash changed to ${data.version})`);\n }\n } catch {\n // Backend not reachable on module load — fine, fallback version handles it.\n }\n})();\n\n// Display order is ENFORCED here, not by where entries are typed. The literal\n// below was hand-alphabetized, so every new provider silently landed wherever\n// its author appended it (PR #50 put Cursor between Grok-Build and Groq — the\n// \"Cursor is in the G's\" bug). Sorting at the boundary makes insertion position\n// irrelevant. localeCompare ignores punctuation at the primary strength, so\n// Grok-Build/GrokAI order by their letters rather than by the hyphen.\nconst BUILT_IN_PROVIDERS = [\n { key: 'anthropic', displayName: 'Anthropic' },\n { key: 'antigravity', displayName: 'Antigravity' },\n { key: 'cerebras', displayName: 'Cerebras' },\n { key: 'chutes', displayName: 'Chutes' },\n { key: 'claude-code', displayName: 'Claude-Code' },\n { key: 'deepseek', displayName: 'DeepSeek' },\n { key: 'gemini', displayName: 'Gemini' },\n { key: 'gemini-cli', displayName: 'Gemini-CLI' },\n { key: 'grokai', displayName: 'GrokAI' },\n { key: 'grok-build', displayName: 'Grok-Build' },\n { key: 'cursor-cli', displayName: 'Cursor' },\n { key: 'groq', displayName: 'Groq' },\n { key: 'kimi', displayName: 'Kimi' },\n { key: 'kimi-code', displayName: 'Kimi-Code' },\n { key: 'local', displayName: 'Local' },\n { key: 'minimax', displayName: 'MiniMax' },\n { key: 'openai', displayName: 'OpenAI' },\n { key: 'openai-codex', displayName: 'OpenAI-Codex' },\n { key: 'openrouter', displayName: 'OpenRouter' },\n { key: 'togetherai', displayName: 'TogetherAI' },\n { key: 'zai', displayName: 'Z.AI' },\n].sort((a, b) => a.displayName.localeCompare(b.displayName, 'en', { sensitivity: 'base' }));\n\n// ─────────────────────────── DERIVED EXPORTS ───────────────────────────\n\n// Map internal provider keys to user-facing display names where they differ\nexport const PROVIDER_DISPLAY_NAMES = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n // Only add entries where key-based capitalization != display name\n const defaultName = p.key.charAt(0).toUpperCase() + p.key.slice(1);\n if (p.displayName !== defaultName) {\n PROVIDER_DISPLAY_NAMES[p.displayName] = p.displayName;\n PROVIDER_DISPLAY_NAMES[p.key] = p.displayName;\n }\n}\n\n// Resolve any provider identifier (display name, key, or mixed case) to its canonical key.\n// e.g. \"Z.AI\" → \"zai\", \"Z-AI\" → \"zai\", \"GrokAI\" → \"grokai\", \"openai\" → \"openai\"\nexport function resolveProviderKey(identifier) {\n if (!identifier) return null;\n const lower = identifier.toLowerCase();\n // Direct key match\n const byKey = BUILT_IN_PROVIDERS.find((p) => p.key === lower);\n if (byKey) return byKey.key;\n // Display name match (case-insensitive)\n const byDisplay = BUILT_IN_PROVIDERS.find((p) => p.displayName.toLowerCase() === lower);\n if (byDisplay) return byDisplay.key;\n // Fuzzy match: strip non-alphanumeric chars (e.g. \"Z-AI\", \"Z.AI\" → \"zai\")\n const stripped = lower.replace(/[^a-z0-9]/g, '');\n const byFuzzy = BUILT_IN_PROVIDERS.find(\n (p) => p.key === stripped || p.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') === stripped,\n );\n if (byFuzzy) return byFuzzy.key;\n // Not a built-in provider — return as-is (custom provider ID)\n return identifier;\n}\n\n// Single source of truth for AI providers that require API keys (excludes 'Local')\nexport const AI_PROVIDERS_WITH_API = BUILT_IN_PROVIDERS.filter((p) => p.key !== 'local').map((p) => p.key);\n\n// Mapping of provider display names to their fetch action names (auto-generated)\nexport const PROVIDER_FETCH_ACTIONS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n const actionSuffix = p.displayName.replace(/[-.]/g, '');\n PROVIDER_FETCH_ACTIONS[p.displayName] = `aiProvider/fetch${actionSuffix}Models`;\n}\n\n// Provider display names list (used by state.providers)\nconst PROVIDER_DISPLAY_LIST = BUILT_IN_PROVIDERS.map((p) => p.displayName);\n\n// Initial allModels shape\nconst INITIAL_ALL_MODELS = {};\nfor (const p of BUILT_IN_PROVIDERS) {\n INITIAL_ALL_MODELS[p.displayName] = [];\n}\n\nfunction normalizeReasoningValue(value) {\n return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : 'default';\n}\n\nfunction isReasoningEnabledValue(value) {\n const normalized = normalizeReasoningValue(value);\n return normalized !== 'default' && normalized !== 'off' && normalized !== 'none';\n}\n\nfunction buildReasoningControl(kind, options, defaultValue = 'default') {\n return { kind, options, defaultValue };\n}\n\nfunction isOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('gpt-5') || /^o\\d/.test(lower);\n}\n\n// MIRROR of backend/src/services/ai/reasoningModels.js — keep regexes in sync.\n// Matches claude-opus-4-6+, claude-sonnet-4-6+ (auto-covers 4.7 / 4.8 / 4.9 / 4.10+),\n// plus the always-on Fable / Mythos family.\nconst ANTHROPIC_VERSIONED_REASONING_RE = /^claude-(opus|sonnet)-4-([6-9]|[1-9]\\d{1,2})(?:-|$)/;\nconst ANTHROPIC_FAMILY_REASONING_RE = /^claude-(fable|mythos)-/;\nconst ANTHROPIC_XHIGH_RE = /^claude-(opus-4-([7-9]|[1-9]\\d{1,2})(?:-|$)|fable-|mythos-)/;\n\nfunction isAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return ANTHROPIC_VERSIONED_REASONING_RE.test(lower) || ANTHROPIC_FAMILY_REASONING_RE.test(lower);\n}\n\nfunction isGemini3ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-3');\n}\n\nfunction isGemini25ReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('gemini-2.5');\n}\n\nfunction supportsDeepSeekThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'deepseek-chat' || lower === 'deepseek-reasoner' || lower.startsWith('deepseek-v4-');\n}\n\nfunction isGroqGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\nfunction isGroqQwenReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower === 'qwen/qwen3-32b' || lower.startsWith('qwen/qwen3-');\n}\n\nfunction isCerebrasGlmReasoningModel(modelId) {\n return String(modelId || '').toLowerCase() === 'zai-glm-4.7';\n}\n\nfunction supportsZaiThinking(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('glm-5') || lower.startsWith('glm-4.7') || lower.startsWith('glm-4.6') || lower.startsWith('glm-4.5');\n}\n\nfunction supportsKimiToggle(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n if (lowerProvider === 'kimi-code') return lowerModel === 'kimi-for-coding';\n return lowerModel.startsWith('kimi-k2') && !lowerModel.includes('thinking');\n}\n\nfunction isOpenRouterOpenAIReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('openai/gpt-5') || /^openai\\/o\\d/.test(lower);\n}\n\nfunction isOpenRouterAnthropicReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n if (!lower.startsWith('anthropic/')) return false;\n return isAnthropicReasoningModel(lower.slice('anthropic/'.length));\n}\n\nfunction isOpenRouterGeminiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('google/gemini-3') || lower.startsWith('google/gemini-2.5');\n}\n\nfunction isOpenRouterXaiReasoningModel(modelId) {\n const lower = String(modelId || '').toLowerCase();\n return lower.startsWith('x-ai/') || lower.startsWith('xai/');\n}\n\nfunction isTogetherGptOssReasoningModel(modelId) {\n return String(modelId || '').toLowerCase().startsWith('openai/gpt-oss-');\n}\n\n// Chutes hosts upstream models inside TEE; reasoning routes by underlying family.\nfunction isChutesKimiReasoningModel(modelId) {\n return /^moonshotai\\/kimi-k2/i.test(String(modelId || ''));\n}\n\nfunction isChutesGlmReasoningModel(modelId) {\n return /^zai-org\\/glm-5/i.test(String(modelId || ''));\n}\n\nfunction isChutesQwenReasoningModel(modelId) {\n return /^qwen\\/qwen3/i.test(String(modelId || ''));\n}\n\nfunction inferReasoningControl(providerKey, modelId) {\n const lowerProvider = String(providerKey || '').toLowerCase();\n const lowerModel = String(modelId || '').toLowerCase();\n\n if (!lowerProvider || !lowerModel) return null;\n\n if (lowerProvider === 'openai' || lowerProvider === 'openai-codex') {\n if (!isOpenAIReasoningModel(modelId)) return null;\n\n if (lowerModel.startsWith('gpt-5.4')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'openai-codex' && (lowerModel.startsWith('gpt-5.3') || lowerModel.startsWith('gpt-5.2'))) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5.2') || lowerModel.startsWith('gpt-5.1')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n\n if (lowerModel.startsWith('gpt-5')) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'minimal', label: 'Minimal' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n if (/^o\\d/.test(lowerModel)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n\n return null;\n }\n\n if (lowerProvider === 'anthropic' || lowerProvider === 'claude-code') {\n if (!isAnthropicReasoningModel(modelId)) return null;\n const options = [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ];\n if (ANTHROPIC_XHIGH_RE.test(lowerModel)) {\n options.push({ value: 'xhigh', label: 'Max' });\n }\n return buildReasoningControl('effort', options);\n }\n\n if (lowerProvider === 'gemini' || lowerProvider === 'gemini-cli' || lowerProvider === 'antigravity') {\n if (isGemini3ReasoningModel(modelId) || isGemini25ReasoningModel(modelId)) {\n const options = [{ value: 'default', label: 'Default' }];\n if (lowerModel.includes('flash')) {\n options.push({ value: 'off', label: 'Off' });\n }\n options.push(\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n );\n return buildReasoningControl('effort', options);\n }\n return null;\n }\n\n if (lowerProvider === 'deepseek') {\n if (!supportsDeepSeekThinking(modelId)) return null;\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'high', label: 'High' },\n { value: 'max', label: 'Max' },\n ]);\n }\n\n if (lowerProvider === 'groq') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isGroqQwenReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'cerebras') {\n if (isGroqGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n if (isCerebrasGlmReasoningModel(modelId)) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'openrouter') {\n if (isOpenRouterOpenAIReasoningModel(modelId) || isOpenRouterXaiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'xhigh', label: 'Max' },\n ]);\n }\n if (isOpenRouterAnthropicReasoningModel(modelId) || isOpenRouterGeminiReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'togetherai') {\n if (isTogetherGptOssReasoningModel(modelId)) {\n return buildReasoningControl('effort', [\n { value: 'default', label: 'Default' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n ]);\n }\n return null;\n }\n\n if (lowerProvider === 'zai') {\n if (!supportsZaiThinking(modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'kimi' || lowerProvider === 'kimi-code') {\n if (!supportsKimiToggle(lowerProvider, modelId)) return null;\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n\n if (lowerProvider === 'chutes') {\n if (\n isChutesKimiReasoningModel(modelId) ||\n isChutesGlmReasoningModel(modelId) ||\n isChutesQwenReasoningModel(modelId)\n ) {\n return buildReasoningControl('toggle', [\n { value: 'default', label: 'Default' },\n { value: 'off', label: 'Off' },\n ]);\n }\n return null;\n }\n\n return null;\n}\n\nconst STORED_REASONING_VALUE = normalizeReasoningValue(localStorage.getItem('reasoningValue'));\nconst INITIAL_REASONING_VALUE = STORED_REASONING_VALUE !== 'default'\n ? STORED_REASONING_VALUE\n : (localStorage.getItem('reasoningEnabled') === 'true' ? 'on' : 'default');\n\nexport default {\n namespaced: true,\n state: {\n providers: [...PROVIDER_DISPLAY_LIST],\n customProviders: [],\n allModels: { ...INITIAL_ALL_MODELS },\n modelMetadata: {},\n selectedProvider: localStorage.getItem('selectedProvider') || null,\n selectedModel: localStorage.getItem('selectedModel') || null,\n reasoningValue: INITIAL_REASONING_VALUE,\n reasoningEnabled: isReasoningEnabledValue(INITIAL_REASONING_VALUE),\n customInstructions: localStorage.getItem('customInstructions') || '',\n // Per-user \"Async tool execution\" toggle. Default FALSE — async tool\n // execution is currently an experimental opt-in feature. Users enable\n // it in Settings → AI Provider. localStorage is the source of truth on\n // the frontend so the toggle survives reloads; loadUserSettings()\n // reconciles it with the backend value on session start.\n asyncToolsEnabled: localStorage.getItem('asyncToolsEnabled') === 'true',\n // Per-user hard cap on tool result size returned to the LLM. Default\n // 100000 matches the historical OrchestratorService MAX_TOOL_RESULT_CHARS.\n // localStorage is the source of truth on the frontend so the value\n // survives reloads; loadUserSettings() reconciles with the backend.\n toolOutputCap: (() => {\n const raw = Number(localStorage.getItem('toolOutputCap'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100000;\n })(),\n // Per-user cap on tool-loop rounds per chat turn. Default 100 matches the\n // historical orchestrator/agent/tool/widget/goal cap. When set, overrides\n // every chat surface uniformly (including workflow/artifact which default\n // to 25 server-side).\n maxToolRounds: (() => {\n const raw = Number(localStorage.getItem('maxToolRounds'));\n return Number.isFinite(raw) && raw > 0 ? raw : 100;\n })(),\n loadingModels: {},\n modelCache: {},\n },\n mutations: {\n SET_SELECTED_PROVIDER(state, newProvider) {\n state.selectedProvider = newProvider;\n\n if (!newProvider) {\n localStorage.removeItem('selectedProvider');\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedProvider', newProvider);\n\n const availableModels = state.allModels[newProvider] || [];\n if (availableModels.length > 0) {\n state.selectedModel = availableModels[0];\n localStorage.setItem('selectedModel', availableModels[0]);\n } else {\n state.selectedModel = null;\n localStorage.removeItem('selectedModel');\n }\n },\n SET_SELECTED_MODEL(state, newModel) {\n state.selectedModel = newModel;\n\n if (!newModel) {\n localStorage.removeItem('selectedModel');\n return;\n }\n\n localStorage.setItem('selectedModel', newModel);\n },\n SET_REASONING_ENABLED(state, enabled) {\n state.reasoningValue = enabled ? 'on' : 'off';\n state.reasoningEnabled = enabled;\n localStorage.setItem('reasoningValue', state.reasoningValue);\n if (enabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_REASONING_VALUE(state, value) {\n state.reasoningValue = normalizeReasoningValue(value);\n state.reasoningEnabled = isReasoningEnabledValue(state.reasoningValue);\n if (state.reasoningValue === 'default') {\n localStorage.removeItem('reasoningValue');\n } else {\n localStorage.setItem('reasoningValue', state.reasoningValue);\n }\n if (state.reasoningEnabled) localStorage.setItem('reasoningEnabled', 'true');\n else localStorage.removeItem('reasoningEnabled');\n },\n SET_CUSTOM_INSTRUCTIONS(state, instructions) {\n const value = typeof instructions === 'string' ? instructions : '';\n state.customInstructions = value;\n if (value) {\n localStorage.setItem('customInstructions', value);\n } else {\n localStorage.removeItem('customInstructions');\n }\n },\n SET_ASYNC_TOOLS_ENABLED(state, enabled) {\n const value = Boolean(enabled);\n state.asyncToolsEnabled = value;\n // Persist explicitly even when true, so a deliberate \"leave it on\"\n // setting is preserved across reloads (rather than relying on the\n // localStorage-missing-key default and tripping over future default\n // changes).\n localStorage.setItem('asyncToolsEnabled', value ? 'true' : 'false');\n },\n SET_TOOL_OUTPUT_CAP(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100000;\n state.toolOutputCap = clean;\n localStorage.setItem('toolOutputCap', String(clean));\n },\n SET_MAX_TOOL_ROUNDS(state, value) {\n const num = Number(value);\n const clean = Number.isFinite(num) && num > 0 ? Math.round(num) : 100;\n state.maxToolRounds = clean;\n localStorage.setItem('maxToolRounds', String(clean));\n },\n ENSURE_VALID_MODEL(state) {\n const availableModels = state.allModels[state.selectedProvider] || [];\n if (!state.selectedModel || !availableModels.includes(state.selectedModel)) {\n const defaultModel = availableModels[0];\n if (defaultModel) {\n state.selectedModel = defaultModel;\n localStorage.setItem('selectedModel', defaultModel);\n }\n }\n },\n SET_PROVIDER_MODELS(state, { provider, models }) {\n state.allModels[provider] = models;\n\n // Auto-select the first (recommended) model when:\n // - This is the currently selected provider, AND\n // - No model is selected, OR the current model isn't in the new model list\n if (state.selectedProvider === provider && models.length > 0) {\n if (!state.selectedModel || !models.includes(state.selectedModel)) {\n state.selectedModel = models[0];\n localStorage.setItem('selectedModel', models[0]);\n }\n }\n },\n SET_MODEL_METADATA(state, { provider, metadata }) {\n state.modelMetadata = { ...state.modelMetadata, [provider]: metadata };\n },\n SET_LOADING_MODELS(state, { provider, loading }) {\n if (!state.loadingModels) {\n state.loadingModels = {};\n }\n state.loadingModels[provider] = loading;\n },\n SET_CUSTOM_PROVIDERS(state, providers) {\n state.customProviders = providers;\n },\n ADD_CUSTOM_PROVIDER(state, provider) {\n state.customProviders.push(provider);\n state.allModels[provider.id] = [];\n },\n UPDATE_CUSTOM_PROVIDER(state, { id, updates }) {\n const index = state.customProviders.findIndex((p) => p.id === id);\n if (index !== -1) {\n state.customProviders[index] = { ...state.customProviders[index], ...updates };\n }\n },\n REMOVE_CUSTOM_PROVIDER(state, id) {\n state.customProviders = state.customProviders.filter((p) => p.id !== id);\n delete state.allModels[id];\n },\n },\n getters: {\n filteredModels(state) {\n return state.allModels[state.selectedProvider] || [];\n },\n selectedModelMetadata(state) {\n if (!state.selectedProvider || !state.selectedModel) return null;\n const providerKey = resolveProviderKey(state.selectedProvider);\n const metadata = state.modelMetadata[state.selectedProvider]?.[state.selectedModel] || null;\n const inferredControl = inferReasoningControl(providerKey, state.selectedModel);\n if (metadata || inferredControl) {\n return inferredControl && !metadata?.reasoningControl\n ? { ...(metadata || {}), reasoningControl: inferredControl }\n : metadata;\n }\n return null;\n },\n selectedReasoningControl(state, getters) {\n return getters.selectedModelMetadata?.reasoningControl || null;\n },\n inferReasoningControl: () => (provider, model) => inferReasoningControl(resolveProviderKey(provider), model),\n filteredProviders(state) {\n return state.providers;\n },\n allProviders(state) {\n const customProviderNames = state.customProviders.map((p) => ({\n id: p.id,\n name: p.provider_name,\n isCustom: true,\n }));\n\n const builtInProviders = state.providers.map((p) => ({\n id: p,\n name: p,\n isCustom: false,\n }));\n\n return [...builtInProviders, ...customProviderNames];\n },\n },\n actions: {\n async setProvider({ commit, state }, newProvider) {\n commit('SET_SELECTED_PROVIDER', newProvider);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: newProvider,\n selectedModel: state.selectedModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync provider with backend:', error);\n }\n },\n\n async setCustomInstructions({ commit }, newInstructions) {\n const value = typeof newInstructions === 'string' ? newInstructions : '';\n commit('SET_CUSTOM_INSTRUCTIONS', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n customInstructions: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync custom instructions with backend:', error);\n }\n },\n\n async setAsyncToolsEnabled({ commit }, enabled) {\n const value = Boolean(enabled);\n commit('SET_ASYNC_TOOLS_ENABLED', value);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n asyncToolsEnabled: value,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync async tools toggle with backend:', error);\n }\n },\n\n async setToolOutputCap({ commit }, newCap) {\n // Clamp to the same window the backend validates against — keeps the\n // PUT from 400-ing on out-of-range values.\n const raw = Number(newCap);\n const clamped = Number.isFinite(raw)\n ? Math.max(25000, Math.min(500000, Math.round(raw)))\n : 100000;\n commit('SET_TOOL_OUTPUT_CAP', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n toolOutputCap: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync tool output cap with backend:', error);\n }\n },\n\n async setMaxToolRounds({ commit }, newRounds) {\n // Clamp to the backend-validated [1, 999999] window.\n const raw = Number(newRounds);\n const clamped = Number.isFinite(raw)\n ? Math.max(1, Math.min(999999, Math.round(raw)))\n : 100;\n commit('SET_MAX_TOOL_ROUNDS', clamped);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n maxToolRounds: clamped,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync max tool rounds with backend:', error);\n }\n },\n\n async setModel({ commit, state }, newModel) {\n commit('SET_SELECTED_MODEL', newModel);\n\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n selectedProvider: state.selectedProvider,\n selectedModel: newModel,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('Backend sync failed:', response.status, errorText);\n }\n }\n } catch (error) {\n console.error('Failed to sync model with backend:', error);\n }\n },\n\n async loadUserSettings({ commit, dispatch, state }) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n const response = await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (response.ok) {\n const settings = await response.json();\n const provider = settings.selectedProvider;\n const model = settings.selectedModel;\n\n if (settings.customInstructions !== undefined) {\n commit('SET_CUSTOM_INSTRUCTIONS', settings.customInstructions || '');\n }\n\n if (settings.asyncToolsEnabled !== undefined) {\n commit('SET_ASYNC_TOOLS_ENABLED', settings.asyncToolsEnabled === true);\n }\n\n if (settings.toolOutputCap !== undefined && Number.isFinite(Number(settings.toolOutputCap))) {\n commit('SET_TOOL_OUTPUT_CAP', Number(settings.toolOutputCap));\n }\n\n if (settings.maxToolRounds !== undefined && Number.isFinite(Number(settings.maxToolRounds))) {\n commit('SET_MAX_TOOL_ROUNDS', Number(settings.maxToolRounds));\n }\n\n if (provider) {\n const isCustomProvider = state.customProviders.some((cp) => cp.id === provider);\n\n if (provider === 'Local') {\n await dispatch('fetchLocalModels');\n } else if (isCustomProvider) {\n await dispatch('fetchCustomProviderModels', provider);\n } else if (state.providers.includes(provider)) {\n await dispatch('fetchProviderModels', { provider });\n }\n\n commit('SET_SELECTED_PROVIDER', provider);\n\n if (model) {\n const availableModels = state.allModels[provider] || [];\n if (availableModels.includes(model)) {\n commit('SET_SELECTED_MODEL', model);\n }\n }\n\n // Ensure model is valid for this provider and sync any correction back to DB\n await dispatch('ensureValidModel');\n }\n }\n }\n } catch (error) {\n console.warn('Failed to load user settings from backend:', error);\n }\n },\n\n // Generic function to fetch models for any provider.\n //\n // Stale-while-revalidate: if cache exists, commit it INSTANTLY for zero\n // paint latency and simultaneously fire a background network refresh.\n // If the fresh result diverges from cache, commit the update and overwrite\n // localStorage. Frontend cache is now display-only — never authoritative,\n // never blocks new-model discovery.\n //\n // forceRefresh: true — caller (RefreshModelsButton, provider reconnect)\n // awaits the network round-trip and skips the SWR early-return.\n async fetchProviderModels({ commit, state, dispatch }, { provider, forceRefresh = false } = {}) {\n if (!provider) return [];\n\n // Route non-built-in providers to their dedicated fetchers. Without this,\n // callers (ModelSelector, AgentForge) that pass 'Local' or a custom-provider\n // UUID hit /api/models/<id>/models, which only knows built-in keys and 400s.\n if (String(provider).toLowerCase() === 'local') {\n return dispatch('fetchLocalModels', { forceRefresh });\n }\n if (state.customProviders.some((cp) => cp.id === provider)) {\n return dispatch('fetchCustomProviderModels', provider);\n }\n\n if (state.loadingModels[provider] && !forceRefresh) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const metaCacheKey = `${provider}_metadata`;\n\n // Step 1: Instant paint from cache if available. No TTL gate — cache is\n // for first-paint speed only, revalidation below always runs.\n let cachedModels = null;\n const rawCached = localStorage.getItem(cacheKey);\n if (rawCached) {\n try {\n const parsed = JSON.parse(rawCached);\n cachedModels = parsed.models;\n if (Array.isArray(cachedModels)) {\n commit('SET_PROVIDER_MODELS', { provider, models: cachedModels });\n const cachedMeta = localStorage.getItem(metaCacheKey);\n if (cachedMeta) {\n try {\n commit('SET_MODEL_METADATA', { provider, metadata: JSON.parse(cachedMeta) });\n } catch { /* ignore malformed meta */ }\n }\n }\n } catch {\n // Malformed cache — nuke it, treat as no cache\n localStorage.removeItem(cacheKey);\n }\n }\n\n // Step 2: revalidate. If we already painted from cache and forceRefresh\n // is false, run in background and return cached immediately. Otherwise\n // await the network.\n const providerLower = resolveProviderKey(provider);\n\n const revalidate = async () => {\n const token = localStorage.getItem('token');\n const isLocalProvider = providerLower === 'openai-codex' || providerLower === 'claude-code' || providerLower === 'gemini-cli' || providerLower === 'antigravity' || providerLower === 'grok-build' || providerLower === 'cursor-cli';\n if (!token && !isLocalProvider) {\n throw new Error(`Authentication required to fetch ${provider} models`);\n }\n\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/models`, { headers });\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n // Only mutate Vuex + localStorage if the list actually changed. Prevents\n // pointless renders and lets model-selector components diff cleanly.\n const changed = !cachedModels\n || cachedModels.length !== models.length\n || JSON.stringify(cachedModels) !== JSON.stringify(models);\n\n if (changed) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n localStorage.setItem(cacheKey, JSON.stringify({ models, timestamp: Date.now() }));\n if (cachedModels) {\n console.log(`[aiProvider] ${provider} model list changed on revalidate (${cachedModels.length} → ${models.length})`);\n }\n }\n\n // Metadata revalidation runs alongside models.\n try {\n const metaRes = await fetch(`${API_CONFIG.BASE_URL}/models/${providerLower}/metadata`);\n if (metaRes.ok) {\n const metaData = await metaRes.json();\n if (metaData.success && metaData.metadata) {\n commit('SET_MODEL_METADATA', { provider, metadata: metaData.metadata });\n localStorage.setItem(metaCacheKey, JSON.stringify(metaData.metadata));\n }\n }\n } catch { /* non-critical */ }\n\n return models;\n };\n\n // Fast-path: we have cache AND forceRefresh is false. Return cached\n // instantly, revalidate in background. Loading indicator not shown —\n // the user already sees a populated dropdown.\n if (cachedModels && !forceRefresh) {\n revalidate().catch((err) => {\n console.warn(`[aiProvider] Background revalidate failed for ${provider}:`, err.message);\n });\n return cachedModels;\n }\n\n // Slow-path: no cache OR forceRefresh. Await the network.\n commit('SET_LOADING_MODELS', { provider, loading: true });\n try {\n const models = await revalidate();\n console.log(`Fetched ${models.length} ${provider} models`);\n return models;\n } catch (error) {\n console.error(`Failed to fetch ${provider} models:`, error);\n // Last-resort fallback: any cached data is better than empty.\n if (cachedModels) return cachedModels;\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n // Full three-layer bust (frontend localStorage + backend 60-min cache +\n // client-versions for CLI-subscription providers) followed by a fresh\n // dispatch so Vuex state ends up with the current model list. Used by the\n // RefreshModelsButton component.\n //\n // With SWR in fetchProviderModels this is now an emergency escape hatch,\n // not a routine action — normal use ships fresh models within one refresh\n // cycle without any manual button press.\n async hardRefreshProviderModels({ dispatch }, { provider }) {\n if (!provider) throw new Error('provider required');\n const providerLower = resolveProviderKey(provider);\n if (!providerLower) throw new Error(`Unknown provider: ${provider}`);\n\n // 1. Clear frontend localStorage caches for BOTH the display-name and\n // lowercase spellings so no split-brain survives. Providers can be\n // referenced as 'OpenAI-Codex' (display) or 'openai-codex' (key) at\n // different call sites; belt-and-suspenders coverage kills that class.\n localStorage.removeItem(`${provider}_models`);\n localStorage.removeItem(`${provider}_metadata`);\n localStorage.removeItem(`${providerLower}_models`);\n localStorage.removeItem(`${providerLower}_metadata`);\n\n const token = localStorage.getItem('token');\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers.Authorization = `Bearer ${token}`;\n\n // 2. Bust client-version cache for CLI-subscription providers so the\n // subsequent /models call uses the freshest upstream CLI version.\n const CLI_KEYS = ['openai-codex', 'claude-code', 'kimi-code', 'grok-build', 'cursor-cli'];\n if (CLI_KEYS.includes(providerLower)) {\n try {\n await fetch(`${API_CONFIG.BASE_URL}/admin/client-versions/refresh`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ keys: [providerLower] }),\n });\n } catch (err) {\n console.warn(`[hardRefresh] client-version refresh failed for ${providerLower}:`, err.message);\n }\n }\n\n // 3. Bust the backend 60-minute in-process models cache and refetch.\n const refreshRes = await fetch(\n `${API_CONFIG.BASE_URL}/models/${providerLower}/models/refresh`,\n { method: 'POST', headers },\n );\n if (!refreshRes.ok) {\n const errBody = await refreshRes.json().catch(() => ({}));\n throw new Error(\n errBody?.error || `Backend refresh failed: HTTP ${refreshRes.status}`,\n );\n }\n\n // 4. Re-dispatch the per-provider fetch with forceRefresh so Vuex state\n // picks up the fresh list (frontend cache is already cleared above).\n const actionFullName = PROVIDER_FETCH_ACTIONS[provider];\n if (actionFullName) {\n const actionName = actionFullName.replace('aiProvider/', '');\n return dispatch(actionName, { forceRefresh: true });\n }\n return dispatch('fetchProviderModels', { provider, forceRefresh: true });\n },\n\n // Per-provider fetch actions (thin wrappers for backward compatibility)\n async fetchOpenRouterModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh });\n },\n async refreshOpenRouterModels({ dispatch }) {\n return dispatch('fetchProviderModels', { provider: 'OpenRouter', forceRefresh: true });\n },\n async fetchAnthropicModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Anthropic', forceRefresh });\n },\n async fetchOpenAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI', forceRefresh });\n },\n async fetchOpenAICodexModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'OpenAI-Codex', forceRefresh });\n },\n async fetchGeminiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini', forceRefresh });\n },\n async fetchGeminiCLIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Gemini-CLI', forceRefresh });\n },\n async fetchGrokAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'GrokAI', forceRefresh });\n },\n async fetchGroqModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Groq', forceRefresh });\n },\n async fetchTogetherAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'TogetherAI', forceRefresh });\n },\n async fetchCerebrasModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Cerebras', forceRefresh });\n },\n async fetchChutesModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Chutes', forceRefresh });\n },\n async fetchClaudeCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Claude-Code', forceRefresh });\n },\n async fetchDeepSeekModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'DeepSeek', forceRefresh });\n },\n async fetchKimiModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi', forceRefresh });\n },\n async fetchKimiCodeModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Kimi-Code', forceRefresh });\n },\n async fetchMiniMaxModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'MiniMax', forceRefresh });\n },\n async fetchZAIModels({ dispatch }, { forceRefresh = false } = {}) {\n return dispatch('fetchProviderModels', { provider: 'Z.AI', forceRefresh });\n },\n\n async fetchLocalModels({ commit, state }, { forceRefresh = false } = {}) {\n const provider = 'Local';\n\n // Skip if local LLM is disabled (hosted environments)\n if (DEPLOYMENT_CONFIG.DISABLE_LOCAL_LLM) {\n console.log('Local LLM polling disabled (hosted mode)');\n return [];\n }\n\n // Check if already loading\n if (state.loadingModels[provider]) {\n return state.allModels[provider];\n }\n\n const cacheKey = `${provider}_models`;\n const cached = localStorage.getItem(cacheKey);\n if (!forceRefresh && cached) {\n try {\n const { models, timestamp } = JSON.parse(cached);\n const cacheAge = Date.now() - timestamp;\n const cacheExpiry = 5 * 60 * 1000;\n\n if (cacheAge < cacheExpiry) {\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n }\n } catch (e) {\n console.warn('Failed to parse cached Local models:', e);\n }\n }\n\n commit('SET_LOADING_MODELS', { provider, loading: true });\n\n try {\n const response = await fetch('http://127.0.0.1:1234/v1/models', {\n method: 'GET',\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = (data.data || []).map((model) => model.id);\n\n if (models.length === 0) {\n return state.allModels[provider] || [];\n }\n\n commit('SET_PROVIDER_MODELS', { provider, models });\n\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n models,\n timestamp: Date.now(),\n }),\n );\n\n console.log(`Fetched ${models.length} Local models from LM Studio`);\n return models;\n } catch (error) {\n console.error('Failed to fetch Local models:', error);\n\n const cached = localStorage.getItem(cacheKey);\n if (cached) {\n try {\n const { models } = JSON.parse(cached);\n commit('SET_PROVIDER_MODELS', { provider, models });\n return models;\n } catch (e) {\n console.warn('Failed to parse expired cached Local models:', e);\n }\n }\n\n return state.allModels[provider] || [];\n } finally {\n commit('SET_LOADING_MODELS', { provider, loading: false });\n }\n },\n\n async refreshLocalModels({ dispatch }) {\n return dispatch('fetchLocalModels', { forceRefresh: true });\n },\n\n async setProviderWithModelFetch({ commit, dispatch, state }, newProvider) {\n if (newProvider === 'Local') {\n await dispatch('fetchLocalModels');\n } else {\n await dispatch('fetchProviderModels', { provider: newProvider });\n }\n\n await dispatch('setProvider', newProvider);\n\n // After models are loaded and provider is set, ensure model is valid and re-sync\n await dispatch('ensureValidModel');\n },\n\n async ensureValidModel({ commit, state }) {\n const oldModel = state.selectedModel;\n commit('ENSURE_VALID_MODEL');\n // If model changed, sync the corrected pair to the backend DB\n if (state.selectedModel !== oldModel) {\n try {\n const token = localStorage.getItem('token');\n if (token) {\n await fetch(`${API_CONFIG.BASE_URL}/users/settings`, {\n method: 'PUT',\n headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ selectedProvider: state.selectedProvider, selectedModel: state.selectedModel }),\n });\n }\n } catch (e) {\n console.error('Failed to sync corrected model to backend:', e);\n }\n }\n },\n\n // Custom provider management actions\n fetchCustomProviders: withFreshness('aiProvider.fetchCustomProviders', async ({ commit, state }) => {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch custom providers: ${response.statusText}`);\n }\n\n const data = await response.json();\n const providers = data.providers || [];\n commit('SET_CUSTOM_PROVIDERS', providers);\n\n if (state.selectedProvider) {\n const isBuiltIn = state.providers.includes(state.selectedProvider);\n const isCustom = providers.some((p) => p.id === state.selectedProvider);\n\n if (!isBuiltIn && !isCustom) {\n console.warn(`Selected provider ${state.selectedProvider} no longer exists, clearing selection`);\n commit('SET_SELECTED_PROVIDER', null);\n commit('SET_SELECTED_MODEL', null);\n }\n }\n\n return providers;\n } catch (error) {\n console.error('Error fetching custom providers:', error);\n return [];\n }\n }, { staleAfter: TTL.aiProviderFetchCustomProviders }),\n\n async createCustomProvider({ commit }, providerData) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(providerData),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to create custom provider');\n }\n\n const data = await response.json();\n commit('ADD_CUSTOM_PROVIDER', data.provider);\n return data.provider;\n } catch (error) {\n console.error('Error creating custom provider:', error);\n throw error;\n }\n },\n\n async updateCustomProvider({ commit }, { id, updates }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updates),\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to update custom provider');\n }\n\n const data = await response.json();\n commit('UPDATE_CUSTOM_PROVIDER', { id, updates: data.provider });\n return data.provider;\n } catch (error) {\n console.error('Error updating custom provider:', error);\n throw error;\n }\n },\n\n async deleteCustomProvider({ commit }, id) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${id}`, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.details || error.error || 'Failed to delete custom provider');\n }\n\n commit('REMOVE_CUSTOM_PROVIDER', id);\n } catch (error) {\n console.error('Error deleting custom provider:', error);\n throw error;\n }\n },\n\n async testCustomProviderConnection(_, { base_url, api_key }) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/test`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ base_url, api_key }),\n });\n\n const data = await response.json();\n return data;\n } catch (error) {\n console.error('Error testing custom provider connection:', error);\n return { success: false, error: error.message };\n }\n },\n\n async fetchCustomProviderModels({ commit }, providerId) {\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('Authentication required');\n }\n\n commit('SET_LOADING_MODELS', { provider: providerId, loading: true });\n\n const response = await fetch(`${API_CONFIG.BASE_URL}/custom-providers/${providerId}/models`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.statusText}`);\n }\n\n const data = await response.json();\n const models = data.models || [];\n\n commit('SET_PROVIDER_MODELS', { provider: providerId, models });\n return models;\n } catch (error) {\n console.error('Error fetching custom provider models:', error);\n return [];\n } finally {\n commit('SET_LOADING_MODELS', { provider: providerId, loading: false });\n }\n },\n },\n};\n"}}}}