Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion backend/app/api/docs/credentials/create.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,46 @@
Persist new credentials for the current organization and project.

Credentials are encrypted and stored securely for provider integrations (OpenAI, Langfuse, etc.). Only one credential per provider is allowed per organization-project combination.
Credentials are encrypted and stored securely for provider integrations (OpenAI, Langfuse, etc.). Only one credential per provider is allowed per organization-project combination. You can send credentials for a single provider or multiple providers in one request. Refer to the examples below for the required input parameters for each provider.

### Supported Providers:
- **LLM:** openai, sarvamai, google(gemini)
- **Observability:** langfuse
- **Audio:** elevenlabs

### Examples:

#### Single Provider
```json
{
"credential": {
"openai": {
"api_key": "sk-proj-..."
}
}
}
```

#### Multiple Providers
```json
{
"credential": {
"openai": {
"api_key": "sk-proj-..."
},
"google": {
"api_key": "AIzaSy..."
},
"sarvamai": {
"api_key": "sarvam-..."
},
"elevenlabs": {
"api_key": "sk_..."
},
"langfuse": {
"public_key": "pk-lf-....",
"secret_key": "sk-lf-...",
"host": "https://cloud.langfuse.com"
}
}
}
```
20 changes: 18 additions & 2 deletions backend/app/api/docs/onboarding/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@
- We’ve also included a list of the providers currently supported by kaapi.

### Supported Providers
- **LLM:** openai
- **LLM:** openai, google, sarvamai
- **Observability:** langfuse
- **Audio:** elevenlabs

