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
2 changes: 1 addition & 1 deletion backend/app/core/cloud/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ def get_cloud_storage(session: Session, project_id: int) -> CloudStorage:
Method to create and configure a cloud storage instance.
"""
# Lazy import to avoid a top-level cycle: storage.py is imported from
# app.services.llm.providers.google_ai, which itself is wired into the
# app.services.llm.providers.google_gcp, which itself is wired into the
# provider registry that app.crud transitively pulls in.
from app.crud import get_project_by_id

Expand Down
14 changes: 14 additions & 0 deletions backend/app/core/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Provider(str, Enum):
OPENAI = "openai"
LANGFUSE = "langfuse"
GOOGLE_AISTUDIO = "google-aistudio"
GOOGLE_GCP = "google-gcp"
SARVAMAI = "sarvamai"
ELEVENLABS = "elevenlabs"
ANTHROPIC = "anthropic"
Expand Down Expand Up @@ -68,6 +69,14 @@ class GoogleCredentials(ProviderCredentialsBase):
api_key: str = Field(description="Google API key")


class GoogleGcpCredentials(ProviderCredentialsBase):
api_key: str = Field(description="Google GCP API key")
project_id: str = Field(description="GCP project ID")
location: str = Field(description="GCP region/location")
sa_key: JsonValue = Field(description="Service account key JSON")
gcs_bucket: str = Field(description="GCS bucket name")
Comment thread
coderabbitai[bot] marked this conversation as resolved.


class WebhookSecretCredentials(ProviderCredentialsBase):
webhook_secret: str = Field(
description="Shared secret used to HMAC-sign outgoing webhooks"
Expand All @@ -86,6 +95,7 @@ class ProxyCredentials(ProviderCredentialsBase):
| ElevenLabsCredentials
| AnthropicCredentials
| GoogleCredentials
| GoogleGcpCredentials
| WebhookSecretCredentials
| ProxyCredentials,
Field(
Expand Down Expand Up @@ -141,6 +151,10 @@ def required_fields(self) -> list[str]:
model=GoogleCredentials,
sensitive_fields=["api_key"],
),
Provider.GOOGLE_GCP: ProviderConfig(
model=GoogleGcpCredentials,
sensitive_fields=["api_key", "sa_key"],
),
Provider.WEBHOOK_SECRET: ProviderConfig(
model=WebhookSecretCredentials, sensitive_fields=["webhook_secret"]
),
Expand Down
23 changes: 22 additions & 1 deletion backend/app/crud/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from sqlmodel import Session, select

from app.core.exception_handlers import HTTPException
from app.core.providers import validate_provider
from app.core.providers import parse_provider_credentials, validate_provider
from app.core.security import decrypt_credentials, encrypt_credentials
from app.core.util import now
from app.models import Credential, CredsCreate, CredsUpdate
Expand Down Expand Up @@ -194,6 +194,27 @@ def update_creds_for_org(
Credential.project_id == project_id,
)
creds = session.exec(statement).one_or_none()

# Merge onto the existing credentials so a partial payload (PATCH) only
# overwrites the fields it supplies instead of dropping the rest.
merged_credential_data = credential_data
if creds and creds.credential:
merged_credential_data = {
**decrypt_credentials(creds.credential),
**credential_data,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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

# Encrypt the entire credentials object
encrypted_credentials = encrypt_credentials(merged_credential_data)

if creds is None:
# Create new credential if it doesn't exist
creds = Credential(
Expand Down
18 changes: 14 additions & 4 deletions backend/app/models/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,12 @@ class CredsUpdate(SQLModel):
provider: Provider = Field(
description="Name of the provider to update/add credentials for"
)
credential: ProviderCredentials = Field(
description="Credentials for the specified provider",
credential: CredentialPayload = Field(

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.

why you have change ProviderCredentials to CredentialPayload?

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.

This is to make sure partial updates for credentials succeed as opposed to ProviderCredentials that makes every field required even for PATCH operation. Alternative is to create partial pydantic models for each provider and take their unions.

description=(
"Credentials for the specified provider. May be a partial payload "
"(PATCH semantics) — completeness is validated after merging with "
"any existing stored credentials, not on this raw payload."
),
)
is_active: bool | None = Field(
default=None, description="Whether the credentials are active"
Expand All @@ -107,15 +111,21 @@ def _parse_credential(cls, data: object) -> object:
if isinstance(nested, dict):
credential = nested

# An empty payload has nothing to merge with an existing stored
# credential, so it is rejected here rather than deferred to the
# crud-level merge check.
Comment on lines +114 to +116

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.

can you please remove these unwated comments, becasue the code already readable.

if isinstance(credential, dict) and not credential:
parse_provider_credentials(provider_key, credential)

return {
**data,
"provider": provider_key,
"credential": parse_provider_credentials(provider_key, credential),
"credential": credential,
}

def credential_payload(self) -> CredentialPayload:
"""Credential dict for `provider`, exactly as submitted."""
return self.credential.model_dump(exclude_unset=True)
return self.credential


class Credential(CredsBase, table=True):
Expand Down
10 changes: 9 additions & 1 deletion backend/app/models/llm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Provider(StrEnum):
ELEVENLABS = "elevenlabs"
ANTHROPIC = "anthropic"
GOOGLE_AISTUDIO = "google-aistudio"
GOOGLE_GCP = "google-gcp"
PROXY = "proxy"


Expand All @@ -17,20 +18,26 @@ class Provider(StrEnum):
# instead of leaving behind stale magic strings.
STTProvider = Literal[
Provider.GOOGLE,
Provider.GOOGLE_GCP,
Provider.SARVAMAI,
Provider.ELEVENLABS,
Provider.GOOGLE_AISTUDIO,
]
TTSProvider = Literal[
Provider.GOOGLE,
Provider.GOOGLE_GCP,
Provider.SARVAMAI,
Provider.ELEVENLABS,
Provider.GOOGLE_AISTUDIO,
]
RAGProvider = Literal[Provider.OPENAI, Provider.GOOGLE_AISTUDIO]

TextProvider = Literal[
Provider.OPENAI, Provider.GOOGLE, Provider.ANTHROPIC, Provider.GOOGLE_AISTUDIO
Provider.OPENAI,
Provider.GOOGLE,
Provider.ANTHROPIC,
Provider.GOOGLE_AISTUDIO,
Provider.GOOGLE_GCP,
]

KaapiProvider = Union[TextProvider, STTProvider, TTSProvider]
Expand All @@ -46,6 +53,7 @@ class Provider(StrEnum):
"elevenlabs-native",
"anthropic-native",
"google-aistudio-native",
"google-gcp-native",
]


Expand Down
5 changes: 3 additions & 2 deletions backend/app/models/llm/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,9 @@ class KaapiTextCompletionConfig(SQLModel):
provider: TextProvider | None = Field(
default=None,
description=(
"LLM provider for text completions (openai, google, anthropic). "
"Omit to use the platform default for the type."
"LLM provider for text completions (openai, google, anthropic, "
"google-aistudio, google-gcp). Omit to use the platform default "
"for the type."
),
)
type: Literal[CompletionType.TEXT] = Field(
Expand Down
4 changes: 3 additions & 1 deletion backend/app/models/model_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ModelConfigBase(SQLModel):
"anthropic",
"proxy",
"google-aistudio",
"google-gcp",
] = Field(
default="openai",
sa_column=sa.Column(
Expand All @@ -29,12 +30,13 @@ class ModelConfigBase(SQLModel):
"anthropic",
"proxy",
"google-aistudio",
"google-gcp",
name="provider_enum",
schema="global",
create_type=False,
),
nullable=False,
comment="provider name (e.g. openai, google, sarvamai, elevenlabs, anthropic, google-aistudio, proxy)",
comment="provider name (e.g. openai, google, sarvamai, elevenlabs, anthropic, google-aistudio, google-gcp, proxy)",
),
)

Expand Down
14 changes: 14 additions & 0 deletions backend/app/services/llm/mappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,20 @@ def transform_kaapi_config_to_native(
warnings,
)

if kaapi_config.provider == Provider.GOOGLE_GCP:
# Kaapi STT/TTS param shape is identical to Google's; reuse the Google mapper.
mapped_params, warnings = map_kaapi_to_google_params(
kaapi_config.params, kaapi_config.type
)
return (
NativeCompletionConfig(
provider="google-gcp-native",
params=mapped_params,
type=kaapi_config.type,
),
warnings,
)

if kaapi_config.provider == Provider.ANTHROPIC:
if kaapi_config.type != CompletionType.TEXT:
raise ValueError(
Expand Down
2 changes: 1 addition & 1 deletion backend/app/services/llm/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from app.services.llm.providers.eleven_ai import ElevenlabsAIProvider
from app.services.llm.providers.sarvam_ai import SarvamAIProvider
from app.services.llm.providers.claude import ClaudeProvider
from app.services.llm.providers.google_ai import GoogleVertexAIProvider
from app.services.llm.providers.google_gcp import GoogleGCPProvider
from app.services.llm.providers.registry import (
LLMProvider,
get_llm_provider,
Expand Down
Loading
Loading