Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions apps/server/api/routes/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,13 @@ async def get_settings(request: Request):
settings["mqttEnabled"] = mqtt_env.lower() == "true"
elif "mqttEnabled" not in settings:
settings["mqttEnabled"] = True

# Gemini model (env var takes precedence over stored setting)
gemini_model_env = os.environ.get("GEMINI_MODEL", "").strip()
if gemini_model_env:
settings["geminiModel"] = gemini_model_env
elif "geminiModel" not in settings:
settings["geminiModel"] = "gemini-2.5-flash"

return settings

Expand Down Expand Up @@ -1249,6 +1256,11 @@ async def save_settings_endpoint(request: Request):
for bool_key in ("autoSync", "autoSyncAiDescription"):
if bool_key in body:
current_settings[bool_key] = bool(body[bool_key])

# Gemini model selection
if "geminiModel" in body:
model_value = str(body["geminiModel"]).strip()
current_settings["geminiModel"] = model_value if model_value else "gemini-2.5-flash"

# For IP and API key changes, also update .env file
env_updated = False
Expand Down Expand Up @@ -1425,6 +1437,16 @@ async def save_settings_endpoint(request: Request):
logger.info("MQTT enabled=%s", mqtt_enabled,
extra={"request_id": request_id})

# Hot-reload Gemini model into process environment
if "geminiModel" in body:
new_model = str(body["geminiModel"]).strip()
if new_model:
os.environ["GEMINI_MODEL"] = new_model
_update_s6_env("GEMINI_MODEL", new_model, request_id)
services_restarted.append("gemini_model")
logger.info("Updated GEMINI_MODEL to %s", new_model,
extra={"request_id": request_id})
Comment on lines +1440 to +1448
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New Gemini model selection behavior in GET/POST /api/settings is not covered by existing endpoint tests (there are tests for /api/settings, but none assert geminiModel env precedence or persistence/hot-reload). Please add tests in apps/server/test_main.py for: GET returning env override; POST updating stored settings and os.environ['GEMINI_MODEL'] (and/or s6 env update call).

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 5e51ea9. Added 3 tests: model persistence round-trip, empty-string fallback to default, and geminiModel presence in GET response. All 824 backend tests passing.


