feat: Implement API and UI for toggling Claude CLI between proxy and paid modes

This commit is contained in:
JEEL TILVA
2026-01-30 13:45:16 +05:30
parent b9b816e2bf
commit 7985524d49
7 changed files with 477 additions and 237 deletions

View File

@@ -356,6 +356,7 @@ Each account object in `accounts.json` contains:
- `/api/accounts/*` - Account management (list, add, remove, refresh) - `/api/accounts/*` - Account management (list, add, remove, refresh)
- `/api/config/*` - Server configuration (read/write) - `/api/config/*` - Server configuration (read/write)
- `/api/claude/config` - Claude CLI settings - `/api/claude/config` - Claude CLI settings
- `/api/claude/mode` - Switch between Proxy/Paid mode (updates settings.json)
- `/api/logs/stream` - SSE endpoint for real-time logs - `/api/logs/stream` - SSE endpoint for real-time logs
- `/api/stats/history` - Retrieve 30-day request history (sorted chronologically) - `/api/stats/history` - Retrieve 30-day request history (sorted chronologically)
- `/api/auth/url` - Generate Google OAuth URL - `/api/auth/url` - Generate Google OAuth URL

View File

@@ -132,7 +132,10 @@ You can configure these settings in two ways:
1. Open the WebUI at `http://localhost:8080`. 1. Open the WebUI at `http://localhost:8080`.
2. Go to **Settings****Claude CLI**. 2. Go to **Settings****Claude CLI**.
3. Select your preferred models and click **Apply to Claude CLI**. 3. Use the **Connection Mode** toggle to switch between:
- **Proxy Mode**: Uses the local proxy server (Antigravity Cloud Code). Configure models, base URL, and presets here.
- **Paid Mode**: Uses the official Anthropic Credits directly (requires your own subscription). This hides proxy settings to prevent accidental misconfiguration.
4. Click **Apply to Claude CLI** to save your changes.
> [!TIP] > **Configuration Precedence**: System environment variables (set in shell profile like `.zshrc`) take precedence over the `settings.json` file. If you use the Web Console to manage settings, ensure you haven't manually exported conflicting variables in your terminal. > [!TIP] > **Configuration Precedence**: System environment variables (set in shell profile like `.zshrc`) take precedence over the `settings.json` file. If you use the Web Console to manage settings, ensure you haven't manually exported conflicting variables in your terminal.
@@ -221,6 +224,18 @@ claude
> **Note:** If Claude Code asks you to select a login method, add `"hasCompletedOnboarding": true` to `~/.claude.json` (macOS/Linux) or `%USERPROFILE%\.claude.json` (Windows), then restart your terminal and try again. > **Note:** If Claude Code asks you to select a login method, add `"hasCompletedOnboarding": true` to `~/.claude.json` (macOS/Linux) or `%USERPROFILE%\.claude.json` (Windows), then restart your terminal and try again.
### Proxy Mode vs. Paid Mode
Toggle in **Settings****Claude CLI**:
| Feature | 🔌 Proxy Mode | 💳 Paid Mode |
| :--- | :--- | :--- |
| **Backend** | Local Server (Antigravity) | Official Anthropic Credits |
| **Cost** | Free (Google Cloud) | Paid (Anthropic Credits) |
| **Models** | Claude + Gemini | Claude Only |
**Paid Mode** automatically clears proxy settings so you can use your official Anthropic account directly.
### Multiple Claude Code Instances (Optional) ### Multiple Claude Code Instances (Optional)
To run both the official Claude Code and Antigravity version simultaneously, add this alias: To run both the official Claude Code and Antigravity version simultaneously, add this alias:

5
package-lock.json generated
View File

@@ -395,7 +395,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -1416,7 +1415,6 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
@@ -1793,7 +1791,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -2522,7 +2519,6 @@
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",
@@ -2647,7 +2643,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },

2
public/css/style.css generated

File diff suppressed because one or more lines are too long

View File

@@ -12,6 +12,10 @@ window.Components.claudeConfig = () => ({
restoring: false, restoring: false,
gemini1mSuffix: false, gemini1mSuffix: false,
// Mode toggle state (proxy/paid)
currentMode: 'proxy', // 'proxy' or 'paid'
modeLoading: false,
// Presets state // Presets state
presets: [], presets: [],
selectedPresetName: '', selectedPresetName: '',
@@ -34,6 +38,7 @@ window.Components.claudeConfig = () => ({
if (this.activeTab === 'claude') { if (this.activeTab === 'claude') {
this.fetchConfig(); this.fetchConfig();
this.fetchPresets(); this.fetchPresets();
this.fetchMode();
} }
// Watch local activeTab (from parent settings scope, skip initial trigger) // Watch local activeTab (from parent settings scope, skip initial trigger)
@@ -41,6 +46,7 @@ window.Components.claudeConfig = () => ({
if (tab === 'claude' && oldTab !== undefined) { if (tab === 'claude' && oldTab !== undefined) {
this.fetchConfig(); this.fetchConfig();
this.fetchPresets(); this.fetchPresets();
this.fetchMode();
} }
}); });
@@ -416,5 +422,70 @@ window.Components.claudeConfig = () => ({
} finally { } finally {
this.deletingPreset = false; this.deletingPreset = false;
} }
},
// ==========================================
// Mode Toggle (Proxy/Paid)
// ==========================================
/**
* Fetch current mode from server
*/
async fetchMode() {
const password = Alpine.store('global').webuiPassword;
try {
const { response, newPassword } = await window.utils.request('/api/claude/mode', {}, password);
if (newPassword) Alpine.store('global').webuiPassword = newPassword;
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (data.status === 'ok') {
this.currentMode = data.mode;
}
} catch (e) {
console.error('Failed to fetch mode:', e);
}
},
/**
* Toggle between proxy and paid mode
* @param {string} newMode - Target mode ('proxy' or 'paid')
*/
async toggleMode(newMode) {
if (this.modeLoading || newMode === this.currentMode) return;
this.modeLoading = true;
const password = Alpine.store('global').webuiPassword;
try {
const { response, newPassword } = await window.utils.request('/api/claude/mode', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: newMode })
}, password);
if (newPassword) Alpine.store('global').webuiPassword = newPassword;
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (data.status === 'ok') {
this.currentMode = data.mode;
this.config = data.config || this.config;
Alpine.store('global').showToast(data.message, 'success');
// Refresh the config and mode state
await this.fetchConfig();
await this.fetchMode();
} else {
throw new Error(data.error || 'Failed to switch mode');
}
} catch (e) {
Alpine.store('global').showToast(
(Alpine.store('global').t('modeToggleFailed') || 'Failed to switch mode') + ': ' + e.message,
'error'
);
} finally {
this.modeLoading = false;
}
} }
}); });

View File

