Claude commited on
Commit
b7fee66
·
unverified ·
1 Parent(s): 2ad4a3a

fix(vertex): corriger le routing API pour VERTEX_API_KEY — ajouter vertexai=True

Browse files

Cause racine du 403 PERMISSION_DENIED dans les logs de production :
genai.Client(api_key=VERTEX_API_KEY)
→ route vers generativelanguage.googleapis.com (Gemini Developer API)
→ rejette les clés Vertex Express avec 403 car elles ne sont pas
autorisées sur cette API

Fix :
genai.Client(vertexai=True, api_key=VERTEX_API_KEY)
→ route vers aiplatform.googleapis.com (Vertex AI endpoint correct)
→ project/location omis car mutually exclusive avec api_key dans le
constructeur SDK (la clé Express encode le projet)

Confirmé par le code source du SDK google-genai 1.67.0 :
"api_key: Applies to the Gemini Developer API only"
"project/location and API key are mutually exclusive"

Fichiers modifiés :
- provider_vertex_key.py : _build_client() avec vertexai=True,
fallback modèles Vertex sans supported_generation_methods (nom "gemini")
- client_factory.py : même correction pour cohérence
- test_ai_providers.py : 3 nouveaux tests Vertex (vertexai flag,
modèles sans méthodes, generate_content), assertion mise à jour
- test_ai_analyzer.py : assertion MockClient mise à jour

https://claude.ai/code/session_018woyEHc8HG2th7V4ewJ4Kg

backend/app/services/ai/client_factory.py CHANGED
@@ -57,8 +57,12 @@ def build_client(provider_type: ProviderType) -> genai.Client:
57
  raise RuntimeError(
58
  "Variable d'environnement manquante : VERTEX_API_KEY"
59
  )
60
- logger.debug("Client Vertex AI (clé API) créé")
61
- return genai.Client(api_key=api_key)
 
 
 
 
62
 
63
  if provider_type == ProviderType.VERTEX_SERVICE_ACCOUNT:
64
  sa_json_str = os.environ.get("VERTEX_SERVICE_ACCOUNT_JSON")
 
57
  raise RuntimeError(
58
  "Variable d'environnement manquante : VERTEX_API_KEY"
59
  )
60
+ logger.debug("Client Vertex AI Express (clé API) créé")
61
+ # vertexai=True route vers aiplatform.googleapis.com (Vertex AI).
62
+ # Sans vertexai=True, le SDK route vers generativelanguage.googleapis.com
63
+ # (Gemini Developer API) qui rejette les clés Vertex Express avec 403.
64
+ # project/location sont omis : mutually exclusive avec api_key dans le SDK.
65
+ return genai.Client(vertexai=True, api_key=api_key)
66
 
67
  if provider_type == ProviderType.VERTEX_SERVICE_ACCOUNT:
68
  sa_json_str = os.environ.get("VERTEX_SERVICE_ACCOUNT_JSON")
backend/app/services/ai/provider_vertex_key.py CHANGED
@@ -1,5 +1,14 @@
1
  """
2
- Provider Vertex AI — authentification via clé API GCP (VERTEX_API_KEY).
 
 
 
 
 
 
 
 
 
3
  """
4
  # 1. stdlib
5
  import logging
@@ -19,10 +28,12 @@ _ENV_KEY = "VERTEX_API_KEY"
19
 
20
 
21
  class VertexAPIKeyProvider(AIProvider):
22
- """Provider Vertex AI via clé API GCP (VERTEX_API_KEY).
23
 
24
- Utilise le SDK google-genai avec la clé GCP. La clé doit être autorisée
25
- sur l'API Generative Language dans la console GCP.
 
 
26
  """
27
 
28
  @property
@@ -32,16 +43,36 @@ class VertexAPIKeyProvider(AIProvider):
32
  def is_configured(self) -> bool:
33
  return bool(os.environ.get(_ENV_KEY))
34
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  def list_models(self) -> list[ModelInfo]:
36
  if not self.is_configured():
37
  raise RuntimeError(f"Variable d'environnement manquante : {_ENV_KEY}")