return {
"status": "success",
"message": "Settings saved successfully",
Expand Down
1 change: 1 addition & 0 deletions apps/server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ async def lifespan(app: FastAPI):
stored = load_settings()
_ENV_SETTINGS_MAP = {
"geminiApiKey": "GEMINI_API_KEY",
"geminiModel": "GEMINI_MODEL",
"meticulousIp": "METICULOUS_IP",
"authorName": "AUTHOR_NAME",
}
Expand Down
1 change: 1 addition & 0 deletions apps/server/services/settings_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

_DEFAULT_SETTINGS = {
"geminiApiKey": "",
"geminiModel": "gemini-2.5-flash",
"meticulousIp": "",
"serverIp": "",
"authorName": "",
Expand Down
40 changes: 40 additions & 0 deletions apps/server/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7927,6 +7927,46 @@ def test_save_settings_empty_body(self, client):
# Should handle gracefully
assert response.status_code in [200, 400, 500]

def test_save_gemini_model_persists_and_returns(self, client, monkeypatch, tmp_path):
"""POST geminiModel then GET and verify it is returned."""
import services.settings_service as settings_svc

settings_file = tmp_path / "settings.json"
settings_file.write_text("{}")
monkeypatch.setattr(settings_svc, "SETTINGS_FILE", settings_file)
monkeypatch.delenv("GEMINI_MODEL", raising=False)

resp = client.post("/api/settings", json={"geminiModel": "gemini-2.5-pro"})
assert resp.status_code == 200

resp = client.get("/api/settings")
assert resp.status_code == 200
assert resp.json()["geminiModel"] == "gemini-2.5-pro"

def test_save_gemini_model_empty_falls_back_to_default(self, client, monkeypatch, tmp_path):
"""POST empty geminiModel falls back to the default model."""
import services.settings_service as settings_svc

settings_file = tmp_path / "settings.json"
settings_file.write_text("{}")
monkeypatch.setattr(settings_svc, "SETTINGS_FILE", settings_file)
monkeypatch.delenv("GEMINI_MODEL", raising=False)

resp = client.post("/api/settings", json={"geminiModel": " "})
assert resp.status_code == 200

resp = client.get("/api/settings")
assert resp.status_code == 200
assert resp.json()["geminiModel"] == "gemini-2.5-flash"

def test_get_settings_includes_gemini_model(self, client, monkeypatch):
"""GET /api/settings always includes geminiModel."""
monkeypatch.delenv("GEMINI_MODEL", raising=False)

resp = client.get("/api/settings")
assert resp.status_code == 200
assert "geminiModel" in resp.json()


class TestDecompressShotData:
"""Test shot data decompression."""
Expand Down
15 changes: 13 additions & 2 deletions apps/web/public/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@
"authorName": "Autorenname",
"authorNamePlaceholder": "MeticAI",
"authorNameDescription": "Dein Name für das Autorenfeld im generierten Profil-JSON. Standardmäßig \"MeticAI\", wenn leer gelassen.",
"geminiModel": "Gemini-Modell",
"geminiModelDescription": "Wähle das Gemini-Modell für KI-Funktionen aus.",
"mqttEnabled": "MQTT-Brücke",
"mqttEnabledDescription": "Echtzeit-Maschinentelemetrie über MQTT. Betreibt das Live-Dashboard.",
"saveSettings": "Einstellungen speichern",
Expand Down Expand Up @@ -537,7 +539,10 @@
"runsOn": "Runs on",
"andCaffeine": "and caffeine ☕",
"betaChannelFailed": "Fehler beim Wechsel des Betakanals",
"feedbackFailed": "Fehler beim Senden des Feedbacks"
"feedbackFailed": "Fehler beim Senden des Feedbacks",
"geminiModel25Flash": "Gemini 2.5 Flash (Empfohlen)",
"geminiModel25Pro": "Gemini 2.5 Pro",
"geminiModel20Flash": "Gemini 2.0 Flash"
},
"profileBreakdown": {
"title": "Profildetails",
Expand Down Expand Up @@ -860,7 +865,13 @@
"followSystemTheme": "Follow system theme",
"followSystemDescription": "Automatically match your system's dark/light mode preference",
"backgroundAnimations": "Background animations",
"animationsDescription": "Show animated background effects"
"animationsDescription": "Show animated background effects",
"platformTheme": "Plattform-Design",
"platformThemeDescription": "Visuellen Stil an Ihr Gerät anpassen",
"platformThemeAuto": "Automatisch",
"platformThemeIos": "iOS",
"platformThemeMaterial": "Material (Android)",
"platformThemeNone": "Standard"
},
"advanced": {
"detailedKnowledge": "Detailliertes KI-Wissen",
Expand Down
15 changes: 13 additions & 2 deletions apps/web/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@
"authorName": "Author Name",
"authorNamePlaceholder": "MeticAI",
"authorNameDescription": "Your name for the author field in generated profile JSON. Defaults to \"MeticAI\" if left empty.",
"geminiModel": "Gemini Model",
"geminiModelDescription": "Select which Gemini model to use for AI features.",
"mqttEnabled": "MQTT Bridge",
"mqttEnabledDescription": "Real-time machine telemetry via MQTT. Powers the live Control Center dashboard.",
"addToHomeAssistant": "Add to Home Assistant",
Expand Down Expand Up @@ -537,7 +539,10 @@
"version": "Version",
"footer": "MeticAI • Built with ❤️ for coffee lovers",
"feedbackFailed": "Failed to submit feedback",
"betaChannelFailed": "Failed to switch beta channel"
"betaChannelFailed": "Failed to switch beta channel",
"geminiModel25Flash": "Gemini 2.5 Flash (Recommended)",
"geminiModel25Pro": "Gemini 2.5 Pro",
"geminiModel20Flash": "Gemini 2.0 Flash"
},
"profileBreakdown": {
"title": "Profile Details",
Expand Down Expand Up @@ -865,7 +870,13 @@
"followSystemTheme": "Follow system theme",
"followSystemDescription": "Automatically match your system's dark/light mode preference",
"backgroundAnimations": "Background animations",
"animationsDescription": "Show animated background effects"
"animationsDescription": "Show animated background effects",
"platformTheme": "Platform theme",
"platformThemeDescription": "Adjust the visual style to match your device",
"platformThemeAuto": "Auto-detect",
"platformThemeIos": "iOS",
"platformThemeMaterial": "Material (Android)",
"platformThemeNone": "Default"
},
"pourOver": {
"title": "Pour-over",
Expand Down
15 changes: 13 additions & 2 deletions apps/web/public/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@
"authorName": "Nombre del autor",
"authorNamePlaceholder": "MeticAI",
"authorNameDescription": "Tu nombre para el campo de autor en el JSON de perfil generado. Por defecto \"MeticAI\" si se deja vacío.",
"geminiModel": "Modelo Gemini",
"geminiModelDescription": "Selecciona qué modelo Gemini usar para las funciones de IA.",
"mqttEnabled": "Puente MQTT",
"mqttEnabledDescription": "Telemetría en tiempo real de la máquina vía MQTT. Alimenta el panel de control en vivo.",
"saveSettings": "Guardar configuración",
Expand Down Expand Up @@ -537,7 +539,10 @@
"runsOn": "Runs on",
"andCaffeine": "and caffeine ☕",
"betaChannelFailed": "Error al cambiar el canal beta",
"feedbackFailed": "Error al enviar comentarios"
"feedbackFailed": "Error al enviar comentarios",
"geminiModel25Flash": "Gemini 2.5 Flash (Recomendado)",
"geminiModel25Pro": "Gemini 2.5 Pro",
"geminiModel20Flash": "Gemini 2.0 Flash"
},
"profileBreakdown": {
"title": "Detalles del perfil",
Expand Down Expand Up @@ -860,7 +865,13 @@
"followSystemTheme": "Follow system theme",
"followSystemDescription": "Automatically match your system's dark/light mode preference",
"backgroundAnimations": "Background animations",
"animationsDescription": "Show animated background effects"
"animationsDescription": "Show animated background effects",
"platformTheme": "Tema de plataforma",
"platformThemeDescription": "Ajusta el estilo visual según tu dispositivo",
"platformThemeAuto": "Detectar automáticamente",
"platformThemeIos": "iOS",
"platformThemeMaterial": "Material (Android)",
"platformThemeNone": "Predeterminado"
},
"advanced": {
"detailedKnowledge": "Conocimiento detallado de IA",
Expand Down
15 changes: 13 additions & 2 deletions apps/web/public/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@
"authorName": "Nom de l'auteur",
"authorNamePlaceholder": "MeticAI",
"authorNameDescription": "Votre nom pour le champ auteur dans le JSON du profil généré. Par défaut \"MeticAI\" si laissé vide.",
"geminiModel": "Modèle Gemini",
"geminiModelDescription": "Sélectionnez le modèle Gemini à utiliser pour les fonctions IA.",
"mqttEnabled": "Pont MQTT",
"mqttEnabledDescription": "Télémétrie machine en temps réel via MQTT. Alimente le tableau de bord en direct.",
"saveSettings": "Enregistrer les paramètres",
Expand Down Expand Up @@ -537,7 +539,10 @@
"runsOn": "Runs on",
"andCaffeine": "and caffeine ☕",
"betaChannelFailed": "Échec du changement de canal bêta",
"feedbackFailed": "Échec de l'envoi du commentaire"
"feedbackFailed": "Échec de l'envoi du commentaire",
"geminiModel25Flash": "Gemini 2.5 Flash (Recommandé)",
"geminiModel25Pro": "Gemini 2.5 Pro",
"geminiModel20Flash": "Gemini 2.0 Flash"
},
"profileBreakdown": {
"title": "Détails du profil",
Expand Down Expand Up @@ -860,7 +865,13 @@
"followSystemTheme": "Follow system theme",
"followSystemDescription": "Automatically match your system's dark/light mode preference",
"backgroundAnimations": "Background animations",
"animationsDescription": "Show animated background effects"
"animationsDescription": "Show animated background effects",
"platformTheme": "Thème de plateforme",
"platformThemeDescription": "Adapter le style visuel à votre appareil",
"platformThemeAuto": "Détection automatique",
"platformThemeIos": "iOS",
"platformThemeMaterial": "Material (Android)",
"platformThemeNone": "Par défaut"
},
"advanced": {
"detailedKnowledge": "Connaissances IA détaillées",
Expand Down
15 changes: 13 additions & 2 deletions apps/web/public/locales/it/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@
"authorName": "Nome autore",
"authorNamePlaceholder": "MeticAI",
"authorNameDescription": "Il tuo nome per il campo autore nel JSON del profilo generato. Predefinito \"MeticAI\" se lasciato vuoto.",
"geminiModel": "Modello Gemini",
"geminiModelDescription": "Seleziona quale modello Gemini utilizzare per le funzioni IA.",
"mqttEnabled": "Bridge MQTT",
"mqttEnabledDescription": "Telemetria macchina in tempo reale via MQTT. Alimenta la dashboard di controllo dal vivo.",
"saveSettings": "Salva impostazioni",
Expand Down Expand Up @@ -537,7 +539,10 @@
"runsOn": "Runs on",
"andCaffeine": "and caffeine ☕",
"betaChannelFailed": "Impossibile cambiare il canale beta",
"feedbackFailed": "Impossibile inviare il feedback"
"feedbackFailed": "Impossibile inviare il feedback",
"geminiModel25Flash": "Gemini 2.5 Flash (Consigliato)",
"geminiModel25Pro": "Gemini 2.5 Pro",
"geminiModel20Flash": "Gemini 2.0 Flash"
},
"profileBreakdown": {
"title": "Dettagli profilo",
Expand Down Expand Up @@ -860,7 +865,13 @@
"followSystemTheme": "Follow system theme",
"followSystemDescription": "Automatically match your system's dark/light mode preference",
"backgroundAnimations": "Background animations",
"animationsDescription": "Show animated background effects"
"animationsDescription": "Show animated background effects",
"platformTheme": "Tema piattaforma",
"platformThemeDescription": "Adatta lo stile visivo al tuo dispositivo",
"platformThemeAuto": "Rilevamento automatico",
"platformThemeIos": "iOS",
"platformThemeMaterial": "Material (Android)",
"platformThemeNone": "Predefinito"
},
"advanced": {
"detailedKnowledge": "Conoscenza IA dettagliata",
Expand Down
15 changes: 13 additions & 2 deletions apps/web/public/locales/sv/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@
"authorName": "Författarnamn",
"authorNamePlaceholder": "MeticAI",
"authorNameDescription": "Ditt namn för författarfältet i genererad profil-JSON. Standardvärde \"MeticAI\" om det lämnas tomt.",
"geminiModel": "Gemini-modell",
"geminiModelDescription": "Välj vilken Gemini-modell som ska användas för AI-funktioner.",
"mqttEnabled": "MQTT-brygga",
"mqttEnabledDescription": "Realtidstelemetri från maskinen via MQTT. Driver den direkta kontrollpanelen.",
"saveSettings": "Spara inställningar",
Expand Down Expand Up @@ -537,7 +539,10 @@
"runsOn": "Runs on",
"andCaffeine": "and caffeine ☕",
"betaChannelFailed": "Kunde inte byta betakanal",
"feedbackFailed": "Kunde inte skicka feedback"
"feedbackFailed": "Kunde inte skicka feedback",
"geminiModel25Flash": "Gemini 2.5 Flash (Rekommenderad)",
"geminiModel25Pro": "Gemini 2.5 Pro",
"geminiModel20Flash": "Gemini 2.0 Flash"
},
"profileBreakdown": {
"title": "Profildetaljer",
Expand Down Expand Up @@ -865,7 +870,13 @@
"followSystemTheme": "Följ systemtema",
"followSystemDescription": "Matcha automatiskt ditt systems mörkt-/ljust-läge",
"backgroundAnimations": "Bakgrundsanimationer",
"animationsDescription": "Visa animerade bakgrundseffekter"
"animationsDescription": "Visa animerade bakgrundseffekter",
"platformTheme": "Plattformstema",
"platformThemeDescription": "Anpassa utseendet efter din enhet",
"platformThemeAuto": "Automatisk",
"platformThemeIos": "iOS",
"platformThemeMaterial": "Material (Android)",
"platformThemeNone": "Standard"
},
"pourOver": {
"title": "Pour Over",
Expand Down
28 changes: 28 additions & 0 deletions apps/web/src/components/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ interface Settings {
geminiApiKey: string
meticulousIp: string
authorName: string
geminiModel?: string
mqttEnabled?: boolean
geminiApiKeyMasked?: boolean
geminiApiKeyConfigured?: boolean
Expand Down Expand Up @@ -105,6 +106,7 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo
geminiApiKey: '',
meticulousIp: '',
authorName: '',
geminiModel: 'gemini-2.5-flash',
mqttEnabled: true
})
const [isSaving, setIsSaving] = useState(false)
Expand Down Expand Up @@ -201,6 +203,7 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo
geminiApiKey: localStorage.getItem(STORAGE_KEYS.GEMINI_API_KEY) || '',
meticulousIp: window.location.hostname,
authorName: localStorage.getItem(STORAGE_KEYS.AUTHOR_NAME) || '',
geminiModel: localStorage.getItem(STORAGE_KEYS.GEMINI_MODEL) || 'gemini-2.5-flash',
mqttEnabled: true,
geminiApiKeyMasked: false,
geminiApiKeyConfigured: Boolean(localStorage.getItem(STORAGE_KEYS.GEMINI_API_KEY)?.trim()),
Expand All @@ -217,6 +220,7 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo
geminiApiKey: data.geminiApiKey || '',
meticulousIp: data.meticulousIp || '',
authorName: data.authorName || '',
geminiModel: data.geminiModel || 'gemini-2.5-flash',
mqttEnabled: data.mqttEnabled !== false,
geminiApiKeyMasked: data.geminiApiKeyMasked || false,
geminiApiKeyConfigured: data.geminiApiKeyConfigured || false
Expand Down Expand Up @@ -362,6 +366,9 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo
if (settings.authorName) {
localStorage.setItem(STORAGE_KEYS.AUTHOR_NAME, settings.authorName)
}
if (settings.geminiModel) {
localStorage.setItem(STORAGE_KEYS.GEMINI_MODEL, settings.geminiModel)
}
setSaveStatus('success')
setTimeout(() => setSaveStatus('idle'), 3000)
return
Expand All @@ -374,6 +381,7 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo
authorName: settings.authorName,
meticulousIp: settings.meticulousIp,
mqttEnabled: settings.mqttEnabled,
geminiModel: settings.geminiModel,
}

// Only send API key if user actually typed a new value (not the masked stars)
Expand Down Expand Up @@ -966,6 +974,26 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo
</p>
</div>

{/* Gemini Model */}
<div className="space-y-2">
<Label htmlFor="geminiModel" className="text-sm font-medium">
{t('settings.geminiModel')}
</Label>
<select
id="geminiModel"
value={settings.geminiModel || 'gemini-2.5-flash'}
onChange={(e) => handleChange('geminiModel', e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<option value="gemini-2.5-flash">{t('settings.geminiModel25Flash')}</option>
<option value="gemini-2.5-pro">{t('settings.geminiModel25Pro')}</option>
<option value="gemini-2.0-flash">{t('settings.geminiModel20Flash')}</option>
</select>
<p className="text-xs text-muted-foreground">
{t('settings.geminiModelDescription')}
</p>
</div>

{/* MQTT Bridge */}
{hasFeature('bridgeStatus') && <div className="space-y-3 pt-2 border-t border-border">
<div className="flex items-center gap-2">
Expand Down
Loading
Loading