@@ -73,8 +73,7 @@
</label> </label>
<select <select
class="select select-bordered select-sm w-full bg-space-800 border-space-border/50 text-gray-300 focus:border-neon-purple focus:ring-1 focus:ring-neon-purple/50 font-medium transition-all !py-0 leading-tight" class="select select-bordered select-sm w-full bg-space-800 border-space-border/50 text-gray-300 focus:border-neon-purple focus:ring-1 focus:ring-neon-purple/50 font-medium transition-all !py-0 leading-tight"
:value="$store.global.lang" :value="$store.global.lang" @change="$store.global.setLang($event.target.value)">
@change="$store.global.setLang($event.target.value)">
<option value="en">English</option> <option value="en">English</option>
<option value="zh">中文</option> <option value="zh">中文</option>
<option value="tr">Türkçe</option> <option value="tr">Türkçe</option>
@@ -95,13 +94,11 @@
<input type="range" min="10" max="300" class="custom-range custom-range-purple flex-1" <input type="range" min="10" max="300" class="custom-range custom-range-purple flex-1"
x-model.number="$store.settings.refreshInterval" x-model.number="$store.settings.refreshInterval"
:style="`background-size: ${($store.settings.refreshInterval - 10) / 2.9}% 100%`" :style="`background-size: ${($store.settings.refreshInterval - 10) / 2.9}% 100%`"
@change="$store.settings.saveSettings(true)" @change="$store.settings.saveSettings(true)" aria-label="Polling interval slider">
aria-label="Polling interval slider">
<input type="number" min="10" max="300" <input type="number" min="10" max="300"
class="input input-sm input-bordered w-20 bg-space-800 border-space-border text-white font-mono text-center" class="input input-sm input-bordered w-20 bg-space-800 border-space-border text-white font-mono text-center"
x-model.number="$store.settings.refreshInterval" x-model.number="$store.settings.refreshInterval"
@change="$store.settings.saveSettings(true)" @change="$store.settings.saveSettings(true)" aria-label="Polling interval value">
aria-label="Polling interval value">
</div> </div>
<div class="w-full flex justify-between text-xs px-2 mt-2 text-gray-600 font-mono"> <div class="w-full flex justify-between text-xs px-2 mt-2 text-gray-600 font-mono">
<span>10s</span> <span>10s</span>
@@ -118,15 +115,14 @@
x-text="$store.settings.logLimit + ' ' + $store.global.t('lines')"></span> x-text="$store.settings.logLimit + ' ' + $store.global.t('lines')"></span>
</label> </label>
<div class="flex gap-3 items-center"> <div class="flex gap-3 items-center">
<input type="range" min="500" max="5000" step="500" class="custom-range custom-range-purple flex-1" <input type="range" min="500" max="5000" step="500"
class="custom-range custom-range-purple flex-1"
x-model.number="$store.settings.logLimit" x-model.number="$store.settings.logLimit"
:style="`background-size: ${($store.settings.logLimit - 500) / 45}% 100%`" :style="`background-size: ${($store.settings.logLimit - 500) / 45}% 100%`"
@change="$store.settings.saveSettings(true)" @change="$store.settings.saveSettings(true)" aria-label="Log buffer size slider">
aria-label="Log buffer size slider">
<input type="number" min="500" max="5000" step="500" <input type="number" min="500" max="5000" step="500"
class="input input-sm input-bordered w-24 bg-space-800 border-space-border text-white font-mono text-center" class="input input-sm input-bordered w-24 bg-space-800 border-space-border text-white font-mono text-center"
x-model.number="$store.settings.logLimit" x-model.number="$store.settings.logLimit" @change="$store.settings.saveSettings(true)"
@change="$store.settings.saveSettings(true)"
aria-label="Log buffer size value"> aria-label="Log buffer size value">
</div> </div>
<div class="w-full flex justify-between text-xs px-2 mt-2 text-gray-600 font-mono"> <div class="w-full flex justify-between text-xs px-2 mt-2 text-gray-600 font-mono">
@@ -194,41 +190,101 @@
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg> </svg>
<span class="text-gray-400"> <span class="text-gray-400">
<span x-text="$store.global.t('claudeSettingsAlertPrefix')">Settings below directly modify</span> <span x-text="$store.global.t('claudeSettingsAlertPrefix')">Settings below directly
modify</span>
<code class="text-neon-cyan font-mono" x-text="configPath">~/.claude/settings.json</code>. <code class="text-neon-cyan font-mono" x-text="configPath">~/.claude/settings.json</code>.
<span x-text="$store.global.t('claudeSettingsAlertSuffix')">Restart Claude CLI to apply.</span> <span x-text="$store.global.t('claudeSettingsAlertSuffix')">Restart Claude CLI to apply.</span>
</span> </span>
</div> </div>
<!-- Mode Toggle (Proxy/Paid) -->
<div class="card bg-space-900/30 border border-space-border/50 p-5">
<label class="label text-xs uppercase text-gray-500 font-semibold mb-3">Connection Mode</label>
<div class="flex items-center justify-between">
<div class="flex flex-col gap-1">
<div class="flex items-center gap-2">
<span class="text-sm font-semibold"
:class="currentMode === 'proxy' ? 'text-neon-purple' : 'text-neon-green'">
<template x-if="currentMode === 'proxy'"><span>🔌 Proxy Mode</span></template>
<template x-if="currentMode === 'paid'"><span>💳 Paid Mode</span></template>
</span>
<span x-show="modeLoading"
class="loading loading-spinner loading-xs text-gray-400"></span>
</div>
<span class="text-[11px] text-gray-500">
<template x-if="currentMode === 'proxy'"><span>Using local proxy server
(localhost:8080)</span></template>
<template x-if="currentMode === 'paid'"><span>Using official Anthropic API (requires
subscription)</span></template>
</span>
</div>
<div class="flex items-center gap-3">
<span class="text-xs font-medium"
:class="currentMode === 'proxy' ? 'text-neon-purple' : 'text-gray-500'">Proxy</span>
<input type="checkbox" class="toggle toggle-sm"
:class="currentMode === 'paid' ? 'toggle-success' : 'toggle-secondary'"
:checked="currentMode === 'paid'" :disabled="modeLoading"
@change="toggleMode($event.target.checked ? 'paid' : 'proxy')"
aria-label="Toggle between Proxy and Paid mode">
<span class="text-xs font-medium"
:class="currentMode === 'paid' ? 'text-neon-green' : 'text-gray-500'">Paid</span>
</div>
</div>
</div>
<!-- Paid Mode Info (shown only in paid mode) -->
<div x-show="currentMode === 'paid'" class="card bg-neon-green/5 border border-neon-green/30 p-5">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-neon-green flex-shrink-0 mt-0.5" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p class="text-sm font-semibold text-neon-green">Claude CLI is using the official Anthropic
API</p>
<p class="text-[11px] text-gray-400 mt-1">All proxy configuration has been removed. Claude
CLI uses your Anthropic subscription directly.</p>
<p class="text-[10px] text-gray-500 mt-2">Switch to Proxy mode to configure model routing
and presets.</p>
</div>
</div>
</div>
<!-- Configuration Presets --> <!-- Configuration Presets -->
<div class="card bg-space-900/30 border border-neon-cyan/30 p-5"> <div x-show="currentMode === 'proxy'" class="card bg-space-900/30 border border-neon-cyan/30 p-5">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-neon-cyan" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-neon-cyan" fill="none"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" /> viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
</svg> </svg>
<label class="text-xs uppercase text-neon-cyan font-semibold" x-text="$store.global.t('configPresets') || 'Configuration Presets'">Configuration Presets</label> <label class="text-xs uppercase text-neon-cyan font-semibold"
x-text="$store.global.t('configPresets') || 'Configuration Presets'">Configuration
Presets</label>
</div> </div>
<button class="btn btn-xs btn-ghost text-neon-cyan hover:bg-neon-cyan/10 gap-1" <button class="btn btn-xs btn-ghost text-neon-cyan hover:bg-neon-cyan/10 gap-1"
@click="saveCurrentAsPreset()" @click="saveCurrentAsPreset()" :disabled="savingPreset">
:disabled="savingPreset"> <svg x-show="!savingPreset" xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5"
<svg x-show="!savingPreset" xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4" />
</svg> </svg>
<span x-show="!savingPreset" x-text="$store.global.t('saveAsPreset') || 'Save as Preset'">Save as Preset</span> <span x-show="!savingPreset"
x-text="$store.global.t('saveAsPreset') || 'Save as Preset'">Save as Preset</span>
<span x-show="savingPreset" class="loading loading-spinner loading-xs"></span> <span x-show="savingPreset" class="loading loading-spinner loading-xs"></span>
</button> </button>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<select <select
class="select select-sm bg-space-800 border-space-border text-white flex-1 font-mono text-xs" class="select select-sm bg-space-800 border-space-border text-white flex-1 font-mono text-xs"
:disabled="presets.length === 0" :disabled="presets.length === 0" :value="selectedPresetName"
:value="selectedPresetName" @change="onPresetSelect($event.target.value)" aria-label="Select preset">
@change="onPresetSelect($event.target.value)"
aria-label="Select preset">
<option value="" disabled x-show="presets.length === 0">No presets available</option> <option value="" disabled x-show="presets.length === 0">No presets available</option>
<template x-for="preset in presets" :key="preset.name"> <template x-for="preset in presets" :key="preset.name">
<option :value="preset.name" x-text="preset.name" :selected="preset.name === selectedPresetName"></option> <option :value="preset.name" x-text="preset.name"
:selected="preset.name === selectedPresetName"></option>
</template> </template>
</select> </select>
<button class="btn btn-sm btn-ghost text-red-400 hover:bg-red-500/10" <button class="btn btn-sm btn-ghost text-red-400 hover:bg-red-500/10"
@@ -236,18 +292,23 @@
:disabled="!selectedPresetName || presets.length === 0 || deletingPreset" :disabled="!selectedPresetName || presets.length === 0 || deletingPreset"
:title="$store.global.t('deletePreset') || 'Delete preset'"> :title="$store.global.t('deletePreset') || 'Delete preset'">
<span x-show="!deletingPreset"> <span x-show="!deletingPreset">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg> </svg>
</span> </span>
<span x-show="deletingPreset" class="loading loading-spinner loading-xs"></span> <span x-show="deletingPreset" class="loading loading-spinner loading-xs"></span>
</button> </button>
</div> </div>
<p class="text-[10px] text-gray-600 mt-2" x-text="$store.global.t('presetHint') || 'Select a preset to load it. Click \"Apply to Claude CLI\" to save changes.'">Select a preset to load it. Click "Apply to Claude CLI" to save changes.</p> <p class="text-[10px] text-gray-600 mt-2"
x-text="$store.global.t('presetHint') || 'Select a preset to load it. Click \" Apply to Claude
CLI\" to save changes.'">Select a preset to load it. Click "Apply to Claude CLI" to save
changes.</p>
</div> </div>
<!-- Base URL --> <!-- Base URL -->
<div class="card bg-space-900/30 border border-space-border/50 p-5"> <div x-show="currentMode === 'proxy'" class="card bg-space-900/30 border border-space-border/50 p-5">
<label class="label text-xs uppercase text-gray-500 font-semibold mb-2" <label class="label text-xs uppercase text-gray-500 font-semibold mb-2"
x-text="$store.global.t('proxyConnection')">Proxy Connection</label> x-text="$store.global.t('proxyConnection')">Proxy Connection</label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@@ -266,7 +327,7 @@
</div> </div>
<!-- Models Selection --> <!-- Models Selection -->
<div class="card bg-space-900/30 border border-space-border/50 p-5"> <div x-show="currentMode === 'proxy'" class="card bg-space-900/30 border border-space-border/50 p-5">
<label class="label text-xs uppercase text-gray-500 font-semibold mb-2" <label class="label text-xs uppercase text-gray-500 font-semibold mb-2"
x-text="$store.global.t('modelSelection')">Model Selection</label> x-text="$store.global.t('modelSelection')">Model Selection</label>
@@ -276,10 +337,8 @@
<label class="label pt-0 pb-1 text-[11px] text-gray-400 font-bold tracking-wider" <label class="label pt-0 pb-1 text-[11px] text-gray-400 font-bold tracking-wider"
x-text="$store.global.t('primaryModel')">Primary Model</label> x-text="$store.global.t('primaryModel')">Primary Model</label>
<div class="relative w-full" x-data="{ open: false, searchTerm: '' }"> <div class="relative w-full" x-data="{ open: false, searchTerm: '' }">
<input type="text" <input type="text" :value="open ? searchTerm : config.env.ANTHROPIC_MODEL"
:value="open ? searchTerm : config.env.ANTHROPIC_MODEL" @input="searchTerm = $event.target.value" @focus="open = true; searchTerm = ''"
@input="searchTerm = $event.target.value"
@focus="open = true; searchTerm = ''"
@click.away="open = false; searchTerm = ''" @click.away="open = false; searchTerm = ''"
class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-cyan pr-8 placeholder:!text-gray-600" class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-cyan pr-8 placeholder:!text-gray-600"
:placeholder="open ? $store.global.t('typeToSearch') : ''" :placeholder="open ? $store.global.t('typeToSearch') : ''"
@@ -293,25 +352,28 @@
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100" x-transition:enter-end="opacity-100 scale-100"
class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar"> class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar">
<template <template
x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))" x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))"
:key="modelId"> :key="modelId">
<li> <li>
<a @mousedown.prevent="selectModel('ANTHROPIC_MODEL', modelId); open = false; searchTerm = ''" <a @mousedown.prevent="selectModel('ANTHROPIC_MODEL', modelId); open = false; searchTerm = ''"
class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2" class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2"
:class="config.env.ANTHROPIC_MODEL === modelId || config.env.ANTHROPIC_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'"> :class="config.env.ANTHROPIC_MODEL === modelId || config.env.ANTHROPIC_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full" <span class="w-1.5 h-1.5 rounded-full"
:class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span> :class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span>
<span x-text="modelId"></span> <span x-text="modelId"></span>
</div> </div>
<template x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'"> <template
<span class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span> x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'">
</template> <span
</a> class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span>
</li> </template>
</template> </a>
<li x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0"> </li>
</template>
<li
x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0">
<span class="text-xs text-gray-500 italic py-2" <span class="text-xs text-gray-500 italic py-2"
x-text="$store.global.t('noMatchingModels')">No matching models</span> x-text="$store.global.t('noMatchingModels')">No matching models</span>
</li> </li>
@@ -325,10 +387,8 @@
<label class="label pt-0 pb-1 text-[11px] text-gray-400 font-bold tracking-wider" <label class="label pt-0 pb-1 text-[11px] text-gray-400 font-bold tracking-wider"
x-text="$store.global.t('subAgentModel')">Sub-agent Model</label> x-text="$store.global.t('subAgentModel')">Sub-agent Model</label>
<div class="relative w-full" x-data="{ open: false, searchTerm: '' }"> <div class="relative w-full" x-data="{ open: false, searchTerm: '' }">
<input type="text" <input type="text" :value="open ? searchTerm : config.env.CLAUDE_CODE_SUBAGENT_MODEL"
:value="open ? searchTerm : config.env.CLAUDE_CODE_SUBAGENT_MODEL" @input="searchTerm = $event.target.value" @focus="open = true; searchTerm = ''"
@input="searchTerm = $event.target.value"
@focus="open = true; searchTerm = ''"
@click.away="open = false; searchTerm = ''" @click.away="open = false; searchTerm = ''"
class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-purple pr-8 placeholder:!text-gray-600" class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-purple pr-8 placeholder:!text-gray-600"
:placeholder="open ? $store.global.t('typeToSearch') : ''" :placeholder="open ? $store.global.t('typeToSearch') : ''"
@@ -342,25 +402,28 @@
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100" x-transition:enter-end="opacity-100 scale-100"
class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar"> class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar">
<template <template
x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))" x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))"
:key="modelId"> :key="modelId">
<li> <li>
<a @mousedown.prevent="selectModel('CLAUDE_CODE_SUBAGENT_MODEL', modelId); open = false; searchTerm = ''" <a @mousedown.prevent="selectModel('CLAUDE_CODE_SUBAGENT_MODEL', modelId); open = false; searchTerm = ''"
class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2" class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2"
:class="config.env.CLAUDE_CODE_SUBAGENT_MODEL === modelId || config.env.CLAUDE_CODE_SUBAGENT_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'"> :class="config.env.CLAUDE_CODE_SUBAGENT_MODEL === modelId || config.env.CLAUDE_CODE_SUBAGENT_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full" <span class="w-1.5 h-1.5 rounded-full"
:class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span> :class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span>
<span x-text="modelId"></span> <span x-text="modelId"></span>
</div> </div>
<template x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'"> <template
<span class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span> x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'">
</template> <span
</a> class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span>
</li> </template>
</template> </a>
<li x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0"> </li>
</template>
<li
x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0">
<span class="text-xs text-gray-500 italic py-2" <span class="text-xs text-gray-500 italic py-2"
x-text="$store.global.t('noMatchingModels')">No matching models</span> x-text="$store.global.t('noMatchingModels')">No matching models</span>
</li> </li>
@@ -380,10 +443,8 @@
<label class="label pt-0 pb-1 text-[10px] text-gray-500 uppercase font-bold" <label class="label pt-0 pb-1 text-[10px] text-gray-500 uppercase font-bold"
x-text="$store.global.t('opusAlias')">Opus Alias</label> x-text="$store.global.t('opusAlias')">Opus Alias</label>
<div class="relative w-full" x-data="{ open: false, searchTerm: '' }"> <div class="relative w-full" x-data="{ open: false, searchTerm: '' }">
<input type="text" <input type="text" :value="open ? searchTerm : config.env.ANTHROPIC_DEFAULT_OPUS_MODEL"
:value="open ? searchTerm : config.env.ANTHROPIC_DEFAULT_OPUS_MODEL" @input="searchTerm = $event.target.value" @focus="open = true; searchTerm = ''"
@input="searchTerm = $event.target.value"
@focus="open = true; searchTerm = ''"
@click.away="open = false; searchTerm = ''" @click.away="open = false; searchTerm = ''"
class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-cyan pr-8 placeholder:!text-gray-600" class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-cyan pr-8 placeholder:!text-gray-600"
:placeholder="open ? $store.global.t('searchPlaceholder') : ''" :placeholder="open ? $store.global.t('searchPlaceholder') : ''"
@@ -396,25 +457,28 @@
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100" x-transition:enter-end="opacity-100 scale-100"
class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar"> class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar">
<template <template
x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))" x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))"
:key="modelId"> :key="modelId">
<li> <li>
<a @mousedown.prevent="selectModel('ANTHROPIC_DEFAULT_OPUS_MODEL', modelId); open = false; searchTerm = ''" <a @mousedown.prevent="selectModel('ANTHROPIC_DEFAULT_OPUS_MODEL', modelId); open = false; searchTerm = ''"
class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2" class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2"
:class="config.env.ANTHROPIC_DEFAULT_OPUS_MODEL === modelId || config.env.ANTHROPIC_DEFAULT_OPUS_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'"> :class="config.env.ANTHROPIC_DEFAULT_OPUS_MODEL === modelId || config.env.ANTHROPIC_DEFAULT_OPUS_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full" <span class="w-1.5 h-1.5 rounded-full"
:class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span> :class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span>
<span x-text="modelId"></span> <span x-text="modelId"></span>
</div> </div>
<template x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'"> <template
<span class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span> x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'">
</template> <span
</a> class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span>
</li> </template>
</template> </a>
<li x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0"> </li>
</template>
<li
x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0">
<span class="text-xs text-gray-500 italic py-2" <span class="text-xs text-gray-500 italic py-2"
x-text="$store.global.t('noMatchingModels')">No matching models</span> x-text="$store.global.t('noMatchingModels')">No matching models</span>
</li> </li>
@@ -428,8 +492,7 @@
<div class="relative w-full" x-data="{ open: false, searchTerm: '' }"> <div class="relative w-full" x-data="{ open: false, searchTerm: '' }">
<input type="text" <input type="text"
:value="open ? searchTerm : config.env.ANTHROPIC_DEFAULT_SONNET_MODEL" :value="open ? searchTerm : config.env.ANTHROPIC_DEFAULT_SONNET_MODEL"
@input="searchTerm = $event.target.value" @input="searchTerm = $event.target.value" @focus="open = true; searchTerm = ''"
@focus="open = true; searchTerm = ''"
@click.away="open = false; searchTerm = ''" @click.away="open = false; searchTerm = ''"
class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-cyan pr-8 placeholder:!text-gray-600" class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-cyan pr-8 placeholder:!text-gray-600"
:placeholder="open ? $store.global.t('searchPlaceholder') : ''" :placeholder="open ? $store.global.t('searchPlaceholder') : ''"
@@ -442,25 +505,28 @@
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100" x-transition:enter-end="opacity-100 scale-100"
class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar"> class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar">
<template <template
x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))" x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))"
:key="modelId"> :key="modelId">
<li> <li>
<a @mousedown.prevent="selectModel('ANTHROPIC_DEFAULT_SONNET_MODEL', modelId); open = false; searchTerm = ''" <a @mousedown.prevent="selectModel('ANTHROPIC_DEFAULT_SONNET_MODEL', modelId); open = false; searchTerm = ''"
class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2" class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2"
:class="config.env.ANTHROPIC_DEFAULT_SONNET_MODEL === modelId || config.env.ANTHROPIC_DEFAULT_SONNET_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'"> :class="config.env.ANTHROPIC_DEFAULT_SONNET_MODEL === modelId || config.env.ANTHROPIC_DEFAULT_SONNET_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full" <span class="w-1.5 h-1.5 rounded-full"
:class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span> :class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span>
<span x-text="modelId"></span> <span x-text="modelId"></span>
</div> </div>
<template x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'"> <template
<span class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span> x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'">
</template> <span
</a> class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span>
</li> </template>
</template> </a>
<li x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0"> </li>
</template>
<li
x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0">
<span class="text-xs text-gray-500 italic py-2" <span class="text-xs text-gray-500 italic py-2"
x-text="$store.global.t('noMatchingModels')">No matching models</span> x-text="$store.global.t('noMatchingModels')">No matching models</span>
</li> </li>
@@ -472,10 +538,8 @@
<label class="label pt-0 pb-1 text-[10px] text-gray-500 uppercase font-bold" <label class="label pt-0 pb-1 text-[10px] text-gray-500 uppercase font-bold"
x-text="$store.global.t('haikuAlias')">Haiku Alias</label> x-text="$store.global.t('haikuAlias')">Haiku Alias</label>
<div class="relative w-full" x-data="{ open: false, searchTerm: '' }"> <div class="relative w-full" x-data="{ open: false, searchTerm: '' }">
<input type="text" <input type="text" :value="open ? searchTerm : config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL"
:value="open ? searchTerm : config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL" @input="searchTerm = $event.target.value" @focus="open = true; searchTerm = ''"
@input="searchTerm = $event.target.value"
@focus="open = true; searchTerm = ''"
@click.away="open = false; searchTerm = ''" @click.away="open = false; searchTerm = ''"
class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-cyan pr-8 placeholder:!text-gray-600" class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-cyan pr-8 placeholder:!text-gray-600"
:placeholder="open ? $store.global.t('searchPlaceholder') : ''" :placeholder="open ? $store.global.t('searchPlaceholder') : ''"
@@ -488,25 +552,28 @@
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100" x-transition:enter-end="opacity-100 scale-100"
class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar"> class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar">
<template <template
x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))" x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))"
:key="modelId"> :key="modelId">
<li> <li>
<a @mousedown.prevent="selectModel('ANTHROPIC_DEFAULT_HAIKU_MODEL', modelId); open = false; searchTerm = ''" <a @mousedown.prevent="selectModel('ANTHROPIC_DEFAULT_HAIKU_MODEL', modelId); open = false; searchTerm = ''"
class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2" class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2"
:class="config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL === modelId || config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'"> :class="config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL === modelId || config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full" <span class="w-1.5 h-1.5 rounded-full"
:class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span> :class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span>
<span x-text="modelId"></span> <span x-text="modelId"></span>
</div> </div>
<template x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'"> <template
<span class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span> x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'">
</template> <span
</a> class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span>
</li> </template>
</template> </a>
<li x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0"> </li>
</template>
<li
x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0">
<span class="text-xs text-gray-500 italic py-2" <span class="text-xs text-gray-500 italic py-2"
x-text="$store.global.t('noMatchingModels')">No matching models</span> x-text="$store.global.t('noMatchingModels')">No matching models</span>
</li> </li>
@@ -517,14 +584,15 @@
</div> </div>
<!-- MCP CLI Experimental Mode --> <!-- MCP CLI Experimental Mode -->
<div class="card bg-space-900/30 border border-space-border/50 p-5"> <div x-show="currentMode === 'proxy'" class="card bg-space-900/30 border border-space-border/50 p-5">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-sm font-medium transition-colors" <span class="text-sm font-medium transition-colors"
:class="config.env.ENABLE_EXPERIMENTAL_MCP_CLI === 'true' ? 'text-neon-green' : 'text-gray-300'" :class="config.env.ENABLE_EXPERIMENTAL_MCP_CLI === 'true' ? 'text-neon-green' : 'text-gray-300'"
x-text="$store.global.t('mcpCliExperimental')">Experimental MCP CLI</span> x-text="$store.global.t('mcpCliExperimental')">Experimental MCP CLI</span>
<span class="text-[11px] text-gray-500" x-text="$store.global.t('mcpCliDesc')"> <span class="text-[11px] text-gray-500" x-text="$store.global.t('mcpCliDesc')">
Enables experimental MCP integration for reliable tool usage with reduced context consumption. Enables experimental MCP integration for reliable tool usage with reduced context
consumption.
</span> </span>
</div> </div>
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
@@ -540,7 +608,7 @@
</div> </div>
<!-- Gemini 1M Context Suffix Toggle --> <!-- Gemini 1M Context Suffix Toggle -->
<div class="card bg-space-900/30 border border-space-border/50 p-5"> <div x-show="currentMode === 'proxy'" class="card bg-space-900/30 border border-space-border/50 p-5">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-sm font-medium transition-colors" <span class="text-sm font-medium transition-colors"
@@ -554,8 +622,7 @@
</span> </span>
</div> </div>
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer" <input type="checkbox" class="sr-only peer" :checked="gemini1mSuffix"
:checked="gemini1mSuffix"
@change="toggleGemini1mSuffix($event.target.checked)" @change="toggleGemini1mSuffix($event.target.checked)"
aria-label="Gemini 1M context mode toggle"> aria-label="Gemini 1M context mode toggle">
<div <div
@@ -565,8 +632,9 @@
</div> </div>
</div> </div>
<div class="flex justify-end pt-2 gap-3"> <div x-show="currentMode === 'proxy'" class="flex justify-end pt-2 gap-3">
<button class="btn btn-sm btn-ghost border border-space-border/50 hover:border-red-500/30 hover:bg-red-500/5 text-gray-400 hover:text-red-400 px-6 gap-2" <button
class="btn btn-sm btn-ghost border border-space-border/50 hover:border-red-500/30 hover:bg-red-500/5 text-gray-400 hover:text-red-400 px-6 gap-2"
@click="restoreDefaultClaudeConfig" :disabled="restoring"> @click="restoreDefaultClaudeConfig" :disabled="restoring">
<svg x-show="!restoring" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg x-show="!restoring" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -591,20 +659,23 @@
<div class="modal-box bg-space-900 border-2 border-red-500/50"> <div class="modal-box bg-space-900 border-2 border-red-500/50">
<h3 class="font-bold text-lg text-red-400 flex items-center gap-2"> <h3 class="font-bold text-lg text-red-400 flex items-center gap-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg> </svg>
<span x-text="$store.global.t('confirmRestoreTitle')">Confirm Restore</span> <span x-text="$store.global.t('confirmRestoreTitle')">Confirm Restore</span>
</h3> </h3>
<p class="py-4 text-gray-300" x-text="$store.global.t('confirmRestoreMessage')"> <p class="py-4 text-gray-300" x-text="$store.global.t('confirmRestoreMessage')">
Are you sure you want to restore Claude CLI to default settings? This will remove proxy configuration. Are you sure you want to restore Claude CLI to default settings? This will remove proxy
configuration.
</p> </p>
<div class="modal-action"> <div class="modal-action">
<button class="btn btn-ghost text-gray-400" onclick="document.getElementById('restore_defaults_modal').close()" <button class="btn btn-ghost text-gray-400"
onclick="document.getElementById('restore_defaults_modal').close()"
x-text="$store.global.t('cancel')">Cancel</button> x-text="$store.global.t('cancel')">Cancel</button>
<button class="btn bg-red-500 hover:bg-red-600 border-none text-white" @click="executeRestore()" <button class="btn bg-red-500 hover:bg-red-600 border-none text-white"
:disabled="restoring" @click="executeRestore()" :disabled="restoring" :class="{ 'loading': restoring }">
:class="{ 'loading': restoring }"> <span x-text="$store.global.t('confirmRestore')" x-show="!restoring">Confirm
<span x-text="$store.global.t('confirmRestore')" x-show="!restoring">Confirm Restore</span> Restore</span>
</button> </button>
</div> </div>
</div> </div>
@@ -618,19 +689,25 @@
<div class="modal-box bg-space-900 border-2 border-yellow-500/50"> <div class="modal-box bg-space-900 border-2 border-yellow-500/50">
<h3 class="font-bold text-lg text-yellow-400 flex items-center gap-2"> <h3 class="font-bold text-lg text-yellow-400 flex items-center gap-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg> </svg>
<span x-text="$store.global.t('unsavedChangesTitle') || 'Unsaved Changes'">Unsaved Changes</span> <span x-text="$store.global.t('unsavedChangesTitle') || 'Unsaved Changes'">Unsaved
Changes</span>
</h3> </h3>
<p class="py-4 text-gray-300"> <p class="py-4 text-gray-300">
<span x-text="$store.global.t('unsavedChangesMessage') || 'Your current configuration doesn\'t match any saved preset.'">Your current configuration doesn't match any saved preset.</span> <span
x-text="$store.global.t('unsavedChangesMessage') || 'Your current configuration doesn\'t match any saved preset.'">Your
current configuration doesn't match any saved preset.</span>
<br><br> <br><br>
<span class="text-yellow-400/80" x-text="'Load \"' + pendingPresetName + '\" and lose current changes?'"></span> <span class="text-yellow-400/80" x-text="'Load \"' + pendingPresetName + ' \" and lose
current changes?'"></span>
</p> </p>
<div class="modal-action"> <div class="modal-action">
<button class="btn btn-ghost text-gray-400" @click="cancelLoadPreset()" <button class="btn btn-ghost text-gray-400" @click="cancelLoadPreset()"
x-text="$store.global.t('cancel')">Cancel</button> x-text="$store.global.t('cancel')">Cancel</button>
<button class="btn bg-yellow-500 hover:bg-yellow-600 border-none text-black" @click="confirmLoadPreset()"> <button class="btn bg-yellow-500 hover:bg-yellow-600 border-none text-black"
@click="confirmLoadPreset()">
<span x-text="$store.global.t('loadAnyway') || 'Load Anyway'">Load Anyway</span> <span x-text="$store.global.t('loadAnyway') || 'Load Anyway'">Load Anyway</span>
</button> </button>
</div> </div>
@@ -645,16 +722,19 @@
<div class="modal-box bg-space-900 border-2 border-neon-cyan/50"> <div class="modal-box bg-space-900 border-2 border-neon-cyan/50">
<h3 class="font-bold text-lg text-neon-cyan flex items-center gap-2"> <h3 class="font-bold text-lg text-neon-cyan flex items-center gap-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
</svg> </svg>
<span x-text="$store.global.t('savePresetTitle') || 'Save Preset'">Save Preset</span> <span x-text="$store.global.t('savePresetTitle') || 'Save Preset'">Save Preset</span>
</h3> </h3>
<p class="py-2 text-gray-400 text-sm" x-text="$store.global.t('savePresetDesc') || 'Save the current configuration as a reusable preset.'"> <p class="py-2 text-gray-400 text-sm"
x-text="$store.global.t('savePresetDesc') || 'Save the current configuration as a reusable preset.'">
Save the current configuration as a reusable preset. Save the current configuration as a reusable preset.
</p> </p>
<div class="form-control mt-4"> <div class="form-control mt-4">
<label class="label"> <label class="label">
<span class="label-text text-gray-300" x-text="$store.global.t('presetName') || 'Preset Name'">Preset Name</span> <span class="label-text text-gray-300"
x-text="$store.global.t('presetName') || 'Preset Name'">Preset Name</span>
</label> </label>
<input type="text" x-model="newPresetName" <input type="text" x-model="newPresetName"
class="input input-sm input-bordered bg-space-800 border-space-border text-white w-full" class="input input-sm input-bordered bg-space-800 border-space-border text-white w-full"
@@ -664,13 +744,14 @@
aria-label="Preset name"> aria-label="Preset name">
</div> </div>
<div class="modal-action"> <div class="modal-action">
<button class="btn btn-ghost text-gray-400" @click="newPresetName = ''; document.getElementById('save_preset_modal').close()" <button class="btn btn-ghost text-gray-400"
@click="newPresetName = ''; document.getElementById('save_preset_modal').close()"
x-text="$store.global.t('cancel')">Cancel</button> x-text="$store.global.t('cancel')">Cancel</button>
<button class="btn bg-neon-cyan hover:bg-cyan-600 border-none text-black" <button class="btn bg-neon-cyan hover:bg-cyan-600 border-none text-black"
@click="executeSavePreset(newPresetName)" @click="executeSavePreset(newPresetName)"
:disabled="!newPresetName.trim() || savingPreset" :disabled="!newPresetName.trim() || savingPreset" :class="{ 'loading': savingPreset }">
:class="{ 'loading': savingPreset }"> <span x-show="!savingPreset"
<span x-show="!savingPreset" x-text="$store.global.t('savePreset') || 'Save Preset'">Save Preset</span> x-text="$store.global.t('savePreset') || 'Save Preset'">Save Preset</span>
</button> </button>
</div> </div>
</div> </div>
@@ -686,11 +767,15 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<div class="text-sm text-gray-400" x-text="$store.global.t('modelsDesc')">Configure model visibility, pinning, and request mapping.</div> <div class="text-sm text-gray-400" x-text="$store.global.t('modelsDesc')">Configure model
<div class="text-xs text-gray-600 mt-1" x-text="$store.global.t('modelMappingHint')">Model mapping: server-side redirection. Claude Code users: see 'Claude CLI' tab for client-side setup.</div> visibility, pinning, and request mapping.</div>
<div class="text-xs text-gray-600 mt-1" x-text="$store.global.t('modelMappingHint')">Model
mapping: server-side redirection. Claude Code users: see 'Claude CLI' tab for client-side
setup.</div>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-xs text-gray-500" x-text="$store.global.t('showHidden')">Show Hidden Models</span> <span class="text-xs text-gray-500" x-text="$store.global.t('showHidden')">Show Hidden
Models</span>
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer" <input type="checkbox" class="sr-only peer"
:checked="$store.settings.showHiddenModels === true" :checked="$store.settings.showHiddenModels === true"
@@ -709,7 +794,8 @@
<thead> <thead>
<tr> <tr>
<th class="pl-4 w-5/12" x-text="$store.global.t('modelId')">Model ID</th> <th class="pl-4 w-5/12" x-text="$store.global.t('modelId')">Model ID</th>
<th class="w-5/12" x-text="$store.global.t('modelMapping')">Mapping (Target Model ID)</th> <th class="w-5/12" x-text="$store.global.t('modelMapping')">Mapping (Target Model ID)
</th>
<th class="w-2/12 text-right pr-4" x-text="$store.global.t('actions')">Actions</th> <th class="w-2/12 text-right pr-4" x-text="$store.global.t('actions')">Actions</th>
</tr> </tr>
</thead> </thead>
@@ -717,8 +803,7 @@
<template x-for="modelId in $store.data.models" :key="modelId"> <template x-for="modelId in $store.data.models" :key="modelId">
<tr class="hover:bg-white/5 transition-colors group" <tr class="hover:bg-white/5 transition-colors group"
:class="isHidden ? 'opacity-50' : ''" :class="isHidden ? 'opacity-50' : ''"
x-show="!isHidden || $store.settings.showHiddenModels" x-show="!isHidden || $store.settings.showHiddenModels" x-data="{
x-data="{
newMapping: '', newMapping: '',
get config() { return $store.data.modelConfig[modelId] || {} }, get config() { return $store.data.modelConfig[modelId] || {} },
get isPinned() { return !!this.config.pinned }, get isPinned() { return !!this.config.pinned },
@@ -749,14 +834,14 @@
</svg> </svg>
</div> </div>
<div x-show="isEditing(modelId)" class="flex items-center gap-1"> <div x-show="isEditing(modelId)" class="flex items-center gap-1">
<select x-model="newMapping" <select x-model="newMapping" :x-ref="'input-' + modelId"
:x-ref="'input-' + modelId"
class="select select-sm bg-space-800 border-space-border text-white focus:outline-none focus:border-neon-cyan flex-1 font-mono text-xs !h-8 min-h-0" class="select select-sm bg-space-800 border-space-border text-white focus:outline-none focus:border-neon-cyan flex-1 font-mono text-xs !h-8 min-h-0"
@keydown.enter="await updateModelConfig(modelId, { mapping: newMapping }); stopEditing()" @keydown.enter="await updateModelConfig(modelId, { mapping: newMapping }); stopEditing()"
@keydown.escape="newMapping = config.mapping || ''; stopEditing()"> @keydown.escape="newMapping = config.mapping || ''; stopEditing()">
<option value="" x-text="$store.global.t('none')">None</option> <option value="" x-text="$store.global.t('none')">None</option>
<template x-for="mId in $store.data.models" :key="mId"> <template x-for="mId in $store.data.models" :key="mId">
<option :value="mId" x-text="mId" :selected="mId === newMapping"></option> <option :value="mId" x-text="mId" :selected="mId === newMapping">
</option>
</template> </template>
</select> </select>
<button class="btn-action-success" <button class="btn-action-success"
@@ -777,14 +862,14 @@
stroke-width="2" d="M6 18L18 6M6 6l12 12" /> stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
<button x-show="config.mapping" <button x-show="config.mapping" class="btn-action-danger"
class="btn-action-danger" @click="await updateModelConfig(modelId, { mapping: '' }); stopEditing()"
@click="await updateModelConfig(modelId, { mapping: '' }); stopEditing()" :title="$store.global.t('delete')">
:title="$store.global.t('delete')">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" <svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none"
viewBox="0 0 24 24" stroke="currentColor"> viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg> </svg>
</button> </button>
</div> </div>
@@ -890,8 +975,7 @@
</div> </div>
<!-- Debug Mode --> <!-- Debug Mode -->
<div <div class="form-control view-card border-space-border/50 hover:border-neon-purple/50">
class="form-control view-card border-space-border/50 hover:border-neon-purple/50">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-sm font-medium text-gray-200" <span class="text-sm font-medium text-gray-200"
@@ -902,8 +986,7 @@
</div> </div>
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer" :checked="serverConfig.debug === true" <input type="checkbox" class="sr-only peer" :checked="serverConfig.debug === true"
@change="toggleDebug($el.checked)" @change="toggleDebug($el.checked)" aria-label="Debug mode toggle">
aria-label="Debug mode toggle">
<div <div
class="w-9 h-5 bg-space-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-600 after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-neon-purple peer-checked:after:bg-white"> class="w-9 h-5 bg-space-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-600 after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-neon-purple peer-checked:after:bg-white">
</div> </div>
@@ -912,8 +995,7 @@
</div> </div>
<!-- Token Cache --> <!-- Token Cache -->
<div <div class="form-control view-card border-space-border/50 hover:border-neon-green/50">
class="form-control view-card border-space-border/50 hover:border-neon-green/50">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-sm font-medium text-gray-200" <span class="text-sm font-medium text-gray-200"
@@ -926,8 +1008,7 @@
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer" <input type="checkbox" class="sr-only peer"
:checked="serverConfig.persistTokenCache === true" :checked="serverConfig.persistTokenCache === true"
@change="toggleTokenCache($el.checked)" @change="toggleTokenCache($el.checked)" aria-label="Persist token cache toggle">
aria-label="Persist token cache toggle">
<div <div
class="w-9 h-5 bg-space-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-600 after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-neon-green peer-checked:after:bg-white"> class="w-9 h-5 bg-space-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-600 after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-neon-green peer-checked:after:bg-white">
</div> </div>
@@ -946,12 +1027,10 @@
<input type="range" min="1" max="100" class="custom-range custom-range-cyan flex-1" <input type="range" min="1" max="100" class="custom-range custom-range-cyan flex-1"
:value="serverConfig.maxAccounts || 10" :value="serverConfig.maxAccounts || 10"
:style="`background-size: ${((serverConfig.maxAccounts || 10) - 1) / 99 * 100}% 100%`" :style="`background-size: ${((serverConfig.maxAccounts || 10) - 1) / 99 * 100}% 100%`"
@input="toggleMaxAccounts($event.target.value)" @input="toggleMaxAccounts($event.target.value)" aria-label="Max accounts slider">
aria-label="Max accounts slider">
<input type="number" min="1" max="100" <input type="number" min="1" max="100"
class="input input-xs input-bordered w-16 bg-space-800 border-space-border text-white font-mono text-center" class="input input-xs input-bordered w-16 bg-space-800 border-space-border text-white font-mono text-center"
:value="serverConfig.maxAccounts || 10" :value="serverConfig.maxAccounts || 10" @change="toggleMaxAccounts($event.target.value)"
@change="toggleMaxAccounts($event.target.value)"
aria-label="Max accounts value"> aria-label="Max accounts value">
</div> </div>
<span class="text-[11px] text-gray-500 mt-1">Maximum number of Google accounts allowed</span> <span class="text-[11px] text-gray-500 mt-1">Maximum number of Google accounts allowed</span>
@@ -971,17 +1050,19 @@
<div class="flex flex-col gap-1 flex-1"> <div class="flex flex-col gap-1 flex-1">
<span class="text-sm font-medium text-gray-200" <span class="text-sm font-medium text-gray-200"
x-text="$store.global.t('selectionStrategy')">Selection Strategy</span> x-text="$store.global.t('selectionStrategy')">Selection Strategy</span>
<span class="text-[11px] text-gray-500" <span class="text-[11px] text-gray-500" x-text="currentStrategyDescription()">How
x-text="currentStrategyDescription()">How accounts are selected for requests</span> accounts are selected for requests</span>
</div> </div>
<select <select
class="select bg-space-800 border-space-border text-gray-200 focus:border-neon-cyan focus:ring-neon-cyan/20 w-64" class="select bg-space-800 border-space-border text-gray-200 focus:border-neon-cyan focus:ring-neon-cyan/20 w-64"
:value="serverConfig.accountSelection?.strategy || 'hybrid'" :value="serverConfig.accountSelection?.strategy || 'hybrid'"
@change="toggleStrategy($el.value)" @change="toggleStrategy($el.value)" aria-label="Account selection strategy">
aria-label="Account selection strategy"> <option value="hybrid" x-text="$store.global.t('strategyHybridLabel')">Hybrid (Smart
<option value="hybrid" x-text="$store.global.t('strategyHybridLabel')">Hybrid (Smart Distribution)</option> Distribution)</option>
<option value="sticky" x-text="$store.global.t('strategyStickyLabel')">Sticky (Cache Optimized)</option> <option value="sticky" x-text="$store.global.t('strategyStickyLabel')">Sticky (Cache
<option value="round-robin" x-text="$store.global.t('strategyRoundRobinLabel')">Round Robin (Load Balanced)</option> Optimized)</option>
<option value="round-robin" x-text="$store.global.t('strategyRoundRobinLabel')">Round
Robin (Load Balanced)</option>
</select> </select>
</div> </div>
</div> </div>
@@ -1035,13 +1116,11 @@
<input type="range" min="1" max="20" class="custom-range custom-range-purple flex-1" <input type="range" min="1" max="20" class="custom-range custom-range-purple flex-1"
:value="serverConfig.maxRetries || 5" :value="serverConfig.maxRetries || 5"
:style="`background-size: ${((serverConfig.maxRetries || 5) - 1) / 19 * 100}% 100%`" :style="`background-size: ${((serverConfig.maxRetries || 5) - 1) / 19 * 100}% 100%`"
@input="toggleMaxRetries($event.target.value)" @input="toggleMaxRetries($event.target.value)" aria-label="Max retries slider">
aria-label="Max retries slider">
<input type="number" min="1" max="20" <input type="number" min="1" max="20"
class="input input-xs input-bordered w-16 bg-space-800 border-space-border text-white font-mono text-center" class="input input-xs input-bordered w-16 bg-space-800 border-space-border text-white font-mono text-center"
:value="serverConfig.maxRetries || 5" :value="serverConfig.maxRetries || 5"
@change="toggleMaxRetries($event.target.value)" @change="toggleMaxRetries($event.target.value)" aria-label="Max retries value">
aria-label="Max retries value">
</div> </div>
</div> </div>
@@ -1119,7 +1198,8 @@
aria-label="Default cooldown value"> aria-label="Default cooldown value">
</div> </div>
<p class="text-[9px] text-gray-600 mt-1 leading-tight" <p class="text-[9px] text-gray-600 mt-1 leading-tight"
x-text="$store.global.t('defaultCooldownDesc')">Fallback cooldown when API doesn't provide a reset time.</p> x-text="$store.global.t('defaultCooldownDesc')">Fallback cooldown when API doesn't
provide a reset time.</p>
</div> </div>
<div class="form-control"> <div class="form-control">
@@ -1143,7 +1223,8 @@
aria-label="Max wait before error value"> aria-label="Max wait before error value">
</div> </div>
<p class="text-[9px] text-gray-600 mt-1 leading-tight" <p class="text-[9px] text-gray-600 mt-1 leading-tight"
x-text="$store.global.t('maxWaitDesc')">If all accounts are rate-limited longer than this, error immediately.</p> x-text="$store.global.t('maxWaitDesc')">If all accounts are rate-limited longer than
this, error immediately.</p>
</div> </div>
</div> </div>
@@ -1175,13 +1256,15 @@
aria-label="Rate limit dedup window value"> aria-label="Rate limit dedup window value">
</div> </div>
<p class="text-[9px] text-gray-600 mt-1 leading-tight" <p class="text-[9px] text-gray-600 mt-1 leading-tight"
x-text="$store.global.t('rateLimitDedupWindowDesc')">Prevents concurrent retry storms.</p> x-text="$store.global.t('rateLimitDedupWindowDesc')">Prevents concurrent retry
storms.</p>
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label pt-0"> <label class="label pt-0">
<span class="label-text text-gray-400 text-xs" <span class="label-text text-gray-400 text-xs"
x-text="$store.global.t('maxConsecutiveFailures')">Max Consecutive Failures</span> x-text="$store.global.t('maxConsecutiveFailures')">Max Consecutive
Failures</span>
<span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold" <span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
x-text="serverConfig.maxConsecutiveFailures || 3"></span> x-text="serverConfig.maxConsecutiveFailures || 3"></span>
</label> </label>
@@ -1199,7 +1282,8 @@
aria-label="Max consecutive failures value"> aria-label="Max consecutive failures value">
</div> </div>
<p class="text-[9px] text-gray-600 mt-1 leading-tight" <p class="text-[9px] text-gray-600 mt-1 leading-tight"
x-text="$store.global.t('maxConsecutiveFailuresDesc')">Failures before extended cooldown.</p> x-text="$store.global.t('maxConsecutiveFailuresDesc')">Failures before extended
cooldown.</p>
</div> </div>
<div class="form-control"> <div class="form-control">
@@ -1223,7 +1307,8 @@
aria-label="Extended cooldown value"> aria-label="Extended cooldown value">
</div> </div>
<p class="text-[9px] text-gray-600 mt-1 leading-tight" <p class="text-[9px] text-gray-600 mt-1 leading-tight"
x-text="$store.global.t('extendedCooldownDesc')">Applied after max consecutive failures.</p> x-text="$store.global.t('extendedCooldownDesc')">Applied after max consecutive
failures.</p>
</div> </div>
<div class="form-control"> <div class="form-control">
@@ -1247,7 +1332,8 @@
aria-label="Max capacity retries value"> aria-label="Max capacity retries value">
</div> </div>
<p class="text-[9px] text-gray-600 mt-1 leading-tight" <p class="text-[9px] text-gray-600 mt-1 leading-tight"
x-text="$store.global.t('maxCapacityRetriesDesc')">Retries before switching accounts.</p> x-text="$store.global.t('maxCapacityRetriesDesc')">Retries before switching
accounts.</p>
</div> </div>
</div> </div>
</div> </div>
@@ -1282,8 +1368,7 @@
</label> </label>
<input type="password" x-model="passwordDialog.oldPassword" <input type="password" x-model="passwordDialog.oldPassword"
class="input input-sm input-bordered bg-space-800 border-space-border text-white w-full" class="input input-sm input-bordered bg-space-800 border-space-border text-white w-full"
:placeholder="$store.global.t('passwordEmptyDesc')" :placeholder="$store.global.t('passwordEmptyDesc')" aria-label="Current password">
aria-label="Current password">
</div> </div>
<div class="form-control"> <div class="form-control">
@@ -1293,8 +1378,7 @@
</label> </label>
<input type="password" x-model="passwordDialog.newPassword" <input type="password" x-model="passwordDialog.newPassword"
class="input input-sm input-bordered bg-space-800 border-space-border text-white w-full" class="input input-sm input-bordered bg-space-800 border-space-border text-white w-full"
:placeholder="$store.global.t('passwordLengthDesc')" :placeholder="$store.global.t('passwordLengthDesc')" aria-label="New password">
aria-label="New password">
</div> </div>
<div class="form-control"> <div class="form-control">
@@ -1305,8 +1389,7 @@
<input type="password" x-model="passwordDialog.confirmPassword" <input type="password" x-model="passwordDialog.confirmPassword"
class="input input-sm input-bordered bg-space-800 border-space-border text-white w-full" class="input input-sm input-bordered bg-space-800 border-space-border text-white w-full"
:placeholder="$store.global.t('passwordConfirmDesc')" :placeholder="$store.global.t('passwordConfirmDesc')"
@keydown.enter="changePassword()" @keydown.enter="changePassword()" aria-label="Confirm new password">
aria-label="Confirm new password">
</div> </div>
</div> </div>

