Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 4 additions & 0 deletions backend/app/core/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Provider(str, Enum):
ANTHROPIC = "anthropic"
GOOGLE_VERTEX = "google-vertex"
WEBHOOK_SECRET = "webhook_secret"
PROXY = "proxy"


@dataclass
Expand Down Expand Up @@ -61,6 +62,9 @@ class ProviderConfig:
Provider.WEBHOOK_SECRET: ProviderConfig(
required_fields=["webhook_secret"], sensitive_fields=["webhook_secret"]
),
Provider.PROXY: ProviderConfig(
required_fields=["api_key"], sensitive_fields=["api_key"]
),
}


Expand Down
8 changes: 7 additions & 1 deletion backend/app/crud/model_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ def validate_blob_model_or_raise(session: Session, blob: ConfigBlob) -> None:
completion = blob.completion
raw_provider = completion.provider
completion_type = completion.type

# Proxy forwards the request to the client's own LLM endpoint — no model
# lookup, no provider mapping.
if completion_type == "proxy":

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 we use the constent here like:

if completion_type == PROVIDER.PROXY:

return

if raw_provider is None:
return

Expand All @@ -125,7 +131,7 @@ def validate_blob_model_or_raise(session: Session, blob: ConfigBlob) -> None:

provider = _normalize_provider(raw_provider)

model_name = (completion.params or {}).get("model")
model_name = (completion.params or {}).get("model") or None
if not model_name:
raise HTTPException(
status_code=400,
Expand Down
95 changes: 81 additions & 14 deletions backend/app/models/llm/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ class TextLLMParams(SQLModel):


class STTLLMParams(SQLModel):
model_config = {"extra": "forbid"}

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.

what it is for?

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 prevent typos or random keys from silently passing to crud layer. Currently if the user adds lorem:"ipsum" thats not a supported parameter, it silently strips that. Ideally it should throw an error at top level instead of at the provider layer. Did not restrict to TextLLMParams as there are way too many params to take care of. But STT and TTS and Proxy has a small no. of params.

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.

understood


model: str = DEFAULT_STT_MODEL
instructions: str | None = None
input_language: str | None = "auto"
Expand All @@ -86,17 +88,35 @@ class STTLLMParams(SQLModel):


class TTSLLMParams(SQLModel):
model_config = {"extra": "forbid"}

model: str = DEFAULT_TTS_MODEL
voice: str = DEFAULT_TTS_VOICE
language: str | None = None
response_format: Literal["mp3", "wav", "ogg"] | None = "wav"


KaapiLLMParams = Union[
TextLLMParams,
STTLLMParams,
TTSLLMParams,
]
class ProxyLLMParams(SQLModel):
model_config = {"extra": "forbid"}

client_llm_url: HttpUrl = Field(
...,
description=(
"HTTPS URL of the client's own LLM endpoint. Kaapi forwards the "
"(guardrail-sanitised) input here and applies output guardrails to the response."
),
)

@model_validator(mode="after")
def _require_https(self):
if self.client_llm_url.scheme != "https":
raise ValueError(
f"client_llm_url must be HTTPS, got scheme: {self.client_llm_url.scheme}"
)
return self
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated


KaapiLLMParams = Union[TextLLMParams, STTLLMParams, TTSLLMParams, ProxyLLMParams]


# Input type models for discriminated union
Expand Down Expand Up @@ -232,14 +252,16 @@ class NativeCompletionConfig(SQLModel):
Supports any LLM provider's native API format.
"""

provider: Literal[
"openai-native",
"google-native",
"sarvamai-native",
"elevenlabs-native",
"anthropic-native",
"google-vertex-native",
] = Field(
provider: (
Literal[

@vprashrex vprashrex Jun 9, 2026

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.

I will ad proxy value here in my PR. since it need migration to be added, so I will do it.

"openai-native",
"google-native",
"sarvamai-native",
"elevenlabs-native",
"anthropic-native",
"google-vertex-native",
]
) = Field(
...,
description="Native provider type (e.g., openai-native)",
)
Expand Down Expand Up @@ -306,9 +328,39 @@ def validate_params(self):
return self


class ProxyCompletionConfig(SQLModel):

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.

do we really need this ProxyCompletionConfig? can't we re-use class KaapiCompletionConfig(SQLModel)

"""
Proxy completion: Kaapi forwards the (guardrail-sanitised) input to the
client's own LLM endpoint and applies output guardrails to the response.
No upstream provider is dispatched — `provider` is fixed to "proxy" so
the discriminated union can route cleanly.
"""

provider: Literal["proxy"] = Field(
"proxy",
description=(
"Discriminator value for the proxy variant. Auto-injected when "
"type=proxy; clients may omit it."
),
)
type: Literal["proxy"] = Field(..., description="Must be 'proxy'.")
params: dict[str, Any] = Field(
...,
description="Proxy params (client_llm_url, ...)",
)

@model_validator(mode="after")
def validate_params(self):
validated = ProxyLLMParams.model_validate(self.params)
# mode="json" coerces HttpUrl → plain str so downstream consumers
# (httpx.post, urlparse) get the type they expect from params dict.
self.params = validated.model_dump(mode="json", exclude_none=True)
return self


# Discriminated union for completion configs based on provider field
CompletionConfig = Annotated[
Union[NativeCompletionConfig, KaapiCompletionConfig],
Union[NativeCompletionConfig, KaapiCompletionConfig, ProxyCompletionConfig],
Field(discriminator="provider"),
]

Expand All @@ -326,6 +378,21 @@ class ConfigBlob(SQLModel):

completion: CompletionConfig = Field(..., description="Completion configuration")

@model_validator(mode="before")
@classmethod
def _default_proxy_provider(cls, data: Any) -> Any:
"""For `type=proxy`, provider is meaningless to the caller.
Inject provider="proxy" so the CompletionConfig discriminator routes
to ProxyCompletionConfig without forcing the client to set it."""
if not isinstance(data, dict):
return data
completion = data.get("completion")
if isinstance(completion, dict) and completion.get("type") == "proxy":
existing = completion.get("provider")
if existing in (None, "proxy"):
completion["provider"] = "proxy"

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.

Same here, PROVIDER.PROXY.

return data

# used for llm-chain to provide prompt interpolation
prompt_template: PromptTemplate | None = Field(
default=None,
Expand Down
Loading
Loading