personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Vertex AI Express Mode dual-backend support (WS3)

Auto-detect whether a GOOGLE_API_KEY is AI Studio or Vertex AI Express by
probing the AI Studio models endpoint. Cache the result per-process so the
probe runs at most once. Manual override via providers.google_backend config
field ("auto"/"aistudio"/"vertex").

- get_or_create_client() and validate_key() pass vertexai explicitly to
genai.Client() (True for Vertex, False for AI Studio) preventing ambient
GOOGLE_GENAI_USE_VERTEXAI env from overriding the detected backend
- validate_key() returns a backend field alongside valid
- build_cogitate_env() sets GOOGLE_GENAI_USE_VERTEXAI, project, and location
env vars for Vertex; clears inherited Vertex vars for AI Studio
- Settings API GET/PUT for google_backend, vertex_project, vertex_location
- Settings UI backend badge on Google key, backend selector, conditional
project/location fields
- PROVIDER_METADATA gets vertex_env_keys

+486 -19
+34
apps/settings/routes.py
··· 435 435 "api_keys": api_keys, 436 436 "auth": auth, 437 437 "key_validation": key_validation, 438 + "google_backend": providers_config.get("google_backend", "auto"), 439 + "vertex_project": providers_config.get("vertex_project", ""), 440 + "vertex_location": providers_config.get("vertex_location", ""), 438 441 } 439 442 ) 440 443 except Exception: ··· 696 699 "new": ctx_config, 697 700 } 698 701 config["providers"]["contexts"][pattern] = ctx_config 702 + 703 + # Handle Google backend settings 704 + if "google_backend" in request_data: 705 + backend = request_data["google_backend"] 706 + if backend not in ("auto", "aistudio", "vertex"): 707 + return ( 708 + jsonify( 709 + { 710 + "error": f"Invalid google_backend: {backend}. " 711 + "Must be 'auto', 'aistudio', or 'vertex'." 712 + } 713 + ), 714 + 400, 715 + ) 716 + old_val = old_providers.get("google_backend", "auto") 717 + if old_val != backend: 718 + changed_fields["google_backend"] = {"old": old_val, "new": backend} 719 + config["providers"]["google_backend"] = backend 720 + 721 + for vfield in ("vertex_project", "vertex_location"): 722 + if vfield in request_data: 723 + value = request_data[vfield] 724 + if not isinstance(value, str): 725 + return jsonify({"error": f"{vfield} must be a string"}), 400 726 + old_val = old_providers.get(vfield, "") 727 + if old_val != value: 728 + changed_fields[vfield] = {"old": old_val, "new": value} 729 + if value: 730 + config["providers"][vfield] = value 731 + else: 732 + config["providers"].pop(vfield, None) 699 733 700 734 # Write back to file 701 735 with open(config_path, "w", encoding="utf-8") as f:
+91 -1
apps/settings/workspace.html
··· 1911 1911 <small>How the CLI authenticates with the provider</small> 1912 1912 </div> 1913 1913 </div> 1914 + <h3 style="margin:1.2em 0 0.5em 0;font-size:1em">google backend</h3> 1915 + <p style="color:#666;font-size:0.85em;margin:0 0 0.8em 0">How solstone connects to Google AI. Auto-detect works for most users.</p> 1916 + <div class="provider-row"> 1917 + <div class="settings-field"> 1918 + <label for="field-google-backend">backend</label> 1919 + <select id="field-google-backend"> 1920 + <option value="auto">Auto-detect</option> 1921 + <option value="aistudio">AI Studio</option> 1922 + <option value="vertex">Vertex AI Express</option> 1923 + </select> 1924 + <small>Auto-detect probes your key on first use</small> 1925 + </div> 1926 + </div> 1927 + <div id="vertexFields" style="display:none"> 1928 + <div class="provider-row"> 1929 + <div class="settings-field"> 1930 + <label for="field-vertex-project">project</label> 1931 + <input type="text" id="field-vertex-project" placeholder="Optional for Express Mode"> 1932 + <small>Google Cloud project ID</small> 1933 + </div> 1934 + <div class="settings-field"> 1935 + <label for="field-vertex-location">location</label> 1936 + <input type="text" id="field-vertex-location" placeholder="Optional - e.g., us-central1"> 1937 + <small>Google Cloud region</small> 1938 + </div> 1939 + </div> 1940 + </div> 1914 1941 <div id="cogitateProviderKeyWarning" class="provider-key-warning" style="display:none"> 1915 1942 <span>&#9888;</span> 1916 1943 <span>API key not configured for cogitate provider.</span> ··· 3081 3108 validationSpan.className = 'key-validation-status'; 3082 3109 if (validation.valid) { 3083 3110 validationSpan.className += ' key-status-valid'; 3084 - validationSpan.textContent = ' ✓ Valid'; 3111 + let validText = ' ✓ Valid'; 3112 + if (providerName === 'google' && validation.backend) { 3113 + validText += validation.backend === 'vertex' ? ' (Vertex AI)' : ' (AI Studio)'; 3114 + } 3115 + validationSpan.textContent = validText; 3085 3116 } else { 3086 3117 validationSpan.className += ' key-status-invalid'; 3087 3118 const errMsg = validation.error || 'Invalid'; ··· 3714 3745 authSelect.value = data.auth[cogitateProvider] || 'platform'; 3715 3746 } 3716 3747 3748 + // Set Google backend selector 3749 + const backendSelect = document.getElementById('field-google-backend'); 3750 + if (backendSelect) { 3751 + backendSelect.value = data.google_backend || 'auto'; 3752 + } 3753 + const vertexProjectField = document.getElementById('field-vertex-project'); 3754 + const vertexLocationField = document.getElementById('field-vertex-location'); 3755 + if (vertexProjectField) vertexProjectField.value = data.vertex_project || ''; 3756 + if (vertexLocationField) vertexLocationField.value = data.vertex_location || ''; 3757 + // Show/hide vertex fields 3758 + const vertexFields = document.getElementById('vertexFields'); 3759 + if (vertexFields) { 3760 + vertexFields.style.display = (data.google_backend === 'vertex') ? '' : 'none'; 3761 + } 3762 + 3717 3763 // Update status text 3718 3764 document.getElementById('providerStatus').textContent = ''; 3719 3765 ··· 4027 4073 showFieldStatus(authSelect, 'error', err.message); 4028 4074 } 4029 4075 }); 4076 + 4077 + // Google backend change handler 4078 + document.getElementById('field-google-backend')?.addEventListener('change', async function() { 4079 + const value = this.value; 4080 + try { 4081 + const response = await fetch('api/providers', { 4082 + method: 'PUT', 4083 + headers: { 'Content-Type': 'application/json' }, 4084 + body: JSON.stringify({ google_backend: value }) 4085 + }); 4086 + const result = await response.json(); 4087 + if (result.error) throw new Error(result.error); 4088 + providersData = result; 4089 + showFieldStatus(this, 'saved'); 4090 + const vf = document.getElementById('vertexFields'); 4091 + if (vf) vf.style.display = value === 'vertex' ? '' : 'none'; 4092 + } catch (err) { 4093 + console.error('Error saving google backend:', err); 4094 + showFieldStatus(this, 'error', err.message); 4095 + } 4096 + }); 4097 + 4098 + // Vertex project/location change handlers 4099 + for (const vfield of ['vertex-project', 'vertex-location']) { 4100 + const el = document.getElementById(`field-${vfield}`); 4101 + if (!el) continue; 4102 + el.addEventListener('change', async function() { 4103 + const key = vfield.replace('-', '_'); 4104 + try { 4105 + const response = await fetch('api/providers', { 4106 + method: 'PUT', 4107 + headers: { 'Content-Type': 'application/json' }, 4108 + body: JSON.stringify({ [key]: this.value }) 4109 + }); 4110 + const result = await response.json(); 4111 + if (result.error) throw new Error(result.error); 4112 + providersData = result; 4113 + showFieldStatus(this, 'saved'); 4114 + } catch (err) { 4115 + console.error(`Error saving ${vfield}:`, err); 4116 + showFieldStatus(this, 'error', err.message); 4117 + } 4118 + }); 4119 + } 4030 4120 4031 4121 // ========== TRANSCRIPTION ========== 4032 4122 // Backend metadata loaded from API
+10 -2
tests/baselines/api/settings/providers.json
··· 429 429 "provider": "google", 430 430 "tier": 2 431 431 }, 432 + "google_backend": "auto", 432 433 "key_validation": {}, 433 434 "providers": [ 434 435 { ··· 439 440 { 440 441 "env_key": "GOOGLE_API_KEY", 441 442 "label": "Google (Gemini)", 442 - "name": "google" 443 + "name": "google", 444 + "vertex_env_keys": [ 445 + "GOOGLE_GENAI_USE_VERTEXAI", 446 + "GOOGLE_CLOUD_PROJECT", 447 + "GOOGLE_CLOUD_LOCATION" 448 + ] 443 449 }, 444 450 { 445 451 "env_key": "OPENAI_API_KEY", 446 452 "label": "OpenAI (GPT)", 447 453 "name": "openai" 448 454 } 449 - ] 455 + ], 456 + "vertex_location": "", 457 + "vertex_project": "" 450 458 }
+80
tests/test_cli_provider.py
··· 642 642 oai_env = build_cogitate_env("OPENAI_API_KEY") 643 643 assert ant_env["ANTHROPIC_API_KEY"] == "sk-ant" 644 644 assert "OPENAI_API_KEY" not in oai_env 645 + 646 + def test_vertex_backend_sets_env_vars(self): 647 + """Vertex backend sets GOOGLE_GENAI_USE_VERTEXAI and preserves key.""" 648 + config = { 649 + "providers": { 650 + "google_backend": "vertex", 651 + "auth": {"google": "platform"}, 652 + } 653 + } 654 + with ( 655 + patch.dict(os.environ, {"GOOGLE_API_KEY": "gk-test"}, clear=True), 656 + patch("think.utils.get_config", return_value=config), 657 + ): 658 + env = build_cogitate_env("GOOGLE_API_KEY") 659 + assert env["GOOGLE_GENAI_USE_VERTEXAI"] == "true" 660 + assert env["GOOGLE_API_KEY"] == "gk-test" 661 + 662 + def test_vertex_backend_with_project_location(self): 663 + """Vertex config sets project/location env vars.""" 664 + config = { 665 + "providers": { 666 + "google_backend": "vertex", 667 + "vertex_project": "my-project", 668 + "vertex_location": "us-central1", 669 + "auth": {"google": "api_key"}, 670 + } 671 + } 672 + with ( 673 + patch.dict(os.environ, {"GOOGLE_API_KEY": "gk-test"}, clear=True), 674 + patch("think.utils.get_config", return_value=config), 675 + ): 676 + env = build_cogitate_env("GOOGLE_API_KEY") 677 + assert env["GOOGLE_GENAI_USE_VERTEXAI"] == "true" 678 + assert env["GOOGLE_CLOUD_PROJECT"] == "my-project" 679 + assert env["GOOGLE_CLOUD_LOCATION"] == "us-central1" 680 + assert env["GOOGLE_API_KEY"] == "gk-test" 681 + 682 + def test_aistudio_backend_no_vertex_env_vars(self): 683 + """AI Studio backend does not set Vertex env vars.""" 684 + config = { 685 + "providers": { 686 + "google_backend": "aistudio", 687 + "auth": {"google": "api_key"}, 688 + } 689 + } 690 + with ( 691 + patch.dict(os.environ, {"GOOGLE_API_KEY": "gk-test"}, clear=True), 692 + patch("think.utils.get_config", return_value=config), 693 + ): 694 + env = build_cogitate_env("GOOGLE_API_KEY") 695 + assert "GOOGLE_GENAI_USE_VERTEXAI" not in env 696 + assert env["GOOGLE_API_KEY"] == "gk-test" 697 + 698 + def test_auto_backend_detects_vertex(self): 699 + """Auto backend with Vertex detection sets env vars.""" 700 + config = {"providers": {"auth": {"google": "platform"}}} 701 + with ( 702 + patch.dict(os.environ, {"GOOGLE_API_KEY": "gk-test"}, clear=True), 703 + patch("think.utils.get_config", return_value=config), 704 + patch("think.providers.google._detect_backend", return_value="vertex"), 705 + ): 706 + env = build_cogitate_env("GOOGLE_API_KEY") 707 + assert env["GOOGLE_GENAI_USE_VERTEXAI"] == "true" 708 + assert env["GOOGLE_API_KEY"] == "gk-test" 709 + 710 + def test_non_google_key_unaffected_by_vertex(self): 711 + """Vertex logic only applies to GOOGLE_API_KEY.""" 712 + config = { 713 + "providers": { 714 + "google_backend": "vertex", 715 + "auth": {"anthropic": "api_key"}, 716 + } 717 + } 718 + with ( 719 + patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-ant"}, clear=True), 720 + patch("think.utils.get_config", return_value=config), 721 + ): 722 + env = build_cogitate_env("ANTHROPIC_API_KEY") 723 + assert "GOOGLE_GENAI_USE_VERTEXAI" not in env 724 + assert env["ANTHROPIC_API_KEY"] == "sk-ant"
+151 -4
tests/test_validate_key.py
··· 13 13 import think.providers.google 14 14 import think.providers.openai 15 15 from convey import create_app 16 - from tests.conftest import copytree_tracked 17 16 from think.providers import validate_key 18 17 19 18 ··· 22 21 app = create_app(str(journal_copy)) 23 22 app.config["TESTING"] = True 24 23 return app.test_client(), journal_copy 24 + 25 + 26 + @pytest.fixture(autouse=True) 27 + def reset_google_backend_cache(): 28 + original = think.providers.google._detected_backend 29 + think.providers.google._detected_backend = None 30 + yield 31 + think.providers.google._detected_backend = original 25 32 26 33 27 34 def test_validate_key_anthropic_success(): ··· 72 79 client = Mock() 73 80 client.models.list.return_value = [Mock()] 74 81 75 - with patch("think.providers.google.genai.Client", return_value=client) as mock_cls: 82 + with ( 83 + patch("think.providers.google.genai.Client", return_value=client) as mock_cls, 84 + patch("think.providers.google._probe_backend", return_value="aistudio"), 85 + ): 76 86 result = think.providers.google.validate_key("test-key") 77 87 78 - assert result == {"valid": True} 88 + assert result == {"valid": True, "backend": "aistudio"} 79 89 mock_cls.assert_called_once() 80 90 assert mock_cls.call_args.kwargs["api_key"] == "test-key" 81 91 ··· 84 94 client = Mock() 85 95 client.models.list.side_effect = Exception("API key not valid") 86 96 87 - with patch("think.providers.google.genai.Client", return_value=client): 97 + with ( 98 + patch("think.providers.google.genai.Client", return_value=client), 99 + patch("think.providers.google._probe_backend", return_value="aistudio"), 100 + ): 88 101 result = think.providers.google.validate_key("bad-key") 89 102 90 103 assert result["valid"] is False 91 104 assert "API key not valid" in result["error"] 92 105 93 106 107 + def test_validate_key_google_returns_backend_aistudio(): 108 + """validate_key returns backend field when successful.""" 109 + client = Mock() 110 + client.models.list.return_value = [Mock()] 111 + 112 + with ( 113 + patch("think.providers.google.genai.Client", return_value=client), 114 + patch("think.providers.google._probe_backend", return_value="aistudio"), 115 + ): 116 + result = think.providers.google.validate_key("test-key") 117 + 118 + assert result == {"valid": True, "backend": "aistudio"} 119 + 120 + 121 + def test_validate_key_google_returns_backend_vertex(): 122 + """validate_key returns vertex backend and uses vertexai=True.""" 123 + client = Mock() 124 + client.models.list.return_value = [Mock()] 125 + 126 + with ( 127 + patch("think.providers.google.genai.Client", return_value=client) as mock_cls, 128 + patch("think.providers.google._probe_backend", return_value="vertex"), 129 + ): 130 + result = think.providers.google.validate_key("test-key") 131 + 132 + assert result == {"valid": True, "backend": "vertex"} 133 + assert mock_cls.call_args.kwargs["vertexai"] is True 134 + assert mock_cls.call_args.kwargs["api_key"] == "test-key" 135 + 136 + 137 + def test_probe_backend_aistudio(): 138 + """HTTP 200 from AI Studio endpoint -> aistudio.""" 139 + mock_resp = Mock() 140 + mock_resp.status_code = 200 141 + with patch("httpx.get", return_value=mock_resp): 142 + result = think.providers.google._probe_backend("test-key") 143 + assert result == "aistudio" 144 + 145 + 146 + def test_probe_backend_vertex(): 147 + """Non-200 from AI Studio endpoint -> vertex.""" 148 + mock_resp = Mock() 149 + mock_resp.status_code = 403 150 + with patch("httpx.get", return_value=mock_resp): 151 + result = think.providers.google._probe_backend("test-key") 152 + assert result == "vertex" 153 + 154 + 155 + def test_probe_backend_error_defaults_aistudio(): 156 + """Network error defaults to aistudio.""" 157 + with patch("httpx.get", side_effect=Exception("timeout")): 158 + result = think.providers.google._probe_backend("test-key") 159 + assert result == "aistudio" 160 + 161 + 162 + def test_detect_backend_caches(): 163 + """Second call returns cached result without probing.""" 164 + import think.providers.google as gmod 165 + 166 + original = gmod._detected_backend 167 + try: 168 + gmod._detected_backend = None 169 + mock_resp = Mock() 170 + mock_resp.status_code = 403 171 + with patch("httpx.get", return_value=mock_resp) as mock_get: 172 + r1 = gmod._detect_backend("key") 173 + r2 = gmod._detect_backend("key") 174 + assert r1 == "vertex" 175 + assert r2 == "vertex" 176 + assert mock_get.call_count == 1 177 + finally: 178 + gmod._detected_backend = original 179 + 180 + 181 + def test_get_effective_backend_config_override(): 182 + """Config override skips detection.""" 183 + import think.providers.google as gmod 184 + 185 + original = gmod._detected_backend 186 + try: 187 + gmod._detected_backend = None 188 + config = {"providers": {"google_backend": "vertex"}} 189 + with patch("think.utils.get_config", return_value=config): 190 + result = gmod._get_effective_backend("key") 191 + assert result == "vertex" 192 + assert gmod._detected_backend is None 193 + finally: 194 + gmod._detected_backend = original 195 + 196 + 94 197 def test_validate_key_dispatcher_success(): 95 198 with patch("think.providers.google.validate_key", return_value={"valid": True}): 96 199 result = validate_key("google", "test-key") ··· 207 310 208 311 saved = json.loads(config_path.read_text()) 209 312 assert set(saved["providers"]["key_validation"]) == {"google", "openai"} 313 + 314 + 315 + def test_providers_google_backend_roundtrip(settings_client): 316 + """PUT/GET google_backend, vertex_project, vertex_location.""" 317 + client, journal = settings_client 318 + 319 + response = client.put( 320 + "/app/settings/api/providers", 321 + json={ 322 + "google_backend": "vertex", 323 + "vertex_project": "my-project", 324 + "vertex_location": "us-central1", 325 + }, 326 + ) 327 + assert response.status_code == 200 328 + payload = response.get_json() 329 + assert payload["google_backend"] == "vertex" 330 + assert payload["vertex_project"] == "my-project" 331 + assert payload["vertex_location"] == "us-central1" 332 + 333 + # Verify persisted 334 + config = json.loads((journal / "config" / "journal.json").read_text()) 335 + assert config["providers"]["google_backend"] == "vertex" 336 + assert config["providers"]["vertex_project"] == "my-project" 337 + assert config["providers"]["vertex_location"] == "us-central1" 338 + 339 + # GET returns the same 340 + response = client.get("/app/settings/api/providers") 341 + payload = response.get_json() 342 + assert payload["google_backend"] == "vertex" 343 + assert payload["vertex_project"] == "my-project" 344 + assert payload["vertex_location"] == "us-central1" 345 + 346 + 347 + def test_providers_google_backend_invalid(settings_client): 348 + """Invalid google_backend is rejected.""" 349 + client, _journal = settings_client 350 + 351 + response = client.put( 352 + "/app/settings/api/providers", 353 + json={"google_backend": "invalid"}, 354 + ) 355 + assert response.status_code == 400 356 + assert "Invalid google_backend" in response.get_json()["error"]
+10 -2
think/providers/__init__.py
··· 46 46 # Used by settings UI to dynamically build provider dropdowns. 47 47 # --------------------------------------------------------------------------- 48 48 49 - PROVIDER_METADATA: Dict[str, Dict[str, str]] = { 50 - "google": {"label": "Google (Gemini)", "env_key": "GOOGLE_API_KEY"}, 49 + PROVIDER_METADATA: Dict[str, Dict[str, Any]] = { 50 + "google": { 51 + "label": "Google (Gemini)", 52 + "env_key": "GOOGLE_API_KEY", 53 + "vertex_env_keys": [ 54 + "GOOGLE_GENAI_USE_VERTEXAI", 55 + "GOOGLE_CLOUD_PROJECT", 56 + "GOOGLE_CLOUD_LOCATION", 57 + ], 58 + }, 51 59 "openai": {"label": "OpenAI (GPT)", "env_key": "OPENAI_API_KEY"}, 52 60 "anthropic": {"label": "Anthropic (Claude)", "env_key": "ANTHROPIC_API_KEY"}, 53 61 }
+40
think/providers/cli.py
··· 444 444 env = os.environ.copy() 445 445 if auth_mode == "platform": 446 446 env.pop(env_key, None) 447 + 448 + # Vertex AI Express: set backend env vars for Google provider 449 + if env_key == "GOOGLE_API_KEY": 450 + providers_config = config.get("providers", {}) 451 + google_backend = providers_config.get("google_backend", "auto") 452 + 453 + # Determine effective backend 454 + if google_backend in ("aistudio", "vertex"): 455 + effective_backend = google_backend 456 + else: 457 + api_key = os.getenv("GOOGLE_API_KEY", "") 458 + if api_key: 459 + from think.providers.google import _detect_backend 460 + 461 + effective_backend = _detect_backend(api_key) 462 + else: 463 + effective_backend = "aistudio" 464 + 465 + if effective_backend == "vertex": 466 + env["GOOGLE_GENAI_USE_VERTEXAI"] = "true" 467 + # Preserve API key for Vertex Express regardless of auth mode 468 + api_key = os.getenv("GOOGLE_API_KEY") 469 + if api_key: 470 + env["GOOGLE_API_KEY"] = api_key 471 + # Set project/location from config if present 472 + vertex_project = providers_config.get("vertex_project") 473 + if vertex_project: 474 + env["GOOGLE_CLOUD_PROJECT"] = vertex_project 475 + vertex_location = providers_config.get("vertex_location") 476 + if vertex_location: 477 + env["GOOGLE_CLOUD_LOCATION"] = vertex_location 478 + else: 479 + # AI Studio: clear any inherited Vertex env vars so the CLI 480 + # doesn't accidentally run in Vertex mode. 481 + for vkey in ( 482 + "GOOGLE_GENAI_USE_VERTEXAI", 483 + "GOOGLE_CLOUD_PROJECT", 484 + "GOOGLE_CLOUD_LOCATION", 485 + ): 486 + env.pop(vkey, None) 447 487 return env 448 488 449 489
+70 -10
think/providers/google.py
··· 60 60 61 61 logger = logging.getLogger(__name__) 62 62 63 + # Vertex AI Express Mode backend detection cache 64 + _detected_backend: str | None = None 65 + 63 66 64 67 # --------------------------------------------------------------------------- 65 68 # Client and helper functions for generate/agenerate 66 69 # --------------------------------------------------------------------------- 67 70 68 71 72 + def _probe_backend(api_key: str) -> str: 73 + """Probe AI Studio endpoint to classify key type. 74 + 75 + Returns ``"aistudio"`` when the key works against the AI Studio models 76 + endpoint (HTTP 200) or ``"vertex"`` otherwise. Network errors default 77 + to ``"aistudio"`` for backward compatibility. 78 + """ 79 + try: 80 + import httpx 81 + 82 + resp = httpx.get( 83 + "https://generativelanguage.googleapis.com/v1beta/models", 84 + params={"key": api_key}, 85 + timeout=5, 86 + ) 87 + return "aistudio" if resp.status_code == 200 else "vertex" 88 + except Exception: 89 + return "aistudio" 90 + 91 + 92 + def _detect_backend(api_key: str) -> str: 93 + """Return cached backend detection result, probing on first call.""" 94 + global _detected_backend 95 + if _detected_backend is not None: 96 + return _detected_backend 97 + _detected_backend = _probe_backend(api_key) 98 + return _detected_backend 99 + 100 + 101 + def _get_effective_backend(api_key: str) -> str: 102 + """Return effective backend, checking config override before cache. 103 + 104 + Reads ``providers.google_backend`` from journal config. Values 105 + ``"aistudio"`` or ``"vertex"`` bypass detection; ``"auto"`` (the default 106 + when the key is absent) uses :func:`_detect_backend`. 107 + """ 108 + from think.utils import get_config 109 + 110 + configured = get_config().get("providers", {}).get("google_backend", "auto") 111 + if configured in ("aistudio", "vertex"): 112 + return configured 113 + return _detect_backend(api_key) 114 + 115 + 69 116 def get_or_create_client(client: genai.Client | None = None) -> genai.Client: 70 117 """Get existing client or create new one. 71 118 ··· 84 131 api_key = os.getenv("GOOGLE_API_KEY") 85 132 if not api_key: 86 133 raise ValueError("GOOGLE_API_KEY not found in environment") 87 - client = genai.Client( 88 - api_key=api_key, 89 - http_options=types.HttpOptions( 134 + client_kwargs = { 135 + "api_key": api_key, 136 + "http_options": types.HttpOptions( 90 137 retry_options=types.HttpRetryOptions(attempts=8) 91 138 ), 92 - ) 139 + } 140 + client_kwargs["vertexai"] = _get_effective_backend(api_key) == "vertex" 141 + client = genai.Client(**client_kwargs) 93 142 return client 94 143 95 144 ··· 682 731 Creates a temporary client with the provided key. Never uses 683 732 the cached client or environment variables. 684 733 685 - Returns {"valid": True} or {"valid": False, "error": "..."}. 734 + Returns {"valid": True, "backend": "aistudio"|"vertex"} or 735 + {"valid": False, "error": "..."}. 686 736 """ 737 + global _detected_backend 687 738 try: 688 - client = genai.Client( 689 - api_key=api_key, 690 - http_options=types.HttpOptions(timeout=10000), 691 - ) 739 + # Probe backend for this specific key (always probes, bypasses cache). 740 + backend = _probe_backend(api_key) 741 + 742 + client_kwargs = { 743 + "api_key": api_key, 744 + "http_options": types.HttpOptions(timeout=10000), 745 + "vertexai": backend == "vertex", 746 + } 747 + 748 + client = genai.Client(**client_kwargs) 692 749 list(client.models.list(config={"page_size": 1})) 693 - return {"valid": True} 750 + _detected_backend = backend # only cache after successful validation 751 + return {"valid": True, "backend": backend} 694 752 except Exception as e: 695 753 return {"valid": False, "error": str(e)} 696 754 ··· 700 758 "run_generate", 701 759 "run_agenerate", 702 760 "get_or_create_client", 761 + "_detect_backend", 762 + "_get_effective_backend", 703 763 "list_models", 704 764 "validate_key", 705 765 ]