View File

@@ -17,7 +17,7 @@ import { readFileSync } from 'fs';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import express from 'express'; import express from 'express';
import { getPublicConfig, saveConfig, config } from '../config.js'; import { getPublicConfig, saveConfig, config } from '../config.js';
import { DEFAULT_PORT, ACCOUNT_CONFIG_PATH, MAX_ACCOUNTS } from '../constants.js'; import { DEFAULT_PORT, ACCOUNT_CONFIG_PATH, MAX_ACCOUNTS, DEFAULT_PRESETS } from '../constants.js';
import { readClaudeConfig, updateClaudeConfig, replaceClaudeConfig, getClaudeConfigPath, readPresets, savePreset, deletePreset } from '../utils/claude-config.js'; import { readClaudeConfig, updateClaudeConfig, replaceClaudeConfig, getClaudeConfigPath, readPresets, savePreset, deletePreset } from '../utils/claude-config.js';
import { logger } from '../utils/logger.js'; import { logger } from '../utils/logger.js';
import { getAuthorizationUrl, completeOAuthFlow, startCallbackServer } from '../auth/oauth.js'; import { getAuthorizationUrl, completeOAuthFlow, startCallbackServer } from '../auth/oauth.js';
@@ -624,10 +624,85 @@ export function mountWebUI(app, dirname, accountManager) {
} }
}); });
// ==========================================
// Claude CLI Mode Toggle API (Proxy/Paid)
// ==========================================
/**
* GET /api/claude/mode - Get current mode (proxy or paid)
* Returns 'proxy' if ANTHROPIC_BASE_URL is set to localhost, 'paid' otherwise
*/
app.get('/api/claude/mode', async (req, res) => {
try {
const claudeConfig = await readClaudeConfig();
const baseUrl = claudeConfig.env?.ANTHROPIC_BASE_URL || '';
// Determine mode based on ANTHROPIC_BASE_URL
const isProxy = baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1');
res.json({
status: 'ok',
mode: isProxy ? 'proxy' : 'paid'
});
} catch (error) {
res.status(500).json({ status: 'error', error: error.message });
}
});
/**
* POST /api/claude/mode - Switch between proxy and paid mode
* Body: { mode: 'proxy' | 'paid' }
*
* When switching to 'paid' mode:
* - Removes the entire 'env' object from settings.json
* - Claude CLI uses its built-in defaults (official Anthropic API)
*
* When switching to 'proxy' mode:
* - Sets 'env' to the first default preset config (from constants.js)
*/
app.post('/api/claude/mode', async (req, res) => {
try {
const { mode } = req.body;
if (!mode || !['proxy', 'paid'].includes(mode)) {
return res.status(400).json({
status: 'error',
error: 'mode must be "proxy" or "paid"'
});
}
const claudeConfig = await readClaudeConfig();
if (mode === 'proxy') {
// Switch to proxy mode - use first default preset config (e.g., "Claude Thinking")
claudeConfig.env = { ...DEFAULT_PRESETS[0].config };
} else {
// Switch to paid mode - remove env entirely
delete claudeConfig.env;
}
// Save the updated config
const newConfig = await replaceClaudeConfig(claudeConfig);
logger.info(`[WebUI] Switched Claude CLI to ${mode} mode`);
res.json({
status: 'ok',
mode,
config: newConfig,
message: `Switched to ${mode === 'proxy' ? 'Proxy' : 'Paid (Anthropic API)'} mode. Restart Claude CLI to apply.`
});
} catch (error) {
logger.error('[WebUI] Error switching mode:', error);
res.status(500).json({ status: 'error', error: error.message });
}
});
// ========================================== // ==========================================
// Claude CLI Presets API // Claude CLI Presets API
// ========================================== // ==========================================
/** /**
* GET /api/claude/presets - Get all saved presets * GET /api/claude/presets - Get all saved presets
*/ */