### Example: For sending multiple credentials -
```
Expand All @@ -41,9 +42,24 @@
"api_key": "sk-proj-..."
}
},
{
"google": {
"api_key": "AIzaSy..."
}
},
{
"sarvamai": {
"api_key": "sarvam-..."
}
},
{
"elevenlabs": {
"api_key": "sk_..."
}
},
{
"langfuse": {
"public_key": "pk-lf-...",
"public_key": "pk-lf-....",
"secret_key": "sk-lf-...",
"host": "https://cloud.langfuse.com"
}
Expand Down
12 changes: 10 additions & 2 deletions backend/app/api/routes/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ def read_provider_credential(
provider: str,
_current_user: AuthContextDep,
):
provider_enum = validate_provider(provider)
try:
provider_enum = validate_provider(provider)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: can we reuse APIResponse.failure_response() instead of manually raising?

Copy link
Copy Markdown
Collaborator Author

@nishika26 nishika26 Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use failure response directly for situations where we are raising 200 status code with a failure payload, this manual raising is needed for the status code and anyway the global exception handler wraps HTTPException into APIResponse.failure_response() automatically, so the client still gets a consistent APIResponse shape, just with the right non-200 status code.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the error code can be sent in the metadata. but okay we can take that up later


credential = get_provider_credential(
session=session,
org_id=_current_user.organization_.id,
Expand Down Expand Up @@ -143,7 +147,11 @@ def delete_provider_credential(
provider: str,
_current_user: AuthContextDep,
):
provider_enum = validate_provider(provider)
try:
provider_enum = validate_provider(provider)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))

remove_provider_credential(
session=session,
org_id=_current_user.organization_.id,
Expand Down
6 changes: 1 addition & 5 deletions backend/app/core/providers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from typing import Dict, List, Optional
from typing import Dict, List
from enum import Enum
from dataclasses import dataclass

Expand All @@ -10,7 +10,6 @@ class Provider(str, Enum):
"""Enumeration of supported credential providers."""

OPENAI = "openai"
AWS = "aws"
LANGFUSE = "langfuse"
GOOGLE = "google"
SARVAMAI = "sarvamai"
Expand All @@ -27,9 +26,6 @@ class ProviderConfig:
# Provider configurations
PROVIDER_CONFIGS: Dict[Provider, ProviderConfig] = {
Provider.OPENAI: ProviderConfig(required_fields=["api_key"]),
Provider.AWS: ProviderConfig(
required_fields=["access_key_id", "secret_access_key", "region"]
),
Provider.LANGFUSE: ProviderConfig(
required_fields=["secret_key", "public_key", "host"]
),
Expand Down
27 changes: 19 additions & 8 deletions backend/app/crud/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from app.core.util import now
from app.models import Credential, CredsCreate, CredsUpdate


logger = logging.getLogger(__name__)


Expand All @@ -27,9 +28,13 @@ def set_creds_for_org(
raise HTTPException(400, "No credentials provided")

for provider, credentials in creds_add.credential.items():
# Validate provider and credentials
validate_provider(provider)
validate_provider_credentials(provider, credentials)
try:
validate_provider_credentials(provider, credentials)
except ValueError as e:
logger.error(
f"[set_creds_for_org] Validation error | project_id: {project_id}, provider: {provider}, error: {str(e)}"
)
raise HTTPException(status_code=400, detail=str(e))

# Encrypt entire credentials object
encrypted_credentials = encrypt_credentials(credentials)
Expand Down Expand Up @@ -144,7 +149,10 @@ def get_provider_credential(
Raises:
HTTPException: If credentials are not found
"""
validate_provider(provider)
try:
validate_provider(provider)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))

statement = select(Credential).where(
Credential.organization_id == org_id,
Expand Down Expand Up @@ -176,8 +184,13 @@ def update_creds_for_org(
if not creds_in.provider or not creds_in.credential:
raise ValueError("Provider and credential must be provided")

validate_provider(creds_in.provider)
validate_provider_credentials(creds_in.provider, creds_in.credential)
try:
validate_provider_credentials(creds_in.provider, creds_in.credential)
except ValueError as e:
logger.error(
f"[update_creds_for_org] Validation error | organization_id: {org_id}, project_id: {project_id}, provider: {creds_in.provider}, error: {str(e)}"
)
raise HTTPException(status_code=400, detail=str(e))

# Encrypt the entire credentials object
encrypted_credentials = encrypt_credentials(creds_in.credential)
Expand Down Expand Up @@ -216,8 +229,6 @@ def remove_provider_credential(
Raises:
HTTPException: If credentials not found or deletion fails
"""
validate_provider(provider)

# Verify credentials exist before attempting delete
creds = get_provider_credential(
session=session,
Expand Down
45 changes: 18 additions & 27 deletions backend/app/models/onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,39 +90,30 @@ def _validate_credential_list(cls, v: list[dict[str, dict[str, str]]] | None):

if not isinstance(v, list):
raise TypeError(
"credential must be a list of single-key dicts (e.g., {'openai': {...}})."
"Credential must be a list of single-key dicts (e.g., {'openai': {...}})."
)

errors: list[str] = []
for item in v:
if not isinstance(item, dict):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this post filter required inside OnboardingRequest?Imo a better approach would be to decouple this validation logic from onboarding as it might be hard to maintain.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while it may be hard to maintain, this helps us catch any issue early on with the request body when it comes to credentials being given, otherwise we will only get to know about it after crud function is hit and we put the verification in that

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure we can take it up later

raise TypeError(
"Credential must be a dict with a single provider key like {'openai': {...}}."
)
if len(item) != 1:
raise ValueError(
"Credential must have exactly one provider key like {'openai': {...}}."
)

for idx, item in enumerate(v):
try:
if not isinstance(item, dict):
raise TypeError(
"must be a dict with a single provider key like {'openai': {...}}."
)
if len(item) != 1:
raise ValueError(
"must have exactly one provider key like {'openai': {...}}."
)
(provider_key,) = item.keys()
values = item[provider_key]

(provider_key,) = item.keys()
values = item[provider_key]
validate_provider(provider_key)

validate_provider(provider_key)
if not isinstance(values, dict):
raise ValueError(
f"Value for provider '{provider_key}' must be an object/dict."
)

if not isinstance(values, dict):
raise TypeError(
f"value for provider '{provider_key}' must be an object/dict."
)

validate_provider_credentials(provider_key, values)

except (TypeError, ValueError) as e:
errors.append(f"[{idx}] {e}")

if errors:
raise ValueError("credential validation failed:\n" + "\n".join(errors))
validate_provider_credentials(provider_key, values)

return v

Expand Down
Loading
Loading