From f2954af400e235ba3d28cbfc5db57d4b62c4ca0f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 00:27:19 +0000 Subject: [PATCH] Fix: ai_provider reads actual DB schema (config_key/config_value rows) Root cause: ai_provider.py assumed a single JSON blob in an 'encrypted_value' column, but IntegrationConfig stores each credential as a separate row with config_key + config_value columns. Fixes: - Query groups rows by provider, decrypts each config_value separately - Correct decrypt_value(secret_key, ciphertext) argument order - Filter by user_id so providers are per-user - Callers (ai_chat.py, chat()) now pass user_id through --- backend/app/routers/ai_chat.py | 2 +- backend/app/services/ai_provider.py | 34 +++++++++++++++++++---------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/backend/app/routers/ai_chat.py b/backend/app/routers/ai_chat.py index e9b7b24..96c8534 100644 --- a/backend/app/routers/ai_chat.py +++ b/backend/app/routers/ai_chat.py @@ -54,7 +54,7 @@ async def ai_status( db: AsyncSession = Depends(get_db), ): """Check which AI provider is configured.""" - provider = await ai_provider.get_active_ai_provider(db) + provider = await ai_provider.get_active_ai_provider(db, user_id=user.id) if provider: return { "configured": True, diff --git a/backend/app/services/ai_provider.py b/backend/app/services/ai_provider.py index fa1ea7a..ba6d160 100644 --- a/backend/app/services/ai_provider.py +++ b/backend/app/services/ai_provider.py @@ -55,22 +55,34 @@ RULES: - Never fabricate data — only reference what's in the context.""" -async def get_active_ai_provider(db: AsyncSession) -> dict | None: - """Find the first configured AI provider.""" - result = await db.execute( - select(IntegrationConfig).where( - IntegrationConfig.provider.in_(list(AI_PRESETS.keys())) - ) +async def get_active_ai_provider(db: AsyncSession, user_id=None) -> dict | None: + """Find the first configured AI provider for the given user.""" + query = select(IntegrationConfig).where( + IntegrationConfig.provider.in_(list(AI_PRESETS.keys())) ) - configs = result.scalars().all() + if user_id is not None: + query = query.where(IntegrationConfig.user_id == user_id) - for config in configs: - provider_name = config.provider + result = await db.execute(query) + rows = result.scalars().all() + + # Group rows by provider: {"gemini": {"api_key": "decrypted_value"}} + providers: dict[str, dict[str, str]] = {} + for row in rows: + if row.provider not in providers: + providers[row.provider] = {} try: - creds = json.loads(decrypt_value(config.encrypted_value, settings.secret_key)) + providers[row.provider][row.config_key] = decrypt_value( + settings.secret_key, row.config_value + ) except Exception: continue + # Return the first provider that has at least one credential + for provider_name, creds in providers.items(): + if not creds: + continue + preset = AI_PRESETS.get(provider_name, AI_PRESETS["custom_ai"]) api_key = creds.get("api_key", "") base_url = creds.get("endpoint_url", preset["base_url"]) or preset["base_url"] @@ -127,7 +139,7 @@ async def chat( db: AsyncSession, ) -> AsyncGenerator[str, None]: """Route chat to the configured AI provider with streaming.""" - provider = await get_active_ai_provider(db) + provider = await get_active_ai_provider(db, user_id=user_id) if not provider: yield "No AI provider configured. Go to Settings → Integrations to connect one (OpenAI, OpenRouter, Ollama, etc.)." return