personal memory agent

Merge branch 'hopper-w6zmhkql-api-key-validation'

+524 -7
+71 -1
apps/settings/routes.py
··· 8 8 import os 9 9 import re 10 10 import subprocess 11 + from datetime import datetime, timezone 11 12 from pathlib import Path 12 13 from typing import Any 13 14 ··· 198 199 mode = "api_key" if new_val else "platform" 199 200 config["providers"]["auth"][provider] = mode 200 201 202 + # Validate changed provider API keys 203 + if "key_validation" not in config["providers"]: 204 + config["providers"]["key_validation"] = {} 205 + for env_var in changed_fields: 206 + provider = env_to_provider.get(env_var) 207 + if provider: 208 + new_val = data.get(env_var, "") 209 + if new_val: 210 + from think.providers import validate_key as _validate_key 211 + 212 + result = _validate_key(provider, new_val) 213 + result["timestamp"] = datetime.now(timezone.utc).isoformat() 214 + config["providers"]["key_validation"][provider] = result 215 + else: 216 + config["providers"]["key_validation"].pop(provider, None) 217 + 201 218 # Write back to file 202 219 with open(config_path, "w", encoding="utf-8") as f: 203 220 json.dump(config, f, indent=2, ensure_ascii=False) ··· 234 251 if "env" in config: 235 252 config["env"] = {k: bool(v) for k, v in config["env"].items()} 236 253 237 - return jsonify({"success": True, "config": config}) 254 + key_validation = config.get("providers", {}).get("key_validation", {}) 255 + return jsonify( 256 + {"success": True, "config": config, "key_validation": key_validation} 257 + ) 238 258 except Exception as e: 239 259 return jsonify({"error": str(e)}), 500 240 260 ··· 377 397 p["name"]: auth_config.get(p["name"], "platform") for p in providers_list 378 398 } 379 399 400 + # Get cached key validation results 401 + key_validation = providers_config.get("key_validation", {}) 402 + 380 403 return jsonify( 381 404 { 382 405 "providers": providers_list, ··· 386 409 "context_defaults": context_defaults, 387 410 "api_keys": api_keys, 388 411 "auth": auth, 412 + "key_validation": key_validation, 389 413 } 390 414 ) 415 + except Exception as e: 416 + return jsonify({"error": str(e)}), 500 417 + 418 + 419 + @settings_bp.route("/api/validate-keys", methods=["POST"]) 420 + def validate_all_keys() -> Any: 421 + """Re-validate all configured provider API keys. 422 + 423 + Reads keys from journal.json config (not environment), validates each 424 + against the provider API, and stores results in providers.key_validation. 425 + """ 426 + try: 427 + from think.providers import PROVIDER_METADATA, validate_key as _validate_key 428 + 429 + config = get_journal_config() 430 + env_config = config.get("env", {}) 431 + 432 + # Build reverse map: env_key -> provider name 433 + env_to_provider = { 434 + meta["env_key"]: name 435 + for name, meta in PROVIDER_METADATA.items() 436 + if "env_key" in meta 437 + } 438 + 439 + if "providers" not in config: 440 + config["providers"] = {} 441 + key_validation = {} 442 + 443 + for env_var, provider in env_to_provider.items(): 444 + api_key = env_config.get(env_var, "") 445 + if api_key: 446 + result = _validate_key(provider, api_key) 447 + result["timestamp"] = datetime.now(timezone.utc).isoformat() 448 + key_validation[provider] = result 449 + 450 + config["providers"]["key_validation"] = key_validation 451 + 452 + config_dir = Path(state.journal_root) / "config" 453 + config_dir.mkdir(parents=True, exist_ok=True) 454 + config_path = config_dir / "journal.json" 455 + with open(config_path, "w", encoding="utf-8") as f: 456 + json.dump(config, f, indent=2, ensure_ascii=False) 457 + f.write("\n") 458 + os.chmod(config_path, 0o600) 459 + 460 + return jsonify({"success": True, "key_validation": key_validation}) 391 461 except Exception as e: 392 462 return jsonify({"error": str(e)}), 500 393 463
+160 -6
apps/settings/workspace.html
··· 194 194 font-size: 0.85em; 195 195 } 196 196 197 + .key-status-valid { 198 + color: #28a745; 199 + font-size: 0.85em; 200 + margin-left: 0.5em; 201 + } 202 + 203 + .key-status-invalid { 204 + color: #dc3545; 205 + font-size: 0.85em; 206 + margin-left: 0.5em; 207 + } 208 + 209 + .key-status-validating { 210 + color: #6c757d; 211 + font-size: 0.85em; 212 + margin-left: 0.5em; 213 + font-style: italic; 214 + } 215 + 197 216 /* Password field with toggle */ 198 217 .password-wrap { 199 218 position: relative; ··· 1836 1855 <section class="settings-section" id="section-apikeys"> 1837 1856 <h2>API Keys</h2> 1838 1857 <p class="settings-section-desc">Configure API keys for AI providers. Keys are stored in your journal config.</p> 1858 + <div style="margin-bottom: 1em;"> 1859 + <button type="button" id="revalidateAllKeys" class="btn-secondary" onclick="revalidateAllKeys()">Re-validate All Keys</button> 1860 + <span id="revalidateStatus" style="margin-left: 0.5em; font-size: 0.85em;"></span> 1861 + </div> 1839 1862 1840 1863 <div class="settings-field"> 1841 1864 <label for="field-env-google">Google AI (Gemini)</label> ··· 2516 2539 let saveTimeout = null; 2517 2540 let logNextCursor = null; 2518 2541 let insightsData = null; 2542 + let keyValidationData = {}; 2519 2543 2520 2544 // ========== NAVIGATION ========== 2521 2545 const VALID_SECTIONS = ['profile', 'agent', 'providers', 'apikeys', 'transcription', 'observer', 'vision', 'insights', 'security', 'sync', 'storage', 'support', 'facet-appearance', 'facet-activities', 'facet-activity']; ··· 2717 2741 // env = journal config status, runtime_env = loaded into process 2718 2742 const env = config.env || {}; 2719 2743 const sysEnv = config.runtime_env || {}; 2744 + keyValidationData = config.providers?.key_validation || {}; 2720 2745 updateEnvStatus('field-env-google', env.GOOGLE_API_KEY, sysEnv.GOOGLE_API_KEY); 2721 2746 updateEnvStatus('field-env-openai', env.OPENAI_API_KEY, sysEnv.OPENAI_API_KEY); 2722 2747 updateEnvStatus('field-env-anthropic', env.ANTHROPIC_API_KEY, sysEnv.ANTHROPIC_API_KEY); ··· 2755 2780 status.textContent = ' (system)'; 2756 2781 label.appendChild(status); 2757 2782 } 2783 + 2784 + // Add validation status for provider keys 2785 + const providerName = fieldId.replace('field-env-', ''); 2786 + if (['google', 'openai', 'anthropic'].includes(providerName)) { 2787 + const validation = keyValidationData[providerName]; 2788 + const oldValidation = label.querySelector('.key-validation-status'); 2789 + if (oldValidation) oldValidation.remove(); 2790 + 2791 + if (validation) { 2792 + const validationSpan = document.createElement('span'); 2793 + validationSpan.className = 'key-validation-status'; 2794 + if (validation.valid) { 2795 + validationSpan.className += ' key-status-valid'; 2796 + validationSpan.textContent = ' ✓ Valid'; 2797 + } else { 2798 + validationSpan.className += ' key-status-invalid'; 2799 + const errMsg = validation.error || 'Invalid'; 2800 + const shortErr = errMsg.length > 60 ? errMsg.slice(0, 60) + '…' : errMsg; 2801 + validationSpan.textContent = ' ✗ ' + shortErr; 2802 + validationSpan.title = errMsg; 2803 + } 2804 + label.appendChild(validationSpan); 2805 + } 2806 + } 2758 2807 } 2759 2808 } 2760 2809 ··· 2806 2855 clearTimeout(saveTimeout); 2807 2856 saveTimeout = setTimeout(async () => { 2808 2857 try { 2858 + const runtimeEnv = configData?.runtime_env || {}; 2809 2859 const response = await fetch('api/config', { 2810 2860 method: 'PUT', 2811 2861 headers: { 'Content-Type': 'application/json' }, ··· 2814 2864 const result = await response.json(); 2815 2865 if (result.success) { 2816 2866 configData = result.config; 2867 + configData.runtime_env = runtimeEnv; 2817 2868 if (el) showFieldStatus(el, 'saved'); 2818 2869 2819 2870 // For env fields, clear the input and update status indicator 2820 2871 if (el && section === 'env') { 2821 2872 el.value = ''; 2822 - updateEnvStatus(el.id, true, true); 2873 + if (result.key_validation) { 2874 + keyValidationData = result.key_validation; 2875 + configData.providers = configData.providers || {}; 2876 + configData.providers.key_validation = keyValidationData; 2877 + } 2878 + updateEnvStatus( 2879 + el.id, 2880 + configData?.env?.[key], 2881 + configData?.runtime_env?.[key] 2882 + ); 2883 + if (providersData) { 2884 + providersData.key_validation = keyValidationData; 2885 + for (const type of ['generate', 'cogitate']) { 2886 + const providerSelect = document.getElementById(`field-${type}-provider`); 2887 + if (providerSelect) { 2888 + updateTypeProviderKeyWarning( 2889 + type, 2890 + providerSelect.value, 2891 + providersData.api_keys || {}, 2892 + providersData.auth || {} 2893 + ); 2894 + } 2895 + } 2896 + } 2823 2897 } 2824 2898 } else { 2825 2899 throw new Error(result.error); ··· 3254 3328 } 3255 3329 3256 3330 function populateProviders(data) { 3331 + // Cache key validation data 3332 + if (data.key_validation) { 3333 + keyValidationData = data.key_validation; 3334 + } 3335 + 3257 3336 // Populate all provider/backup dropdowns for both types 3258 3337 for (const type of ['generate', 'cogitate']) { 3259 3338 for (const suffix of ['provider', 'backup']) { ··· 3289 3368 // Update status text 3290 3369 document.getElementById('providerStatus').textContent = ''; 3291 3370 3371 + // Refresh env status displays with latest validation data 3372 + const env = configData?.env || {}; 3373 + const sysEnv = configData?.runtime_env || {}; 3374 + updateEnvStatus('field-env-google', env.GOOGLE_API_KEY, sysEnv.GOOGLE_API_KEY); 3375 + updateEnvStatus('field-env-openai', env.OPENAI_API_KEY, sysEnv.OPENAI_API_KEY); 3376 + updateEnvStatus('field-env-anthropic', env.ANTHROPIC_API_KEY, sysEnv.ANTHROPIC_API_KEY); 3377 + 3292 3378 // Populate context overrides 3293 3379 renderContextGroups(data); 3294 3380 } ··· 3297 3383 const warning = document.getElementById(`${type}ProviderKeyWarning`); 3298 3384 const link = document.getElementById(`${type}ProviderKeyLink`); 3299 3385 if (!warning || !link) return; 3386 + const message = warning.querySelectorAll('span')[1]; 3300 3387 3301 3388 // For cogitate, hide key warning when using platform auth 3302 3389 const authMode = (auth && type === 'cogitate') ? (auth[provider] || 'platform') : 'api_key'; 3303 - if (authMode === 'platform' || !provider || apiKeys[provider]) { 3390 + const validation = provider ? keyValidationData[provider] : null; 3391 + if (authMode === 'platform' || !provider) { 3392 + warning.style.display = 'none'; 3393 + warning.title = ''; 3394 + return; 3395 + } 3396 + 3397 + if (validation) { 3398 + if (validation.valid) { 3399 + warning.style.display = 'none'; 3400 + warning.title = ''; 3401 + return; 3402 + } 3403 + warning.style.display = 'flex'; 3404 + if (message) { 3405 + message.textContent = `API key validation failed for ${type} provider.`; 3406 + } 3407 + warning.title = validation.error || 'Invalid API key'; 3408 + } else if (apiKeys[provider]) { 3304 3409 warning.style.display = 'none'; 3410 + warning.title = ''; 3411 + return; 3305 3412 } else { 3306 3413 warning.style.display = 'flex'; 3307 - link.onclick = (e) => { 3308 - e.preventDefault(); 3309 - switchSection('apikeys'); 3310 - }; 3414 + if (message) { 3415 + message.textContent = `API key not configured for ${type} provider.`; 3416 + } 3417 + warning.title = ''; 3418 + } 3419 + 3420 + link.onclick = (e) => { 3421 + e.preventDefault(); 3422 + switchSection('apikeys'); 3423 + }; 3424 + } 3425 + 3426 + async function revalidateAllKeys() { 3427 + const btn = document.getElementById('revalidateAllKeys'); 3428 + const status = document.getElementById('revalidateStatus'); 3429 + btn.disabled = true; 3430 + status.textContent = 'Validating...'; 3431 + status.className = 'key-status-validating'; 3432 + try { 3433 + const response = await fetch('api/validate-keys', { method: 'POST' }); 3434 + const result = await response.json(); 3435 + if (result.error) throw new Error(result.error); 3436 + keyValidationData = result.key_validation || {}; 3437 + if (configData) { 3438 + configData.providers = configData.providers || {}; 3439 + configData.providers.key_validation = keyValidationData; 3440 + } 3441 + // Refresh env status displays 3442 + const env = configData?.env || {}; 3443 + const sysEnv = configData?.runtime_env || {}; 3444 + updateEnvStatus('field-env-google', env.GOOGLE_API_KEY, sysEnv.GOOGLE_API_KEY); 3445 + updateEnvStatus('field-env-openai', env.OPENAI_API_KEY, sysEnv.OPENAI_API_KEY); 3446 + updateEnvStatus('field-env-anthropic', env.ANTHROPIC_API_KEY, sysEnv.ANTHROPIC_API_KEY); 3447 + // Update provider warnings too 3448 + if (providersData) { 3449 + providersData.key_validation = keyValidationData; 3450 + for (const type of ['generate', 'cogitate']) { 3451 + const providerSelect = document.getElementById(`field-${type}-provider`); 3452 + if (providerSelect) { 3453 + updateTypeProviderKeyWarning(type, providerSelect.value, providersData.api_keys, providersData.auth); 3454 + } 3455 + } 3456 + } 3457 + status.textContent = 'Done'; 3458 + status.className = 'key-status-valid'; 3459 + setTimeout(() => { status.textContent = ''; }, 2000); 3460 + } catch (err) { 3461 + status.textContent = 'Error: ' + err.message; 3462 + status.className = 'key-status-invalid'; 3463 + } finally { 3464 + btn.disabled = false; 3311 3465 } 3312 3466 } 3313 3467
+1
tests/baselines/api/settings/providers.json
··· 451 451 "provider": "google", 452 452 "tier": 2 453 453 }, 454 + "key_validation": {}, 454 455 "providers": [ 455 456 { 456 457 "env_key": "ANTHROPIC_API_KEY",
+209
tests/test_validate_key.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from __future__ import annotations 5 + 6 + import json 7 + import shutil 8 + from pathlib import Path 9 + from unittest.mock import Mock, patch 10 + 11 + import pytest 12 + 13 + import think.providers 14 + import think.providers.anthropic 15 + import think.providers.google 16 + import think.providers.openai 17 + from convey import create_app 18 + from think.providers import validate_key 19 + 20 + 21 + @pytest.fixture 22 + def settings_client(tmp_path, monkeypatch): 23 + src = Path(__file__).resolve().parent / "fixtures" / "journal" 24 + journal = tmp_path / "journal" 25 + shutil.copytree(src, journal) 26 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 27 + 28 + app = create_app(str(journal)) 29 + app.config["TESTING"] = True 30 + return app.test_client(), journal 31 + 32 + 33 + def test_validate_key_anthropic_success(): 34 + client = Mock() 35 + client.models.list.return_value = [Mock()] 36 + 37 + with patch("anthropic.Anthropic", return_value=client) as mock_cls: 38 + result = think.providers.anthropic.validate_key("test-key") 39 + 40 + assert result == {"valid": True} 41 + mock_cls.assert_called_once_with(api_key="test-key", timeout=10) 42 + 43 + 44 + def test_validate_key_anthropic_auth_error(): 45 + client = Mock() 46 + client.models.list.side_effect = Exception("invalid x-api-key") 47 + 48 + with patch("anthropic.Anthropic", return_value=client): 49 + result = think.providers.anthropic.validate_key("bad-key") 50 + 51 + assert result["valid"] is False 52 + assert "invalid x-api-key" in result["error"] 53 + 54 + 55 + def test_validate_key_openai_success(): 56 + client = Mock() 57 + client.models.list.return_value = [Mock()] 58 + 59 + with patch("openai.OpenAI", return_value=client) as mock_cls: 60 + result = think.providers.openai.validate_key("test-key") 61 + 62 + assert result == {"valid": True} 63 + mock_cls.assert_called_once_with(api_key="test-key", timeout=10) 64 + 65 + 66 + def test_validate_key_openai_auth_error(): 67 + client = Mock() 68 + client.models.list.side_effect = Exception("Incorrect API key") 69 + 70 + with patch("openai.OpenAI", return_value=client): 71 + result = think.providers.openai.validate_key("bad-key") 72 + 73 + assert result["valid"] is False 74 + assert "Incorrect API key" in result["error"] 75 + 76 + 77 + def test_validate_key_google_success(): 78 + client = Mock() 79 + client.models.list.return_value = [Mock()] 80 + 81 + with patch("think.providers.google.genai.Client", return_value=client) as mock_cls: 82 + result = think.providers.google.validate_key("test-key") 83 + 84 + assert result == {"valid": True} 85 + mock_cls.assert_called_once() 86 + assert mock_cls.call_args.kwargs["api_key"] == "test-key" 87 + 88 + 89 + def test_validate_key_google_auth_error(): 90 + client = Mock() 91 + client.models.list.side_effect = Exception("API key not valid") 92 + 93 + with patch("think.providers.google.genai.Client", return_value=client): 94 + result = think.providers.google.validate_key("bad-key") 95 + 96 + assert result["valid"] is False 97 + assert "API key not valid" in result["error"] 98 + 99 + 100 + def test_validate_key_dispatcher_success(): 101 + with patch("think.providers.google.validate_key", return_value={"valid": True}): 102 + result = validate_key("google", "test-key") 103 + 104 + assert result == {"valid": True} 105 + 106 + 107 + def test_validate_key_dispatcher_unknown_provider(): 108 + with pytest.raises(ValueError, match="Unknown provider"): 109 + validate_key("bogus", "test-key") 110 + 111 + 112 + def test_validate_key_timeout(): 113 + """Validate that timeout exceptions are caught and reported.""" 114 + client = Mock() 115 + client.models.list.side_effect = TimeoutError("Connection timed out") 116 + 117 + with patch("openai.OpenAI", return_value=client): 118 + result = think.providers.openai.validate_key("test-key") 119 + 120 + assert result["valid"] is False 121 + assert "timed out" in result["error"] 122 + 123 + 124 + def test_update_config_saves_key_validation(settings_client): 125 + client, journal = settings_client 126 + 127 + with patch("think.providers.validate_key", return_value={"valid": False, "error": "bad key"}): 128 + response = client.put( 129 + "/app/settings/api/config", 130 + json={"section": "env", "data": {"GOOGLE_API_KEY": "bad-key"}}, 131 + ) 132 + 133 + assert response.status_code == 200 134 + payload = response.get_json() 135 + assert payload["success"] is True 136 + assert payload["key_validation"]["google"]["valid"] is False 137 + assert payload["key_validation"]["google"]["error"] == "bad key" 138 + assert "timestamp" in payload["key_validation"]["google"] 139 + 140 + config = json.loads((journal / "config" / "journal.json").read_text()) 141 + assert config["providers"]["auth"]["google"] == "api_key" 142 + assert config["providers"]["key_validation"]["google"]["valid"] is False 143 + 144 + 145 + def test_update_config_clears_key_validation(settings_client): 146 + client, journal = settings_client 147 + config_path = journal / "config" / "journal.json" 148 + config = json.loads(config_path.read_text()) 149 + config.setdefault("env", {})["GOOGLE_API_KEY"] = "existing-key" 150 + config.setdefault("providers", {}).setdefault("auth", {})["google"] = "api_key" 151 + config["providers"]["key_validation"] = { 152 + "google": {"valid": True, "timestamp": "2026-01-01T00:00:00+00:00"} 153 + } 154 + config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8") 155 + 156 + response = client.put( 157 + "/app/settings/api/config", 158 + json={"section": "env", "data": {"GOOGLE_API_KEY": ""}}, 159 + ) 160 + 161 + assert response.status_code == 200 162 + payload = response.get_json() 163 + assert payload["success"] is True 164 + assert "google" not in payload["key_validation"] 165 + 166 + saved = json.loads(config_path.read_text()) 167 + assert saved["providers"]["auth"]["google"] == "platform" 168 + assert "google" not in saved["providers"]["key_validation"] 169 + 170 + 171 + def test_get_providers_includes_key_validation(settings_client): 172 + client, journal = settings_client 173 + config_path = journal / "config" / "journal.json" 174 + config = json.loads(config_path.read_text()) 175 + config.setdefault("providers", {})["key_validation"] = { 176 + "openai": {"valid": True, "timestamp": "2026-01-01T00:00:00+00:00"} 177 + } 178 + config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8") 179 + 180 + response = client.get("/app/settings/api/providers") 181 + 182 + assert response.status_code == 200 183 + payload = response.get_json() 184 + assert payload["key_validation"]["openai"]["valid"] is True 185 + 186 + 187 + def test_validate_all_keys_endpoint(settings_client): 188 + client, journal = settings_client 189 + config_path = journal / "config" / "journal.json" 190 + config = json.loads(config_path.read_text()) 191 + config.setdefault("env", {})["GOOGLE_API_KEY"] = "google-key" 192 + config["env"]["OPENAI_API_KEY"] = "openai-key" 193 + config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8") 194 + 195 + def fake_validate(provider: str, api_key: str) -> dict: 196 + return {"valid": provider == "google", "error": "" if provider == "google" else "bad key"} 197 + 198 + with patch("think.providers.validate_key", side_effect=fake_validate): 199 + response = client.post("/app/settings/api/validate-keys") 200 + 201 + assert response.status_code == 200 202 + payload = response.get_json() 203 + assert payload["success"] is True 204 + assert payload["key_validation"]["google"]["valid"] is True 205 + assert payload["key_validation"]["openai"]["valid"] is False 206 + assert "timestamp" in payload["key_validation"]["google"] 207 + 208 + saved = json.loads(config_path.read_text()) 209 + assert set(saved["providers"]["key_validation"]) == {"google", "openai"}
+25
think/providers/__init__.py
··· 117 117 return module.list_models() 118 118 119 119 120 + def validate_key(provider: str, api_key: str) -> dict: 121 + """Validate an API key for a provider. 122 + 123 + Parameters 124 + ---------- 125 + provider 126 + Provider name (e.g., "google", "openai", "anthropic"). 127 + api_key 128 + The API key string to validate. 129 + 130 + Returns 131 + ------- 132 + dict 133 + {"valid": True} or {"valid": False, "error": "..."}. 134 + 135 + Raises 136 + ------ 137 + ValueError 138 + If the provider is not registered. 139 + """ 140 + module = get_provider_module(provider) 141 + return module.validate_key(api_key) 142 + 143 + 120 144 __all__ = [ 121 145 "PROVIDER_REGISTRY", 122 146 "PROVIDER_METADATA", 123 147 "get_provider_module", 124 148 "get_provider_list", 125 149 "get_provider_models", 150 + "validate_key", 126 151 ]
+19
think/providers/anthropic.py
··· 566 566 return [m.model_dump() for m in client.models.list()] 567 567 568 568 569 + def validate_key(api_key: str) -> dict: 570 + """Validate an Anthropic API key by listing models. 571 + 572 + Creates a temporary client with the provided key. Never uses 573 + the cached client or environment variables. 574 + 575 + Returns {"valid": True} or {"valid": False, "error": "..."}. 576 + """ 577 + try: 578 + from anthropic import Anthropic 579 + 580 + client = Anthropic(api_key=api_key, timeout=10) 581 + list(client.models.list(limit=1)) 582 + return {"valid": True} 583 + except Exception as e: 584 + return {"valid": False, "error": str(e)} 585 + 586 + 569 587 __all__ = [ 570 588 "run_cogitate", 571 589 "run_generate", 572 590 "run_agenerate", 573 591 "list_models", 592 + "validate_key", 574 593 ]
+20
think/providers/google.py
··· 674 674 return [m.model_dump() for m in client.models.list()] 675 675 676 676 677 + def validate_key(api_key: str) -> dict: 678 + """Validate a Google API key by listing models. 679 + 680 + Creates a temporary client with the provided key. Never uses 681 + the cached client or environment variables. 682 + 683 + Returns {"valid": True} or {"valid": False, "error": "..."}. 684 + """ 685 + try: 686 + client = genai.Client( 687 + api_key=api_key, 688 + http_options=types.HttpOptions(timeout=10000), 689 + ) 690 + list(client.models.list(config={"page_size": 1})) 691 + return {"valid": True} 692 + except Exception as e: 693 + return {"valid": False, "error": str(e)} 694 + 695 + 677 696 __all__ = [ 678 697 "run_cogitate", 679 698 "run_generate", 680 699 "run_agenerate", 681 700 "get_or_create_client", 682 701 "list_models", 702 + "validate_key", 683 703 ]
+19
think/providers/openai.py
··· 465 465 return [m.model_dump() for m in client.models.list()] 466 466 467 467 468 + def validate_key(api_key: str) -> dict: 469 + """Validate an OpenAI API key by listing models. 470 + 471 + Creates a temporary client with the provided key. Never uses 472 + the cached client or environment variables. 473 + 474 + Returns {"valid": True} or {"valid": False, "error": "..."}. 475 + """ 476 + try: 477 + import openai 478 + 479 + client = openai.OpenAI(api_key=api_key, timeout=10) 480 + list(client.models.list()) 481 + return {"valid": True} 482 + except Exception as e: 483 + return {"valid": False, "error": str(e)} 484 + 485 + 468 486 __all__ = [ 469 487 "run_cogitate", 470 488 "run_generate", 471 489 "run_agenerate", 472 490 "list_models", 491 + "validate_key", 473 492 ]