AI Vision: multi-provider config, model testing, per-client AI preferences
Backend: - Add ai_provider + ai_model columns to APIClient model - New admin_ai router: GET /admin/api/ai/status + POST /admin/api/ai/test - Update ai_vision.py: provider_override + model_override support - Update pipeline: load client AI prefs for image processing - ClientUpdate/ClientResponse schemas include ai_provider/ai_model Frontend: - New AIConfigSection component: provider list, model dropdowns, test button with live results (no page refresh needed) - AI types (ai.types.ts) + API (api/ai.ts) - APIClient types extended with ai_provider/ai_model
This commit is contained in:
+36
-11
@@ -114,7 +114,8 @@ async def _generate_gemini(
|
||||
prompt: str,
|
||||
image_bytes: Optional[bytes] = None,
|
||||
media_type: Optional[str] = None,
|
||||
max_tokens: int = 1024
|
||||
max_tokens: int = 1024,
|
||||
model_override: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Appel à Google Gemini via SDK avec timeout et retry."""
|
||||
if not settings.GEMINI_API_KEY:
|
||||
@@ -127,11 +128,13 @@ async def _generate_gemini(
|
||||
contents.append(types.Part.from_bytes(data=image_bytes, mime_type=media_type))
|
||||
contents.append(prompt)
|
||||
|
||||
model = model_override or settings.GEMINI_MODEL
|
||||
|
||||
async def _call():
|
||||
return await asyncio.wait_for(
|
||||
asyncio.to_thread(
|
||||
client.models.generate_content,
|
||||
model=settings.GEMINI_MODEL,
|
||||
model=model,
|
||||
contents=contents,
|
||||
config=types.GenerateContentConfig(
|
||||
max_output_tokens=max_tokens,
|
||||
@@ -154,13 +157,16 @@ async def _generate_openrouter(
|
||||
prompt: str,
|
||||
image_bytes: Optional[bytes] = None,
|
||||
media_type: Optional[str] = None,
|
||||
max_tokens: int = 1024
|
||||
max_tokens: int = 1024,
|
||||
model_override: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Appel à OpenRouter via HTTP avec timeout et retry."""
|
||||
if not settings.OPENROUTER_API_KEY:
|
||||
logger.warning("ai.openrouter.no_key")
|
||||
return {"text": None, "usage": (None, None)}
|
||||
|
||||
model = model_override or settings.OPENROUTER_MODEL
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {settings.OPENROUTER_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
@@ -184,7 +190,7 @@ async def _generate_openrouter(
|
||||
messages.append({"role": "user", "content": content_payload})
|
||||
|
||||
payload = {
|
||||
"model": settings.OPENROUTER_MODEL,
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"max_tokens": max_tokens,
|
||||
"response_format": {"type": "json_object"}
|
||||
@@ -222,16 +228,24 @@ async def _generate(
|
||||
prompt: str,
|
||||
image_bytes: Optional[bytes] = None,
|
||||
media_type: Optional[str] = None,
|
||||
max_tokens: int = 1024
|
||||
max_tokens: int = 1024,
|
||||
provider_override: Optional[str] = None,
|
||||
model_override: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Dispatcher vers le bon provider."""
|
||||
provider = settings.AI_PROVIDER.lower()
|
||||
"""Dispatcher vers le bon provider, avec override optionnel par client."""
|
||||
provider = provider_override or settings.AI_PROVIDER.lower()
|
||||
logger.info("ai.generate", extra={"provider": provider})
|
||||
|
||||
if provider == "openrouter":
|
||||
return await _generate_openrouter(prompt, image_bytes, media_type, max_tokens)
|
||||
return await _generate_openrouter(
|
||||
prompt, image_bytes, media_type, max_tokens,
|
||||
model_override=model_override,
|
||||
)
|
||||
else:
|
||||
return await _generate_gemini(prompt, image_bytes, media_type, max_tokens)
|
||||
return await _generate_gemini(
|
||||
prompt, image_bytes, media_type, max_tokens,
|
||||
model_override=model_override,
|
||||
)
|
||||
|
||||
|
||||
def _build_prompt(ocr_hint: Optional[str], language: str) -> str:
|
||||
@@ -265,18 +279,27 @@ async def analyze_image(
|
||||
file_path: str,
|
||||
ocr_hint: Optional[str] = None,
|
||||
language: str = "français",
|
||||
provider_override: Optional[str] = None,
|
||||
model_override: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Envoie l'image à l'AI pour analyse (Description + Tags).
|
||||
Supporte l'override par client pour le fournisseur/modèle.
|
||||
"""
|
||||
if not settings.AI_ENABLED:
|
||||
return {}
|
||||
|
||||
active_provider = provider_override or settings.AI_PROVIDER
|
||||
active_model = model_override or (
|
||||
settings.OPENROUTER_MODEL if active_provider == "openrouter"
|
||||
else settings.GEMINI_MODEL
|
||||
)
|
||||
|
||||
result = {
|
||||
"description": None,
|
||||
"tags": [],
|
||||
"confidence": None,
|
||||
"model": settings.OPENROUTER_MODEL if settings.AI_PROVIDER == "openrouter" else settings.GEMINI_MODEL,
|
||||
"model": active_model,
|
||||
"prompt_tokens": None,
|
||||
"output_tokens": None,
|
||||
}
|
||||
@@ -289,7 +312,9 @@ async def analyze_image(
|
||||
prompt=prompt,
|
||||
image_bytes=image_bytes,
|
||||
media_type=media_type,
|
||||
max_tokens=settings.GEMINI_MAX_TOKENS
|
||||
max_tokens=settings.GEMINI_MAX_TOKENS,
|
||||
provider_override=provider_override,
|
||||
model_override=model_override,
|
||||
)
|
||||
|
||||
text = response.get("text")
|
||||
|
||||
@@ -181,9 +181,25 @@ async def process_image_pipeline(
|
||||
try:
|
||||
logger.info("pipeline.step.start", extra={"image_id": image_id, "step": "ai", "step_num": "3/3"})
|
||||
t0 = time.time()
|
||||
|
||||
# Charger les préférences AI du client
|
||||
provider_override = None
|
||||
model_override = None
|
||||
if image.client_id:
|
||||
from app.models.client import APIClient
|
||||
client_result = await db.execute(
|
||||
select(APIClient).where(APIClient.id == image.client_id)
|
||||
)
|
||||
owner = client_result.scalar_one_or_none()
|
||||
if owner:
|
||||
provider_override = owner.ai_provider
|
||||
model_override = owner.ai_model
|
||||
|
||||
ai = await analyze_image(
|
||||
file_path=file_path,
|
||||
ocr_hint=image.ocr_text,
|
||||
provider_override=provider_override,
|
||||
model_override=model_override,
|
||||
)
|
||||
|
||||
image.ai_description = ai.get("description")
|
||||
|
||||
Reference in New Issue
Block a user