38
 
39
- client = genai.Client(api_key=os.environ[_ENV_KEY])
40
  result: list[ModelInfo] = []
41
 
42
  for model in client.models.list():
43
  methods = getattr(model, "supported_generation_methods", []) or []
44
- if "generateContent" not in methods:
 
 
 
 
 
 
 
 
45
  continue
46
 
47
  result.append(ModelInfo(
@@ -54,15 +85,15 @@ class VertexAPIKeyProvider(AIProvider):
54
  ))
55
 
56
  logger.info(
57
- "Vertex API key models fetched",
58
- extra={"provider": self.provider_type, "count": len(result)},
59
  )
60
  return result
61
 
62
  def generate_content(self, image_bytes: bytes, prompt: str, model_id: str) -> str:
63
  if not self.is_configured():
64
  raise RuntimeError(f"Variable d'environnement manquante : {_ENV_KEY}")
65
- client = genai.Client(api_key=os.environ[_ENV_KEY])
66
  image_part = types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg")
67
  response = client.models.generate_content(
68
  model=model_id,
 
1
  """
2
+ Provider Vertex AI — authentification via clé API Express Vertex (VERTEX_API_KEY).
3
+
4
+ La clé Vertex Express (format AQ.Ab...) encode le projet GCP ; elle est utilisée
5
+ avec vertexai=True pour router vers aiplatform.googleapis.com (et non vers
6
+ generativelanguage.googleapis.com qui est l'endpoint Google AI Studio).
7
+
8
+ Référence SDK google-genai :
9
+ api_key seul → Gemini Developer API (generativelanguage)
10
+ vertexai=True + api_key → Vertex AI Express mode (aiplatform)
11
+ project/location + api_key → ValueError (mutually exclusive dans le constructeur)
12
  """
13
  # 1. stdlib
14
  import logging
 
28
 
29
 
30
  class VertexAPIKeyProvider(AIProvider):
31
+ """Provider Vertex AI via clé API Express (VERTEX_API_KEY).
32
 
33
+ Utilise genai.Client(vertexai=True, api_key=...) pour router vers
34
+ l'endpoint Vertex AI (aiplatform.googleapis.com). La clé Express encode
35
+ le projet GCP ; project/location explicites sont omis car ils sont
36
+ mutually exclusive avec api_key dans le constructeur SDK.
37
  """
38
 
39
  @property
 
43
  def is_configured(self) -> bool:
44
  return bool(os.environ.get(_ENV_KEY))
45
 
46
+ def _build_client(self) -> genai.Client:
47
+ """Construit un client Vertex AI en mode Express API key.
48
+
49
+ vertexai=True route vers aiplatform.googleapis.com.
50
+ project/location sont omis : mutually exclusive avec api_key
51
+ dans le SDK (la clé Express encode le projet).
52
+ """
53
+ return genai.Client(
54
+ vertexai=True,
55
+ api_key=os.environ[_ENV_KEY],
56
+ )
57
+
58
  def list_models(self) -> list[ModelInfo]:
59
  if not self.is_configured():
60
  raise RuntimeError(f"Variable d'environnement manquante : {_ENV_KEY}")
61
 
62
+ client = self._build_client()
63
  result: list[ModelInfo] = []
64
 
65
  for model in client.models.list():
66
  methods = getattr(model, "supported_generation_methods", []) or []
67
+ # Pour Vertex, certains modèles peuvent ne pas avoir
68
+ # supported_generation_methods renseigné ; on les inclut
69
+ # s'ils contiennent "gemini" dans le nom (modèles génératifs Vertex).
70
+ name_lower = (getattr(model, "name", "") or "").lower()
71
+ is_generative = (
72
+ "generateContent" in methods
73
+ or (not methods and "gemini" in name_lower)
74
+ )
75
+ if not is_generative:
76
  continue
77
 
78
  result.append(ModelInfo(
 
85
  ))
86
 
87
  logger.info(
88
+ "Vertex API key (Express) models fetched",
89
+ extra={"provider": self.provider_type.value, "count": len(result)},
90
  )
91
  return result
92
 
93
  def generate_content(self, image_bytes: bytes, prompt: str, model_id: str) -> str:
94
  if not self.is_configured():
95
  raise RuntimeError(f"Variable d'environnement manquante : {_ENV_KEY}")
96
+ client = self._build_client()
97
  image_part = types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg")
98
  response = client.models.generate_content(
99
  model=model_id,
backend/tests/test_ai_analyzer.py CHANGED
@@ -198,7 +198,7 @@ def test_build_client_vertex_api_key(monkeypatch):
198
  mock_cls.return_value = MagicMock()
199
  client = build_client(ProviderType.VERTEX_API_KEY)
200
 
201
- mock_cls.assert_called_once_with(api_key="fake-vertex-key")
202
  assert client is mock_cls.return_value
203
 
204
 
 
198
  mock_cls.return_value = MagicMock()
199
  client = build_client(ProviderType.VERTEX_API_KEY)
200
 
201
+ mock_cls.assert_called_once_with(vertexai=True, api_key="fake-vertex-key")
202
  assert client is mock_cls.return_value
203
 
204
 
backend/tests/test_ai_providers.py CHANGED
@@ -261,7 +261,46 @@ def test_vertex_key_provider_list_models_success(monkeypatch):
261
  assert len(models) == 1
262
  assert models[0].model_id == "models/gemini-2.0-flash"
263
  assert models[0].provider == ProviderType.VERTEX_API_KEY
264
- MockClient.assert_called_once_with(api_key="fake-vertex-key")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
 
267
  # ---------------------------------------------------------------------------
 
261
  assert len(models) == 1
262
  assert models[0].model_id == "models/gemini-2.0-flash"
263
  assert models[0].provider == ProviderType.VERTEX_API_KEY
264
+ # vertexai=True est obligatoire pour router vers aiplatform.googleapis.com
265
+ # (sans ça, le SDK route vers generativelanguage.googleapis.com → 403)
266
+ MockClient.assert_called_once_with(vertexai=True, api_key="fake-vertex-key")
267
+
268
+
269
+ def test_vertex_key_provider_list_models_includes_gemini_without_methods(monkeypatch):
270
+ """Vertex peut retourner des modèles sans supported_generation_methods.
271
+ Si le nom contient 'gemini', on les inclut quand même."""
272
+ monkeypatch.setenv("VERTEX_API_KEY", "fake-vertex-key")
273
+ model_no_methods = _make_mock_model(
274
+ name="publishers/google/models/gemini-1.5-pro-002",
275
+ display_name="Gemini 1.5 Pro 002",
276
+ methods=[],
277
+ )
278
+ model_non_gemini = _make_mock_model(
279
+ name="publishers/google/models/text-bison",
280
+ display_name="Text Bison",
281
+ methods=[],
282
+ )
283
+
284
+ with patch("app.services.ai.provider_vertex_key.genai.Client") as MockClient:
285
+ MockClient.return_value.models.list.return_value = [model_no_methods, model_non_gemini]
286
+ models = VertexAPIKeyProvider().list_models()
287
+
288
+ assert len(models) == 1
289
+ assert "gemini" in models[0].model_id.lower()
290
+
291
+
292
+ def test_vertex_key_provider_generate_content_uses_vertexai(monkeypatch):
293
+ """generate_content doit aussi utiliser vertexai=True."""
294
+ monkeypatch.setenv("VERTEX_API_KEY", "fake-vertex-key")
295
+
296
+ with patch("app.services.ai.provider_vertex_key.genai.Client") as MockClient:
297
+ with patch("app.services.ai.provider_vertex_key.types.Part.from_bytes") as mock_part:
298
+ mock_part.return_value = "fake-part"
299
+ MockClient.return_value.models.generate_content.return_value.text = "result"
300
+ result = VertexAPIKeyProvider().generate_content(b"img", "prompt", "gemini-2.0-flash")
301
+
302
+ MockClient.assert_called_once_with(vertexai=True, api_key="fake-vertex-key")
303
+ assert result == "result"
304
 
305
 
306
  # ---------------------------------------------------------------------------