mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-07 07:55:16 +02:00
feat: allow multiple API keys (#186)
* feat: allow multiple API keys * chore: cleanup * chore: upgrade pipecat * feat: make default api_key as list
This commit is contained in:
parent
162bfabac3
commit
57e8768e0b
10 changed files with 174 additions and 137 deletions
|
|
@ -71,10 +71,10 @@ async def get_auth_user(
|
|||
|
||||
|
||||
class UserConfigurationRequestResponseSchema(BaseModel):
|
||||
llm: dict[str, Union[str, float]] | None = None
|
||||
tts: dict[str, Union[str, float]] | None = None
|
||||
stt: dict[str, Union[str, float]] | None = None
|
||||
embeddings: dict[str, Union[str, float]] | None = None
|
||||
llm: dict[str, Union[str, float, list[str]]] | None = None
|
||||
tts: dict[str, Union[str, float, list[str]]] | None = None
|
||||
stt: dict[str, Union[str, float, list[str]]] | None = None
|
||||
embeddings: dict[str, Union[str, float, list[str]]] | None = None
|
||||
test_phone_number: str | None = None
|
||||
timezone: str | None = None
|
||||
organization_pricing: dict[str, Union[float, str, bool]] | None = None
|
||||
|
|
|
|||
|
|
@ -251,18 +251,18 @@ async def create_user_configuration_with_mps_key(
|
|||
configuration = {
|
||||
"llm": {
|
||||
"provider": ServiceProviders.DOGRAH.value,
|
||||
"api_key": service_key,
|
||||
"api_key": [service_key],
|
||||
"model": "default",
|
||||
},
|
||||
"tts": {
|
||||
"provider": ServiceProviders.DOGRAH.value,
|
||||
"api_key": service_key,
|
||||
"api_key": [service_key],
|
||||
"model": "default",
|
||||
"voice": "default",
|
||||
},
|
||||
"stt": {
|
||||
"provider": ServiceProviders.DOGRAH.value,
|
||||
"api_key": service_key,
|
||||
"api_key": [service_key],
|
||||
"model": "default",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,36 @@ def is_mask_of(masked: str, real_key: str) -> bool:
|
|||
return mask_key(real_key) == masked
|
||||
|
||||
|
||||
def resolve_masked_api_keys(
|
||||
incoming: str | list[str], existing: str | list[str]
|
||||
) -> str | list[str]:
|
||||
"""Resolve masked API keys against existing real keys.
|
||||
|
||||
For each incoming key, if it matches the mask of an existing key, the real
|
||||
key is restored. New (unmasked) keys are kept as-is. This handles adds,
|
||||
removes, reorders, and partial replacements correctly.
|
||||
"""
|
||||
if isinstance(incoming, str) and isinstance(existing, str):
|
||||
return existing if is_mask_of(incoming, existing) else incoming
|
||||
|
||||
existing_list = existing if isinstance(existing, list) else [existing]
|
||||
incoming_list = incoming if isinstance(incoming, list) else [incoming]
|
||||
|
||||
resolved: list[str] = []
|
||||
used: set[int] = set()
|
||||
for key in incoming_list:
|
||||
matched = False
|
||||
for i, real in enumerate(existing_list):
|
||||
if i not in used and is_mask_of(key, real):
|
||||
resolved.append(real)
|
||||
used.add(i)
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
resolved.append(key)
|
||||
return resolved
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# High-level helpers for UserConfiguration objects
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -53,7 +83,11 @@ def _mask_service(service_cfg: Optional[ServiceConfig]) -> Optional[Dict[str, An
|
|||
# Work on a dict copy so we don't mutate original models
|
||||
data = service_cfg.model_dump()
|
||||
if "api_key" in data and data["api_key"]:
|
||||
data["api_key"] = mask_key(data["api_key"])
|
||||
raw = data["api_key"]
|
||||
if isinstance(raw, list):
|
||||
data["api_key"] = [mask_key(k) for k in raw]
|
||||
else:
|
||||
data["api_key"] = mask_key(raw)
|
||||
return data
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ stored, while honouring masked API keys.
|
|||
from typing import Dict
|
||||
|
||||
from api.schemas.user_configuration import UserConfiguration
|
||||
from api.services.configuration.masking import is_mask_of
|
||||
from api.services.configuration.masking import resolve_masked_api_keys
|
||||
|
||||
SERVICE_FIELDS = ("llm", "tts", "stt", "embeddings")
|
||||
|
||||
|
|
@ -50,12 +50,10 @@ def merge_user_configurations(
|
|||
if not provider_changed:
|
||||
# conditional preservation of api_key
|
||||
if incoming_api_key is not None:
|
||||
if (
|
||||
old_cfg
|
||||
and "api_key" in old_cfg
|
||||
and is_mask_of(incoming_api_key, old_cfg["api_key"])
|
||||
):
|
||||
incoming_cfg["api_key"] = old_cfg["api_key"]
|
||||
if old_cfg and "api_key" in old_cfg:
|
||||
incoming_cfg["api_key"] = resolve_masked_api_keys(
|
||||
incoming_api_key, old_cfg["api_key"]
|
||||
)
|
||||
else:
|
||||
if "api_key" in old_cfg:
|
||||
incoming_cfg["api_key"] = old_cfg["api_key"]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import random
|
||||
from enum import Enum, auto
|
||||
from typing import Annotated, Dict, Literal, Type, TypeVar, Union
|
||||
|
||||
from pydantic import BaseModel, Field, computed_field
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Field, computed_field, field_validator
|
||||
|
||||
|
||||
class ServiceType(Enum):
|
||||
|
|
@ -38,7 +40,29 @@ class BaseServiceConfiguration(BaseModel):
|
|||
ServiceProviders.DOGRAH,
|
||||
# ServiceProviders.SARVAM,
|
||||
]
|
||||
api_key: str
|
||||
api_key: str | list[str]
|
||||
|
||||
@field_validator("api_key")
|
||||
@classmethod
|
||||
def validate_api_key(cls, v):
|
||||
if isinstance(v, list) and len(v) == 0:
|
||||
raise ValueError("api_key list must not be empty")
|
||||
return v
|
||||
|
||||
def __getattribute__(self, name: str):
|
||||
if name == "api_key":
|
||||
value = super().__getattribute__(name)
|
||||
if isinstance(value, list):
|
||||
return random.choice(value)
|
||||
return value
|
||||
return super().__getattribute__(name)
|
||||
|
||||
def get_all_api_keys(self) -> list[str]:
|
||||
"""Get all API keys as a list (bypasses random selection)."""
|
||||
value = super().__getattribute__("api_key")
|
||||
if isinstance(value, list):
|
||||
return list(value)
|
||||
return [value]
|
||||
|
||||
|
||||
class BaseLLMConfiguration(BaseServiceConfiguration):
|
||||
|
|
@ -150,7 +174,6 @@ DOGRAH_LLM_MODELS = ["default", "accurate", "fast", "lite", "zen"]
|
|||
class OpenAILLMService(BaseLLMConfiguration):
|
||||
provider: Literal[ServiceProviders.OPENAI] = ServiceProviders.OPENAI
|
||||
model: str = Field(default="gpt-4.1", json_schema_extra={"examples": OPENAI_MODELS})
|
||||
api_key: str
|
||||
|
||||
|
||||
@register_llm
|
||||
|
|
@ -159,7 +182,6 @@ class GoogleLLMService(BaseLLMConfiguration):
|
|||
model: str = Field(
|
||||
default="gemini-2.0-flash", json_schema_extra={"examples": GOOGLE_MODELS}
|
||||
)
|
||||
api_key: str
|
||||
|
||||
|
||||
@register_llm
|
||||
|
|
@ -168,7 +190,6 @@ class GroqLLMService(BaseLLMConfiguration):
|
|||
model: str = Field(
|
||||
default="llama-3.3-70b-versatile", json_schema_extra={"examples": GROQ_MODELS}
|
||||
)
|
||||
api_key: str
|
||||
|
||||
|
||||
@register_llm
|
||||
|
|
@ -177,7 +198,7 @@ class OpenRouterLLMConfiguration(BaseLLMConfiguration):
|
|||
model: str = Field(
|
||||
default="openai/gpt-4.1", json_schema_extra={"examples": OPENROUTER_MODELS}
|
||||
)
|
||||
api_key: str
|
||||
|
||||
base_url: str = Field(default="https://openrouter.ai/api/v1")
|
||||
|
||||
|
||||
|
|
@ -187,7 +208,7 @@ class AzureLLMService(BaseLLMConfiguration):
|
|||
model: str = Field(
|
||||
default="gpt-4.1-mini", json_schema_extra={"examples": AZURE_MODELS}
|
||||
)
|
||||
api_key: str
|
||||
|
||||
endpoint: str
|
||||
|
||||
|
||||
|
|
@ -197,7 +218,6 @@ class DograhLLMService(BaseLLMConfiguration):
|
|||
model: str = Field(
|
||||
default="default", json_schema_extra={"examples": DOGRAH_LLM_MODELS}
|
||||
)
|
||||
api_key: str
|
||||
|
||||
|
||||
LLMConfig = Annotated[
|
||||
|
|
@ -219,7 +239,6 @@ LLMConfig = Annotated[
|
|||
class DeepgramTTSConfiguration(BaseServiceConfiguration):
|
||||
provider: Literal[ServiceProviders.DEEPGRAM] = ServiceProviders.DEEPGRAM
|
||||
voice: str = "aura-2-helena-en"
|
||||
api_key: str
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
|
|
@ -247,7 +266,6 @@ class ElevenlabsTTSConfiguration(BaseServiceConfiguration):
|
|||
default="eleven_flash_v2_5",
|
||||
json_schema_extra={"examples": ELEVENLABS_TTS_MODELS},
|
||||
)
|
||||
api_key: str
|
||||
|
||||
|
||||
OPENAI_TTS_MODELS = ["gpt-4o-mini-tts"]
|
||||
|
|
@ -260,7 +278,6 @@ class OpenAITTSService(BaseTTSConfiguration):
|
|||
default="gpt-4o-mini-tts", json_schema_extra={"examples": OPENAI_TTS_MODELS}
|
||||
)
|
||||
voice: str = "alloy"
|
||||
api_key: str
|
||||
|
||||
|
||||
DOGRAH_TTS_MODELS = ["default"]
|
||||
|
|
@ -274,7 +291,6 @@ class DograhTTSService(BaseTTSConfiguration):
|
|||
)
|
||||
voice: str = "default"
|
||||
speed: float = Field(default=1.0, ge=0.5, le=2.0, description="Speed of the voice")
|
||||
api_key: str
|
||||
|
||||
|
||||
CARTESIA_TTS_MODELS = ["sonic-3"]
|
||||
|
|
@ -287,7 +303,6 @@ class CartesiaTTSConfiguration(BaseTTSConfiguration):
|
|||
default="sonic-3", json_schema_extra={"examples": CARTESIA_TTS_MODELS}
|
||||
)
|
||||
voice: str = Field(default="3faa81ae-d3d8-4ab1-9e44-e50e46d33c30")
|
||||
api_key: str
|
||||
|
||||
|
||||
SARVAM_TTS_MODELS = ["bulbul:v2", "bulbul:v3"]
|
||||
|
|
@ -376,7 +391,6 @@ class SarvamTTSConfiguration(BaseTTSConfiguration):
|
|||
language: str = Field(
|
||||
default="hi-IN", json_schema_extra={"examples": SARVAM_LANGUAGES}
|
||||
)
|
||||
api_key: str
|
||||
|
||||
|
||||
TTSConfig = Annotated[
|
||||
|
|
@ -496,7 +510,6 @@ class DeepgramSTTConfiguration(BaseSTTConfiguration):
|
|||
},
|
||||
},
|
||||
)
|
||||
api_key: str
|
||||
|
||||
|
||||
CARTESIA_STT_MODELS = ["ink-whisper"]
|
||||
|
|
@ -508,7 +521,6 @@ class CartesiaSTTConfiguration(BaseSTTConfiguration):
|
|||
model: str = Field(
|
||||
default="ink-whisper", json_schema_extra={"examples": CARTESIA_STT_MODELS}
|
||||
)
|
||||
api_key: str
|
||||
|
||||
|
||||
OPENAI_STT_MODELS = ["gpt-4o-transcribe"]
|
||||
|
|
@ -520,7 +532,6 @@ class OpenAISTTConfiguration(BaseSTTConfiguration):
|
|||
model: str = Field(
|
||||
default="gpt-4o-transcribe", json_schema_extra={"examples": OPENAI_STT_MODELS}
|
||||
)
|
||||
api_key: str
|
||||
|
||||
|
||||
# Dograh STT Service
|
||||
|
|
@ -537,7 +548,6 @@ class DograhSTTService(BaseSTTConfiguration):
|
|||
language: str = Field(
|
||||
default="multi", json_schema_extra={"examples": DOGRAH_STT_LANGUAGES}
|
||||
)
|
||||
api_key: str
|
||||
|
||||
|
||||
# Sarvam STT Service
|
||||
|
|
@ -553,7 +563,6 @@ class SarvamSTTConfiguration(BaseSTTConfiguration):
|
|||
language: str = Field(
|
||||
default="hi-IN", json_schema_extra={"examples": SARVAM_LANGUAGES}
|
||||
)
|
||||
api_key: str
|
||||
|
||||
|
||||
# Speechmatics STT Service
|
||||
|
|
@ -593,7 +602,6 @@ class SpeechmaticsSTTConfiguration(BaseSTTConfiguration):
|
|||
language: str = Field(
|
||||
default="en", json_schema_extra={"examples": SPEECHMATICS_STT_LANGUAGES}
|
||||
)
|
||||
api_key: str
|
||||
|
||||
|
||||
STTConfig = Annotated[
|
||||
|
|
@ -619,7 +627,6 @@ class OpenAIEmbeddingsConfiguration(BaseEmbeddingsConfiguration):
|
|||
default="text-embedding-3-small",
|
||||
json_schema_extra={"examples": OPENAI_EMBEDDING_MODELS},
|
||||
)
|
||||
api_key: str
|
||||
|
||||
|
||||
OPENROUTER_EMBEDDING_MODELS = ["openai/text-embedding-3-small"]
|
||||
|
|
@ -632,7 +639,7 @@ class OpenRouterEmbeddingsConfiguration(BaseEmbeddingsConfiguration):
|
|||
default="openai/text-embedding-3-small",
|
||||
json_schema_extra={"examples": OPENROUTER_EMBEDDING_MODELS},
|
||||
)
|
||||
api_key: str
|
||||
|
||||
base_url: str = Field(default="https://openrouter.ai/api/v1")
|
||||
|
||||
|
||||
|
|
|
|||
2
pipecat
2
pipecat
|
|
@ -1 +1 @@
|
|||
Subproject commit efcd34192ce28530053e90aed67890644c71f1e1
|
||||
Subproject commit 10e8ded96672b08503db48c3d34e8345b11be4a2
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -202,10 +202,10 @@ export type CircuitBreakerConfigRequest = {
|
|||
};
|
||||
|
||||
export type CircuitBreakerConfigResponse = {
|
||||
enabled: boolean;
|
||||
failure_threshold: number;
|
||||
window_seconds: number;
|
||||
min_calls_in_window: number;
|
||||
enabled?: boolean;
|
||||
failure_threshold?: number;
|
||||
window_seconds?: number;
|
||||
min_calls_in_window?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -1150,16 +1150,16 @@ export type UsageHistoryResponse = {
|
|||
|
||||
export type UserConfigurationRequestResponseSchema = {
|
||||
llm?: {
|
||||
[key: string]: string | number;
|
||||
[key: string]: string | number | Array<string>;
|
||||
} | null;
|
||||
tts?: {
|
||||
[key: string]: string | number;
|
||||
[key: string]: string | number | Array<string>;
|
||||
} | null;
|
||||
stt?: {
|
||||
[key: string]: string | number;
|
||||
[key: string]: string | number | Array<string>;
|
||||
} | null;
|
||||
embeddings?: {
|
||||
[key: string]: string | number;
|
||||
[key: string]: string | number | Array<string>;
|
||||
} | null;
|
||||
test_phone_number?: string | null;
|
||||
timezone?: string | null;
|
||||
|
|
@ -1599,35 +1599,6 @@ export type HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWo
|
|||
200: unknown;
|
||||
};
|
||||
|
||||
export type HandleCloudonixAmdCallbackApiV1TelephonyCloudonixAmdCallbackWorkflowRunIdPostData = {
|
||||
body?: never;
|
||||
path: {
|
||||
workflow_run_id: number;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/telephony/cloudonix/amd-callback/{workflow_run_id}';
|
||||
};
|
||||
|
||||
export type HandleCloudonixAmdCallbackApiV1TelephonyCloudonixAmdCallbackWorkflowRunIdPostErrors = {
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: unknown;
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type HandleCloudonixAmdCallbackApiV1TelephonyCloudonixAmdCallbackWorkflowRunIdPostError = HandleCloudonixAmdCallbackApiV1TelephonyCloudonixAmdCallbackWorkflowRunIdPostErrors[keyof HandleCloudonixAmdCallbackApiV1TelephonyCloudonixAmdCallbackWorkflowRunIdPostErrors];
|
||||
|
||||
export type HandleCloudonixAmdCallbackApiV1TelephonyCloudonixAmdCallbackWorkflowRunIdPostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { Plus, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
|
|
@ -171,13 +172,19 @@ export default function ServiceConfiguration() {
|
|||
stt: "",
|
||||
embeddings: ""
|
||||
});
|
||||
const [apiKeys, setApiKeys] = useState<Record<ServiceSegment, string[]>>({
|
||||
llm: [""],
|
||||
tts: [""],
|
||||
stt: [""],
|
||||
embeddings: [""],
|
||||
});
|
||||
const [isManualModelInput, setIsManualModelInput] = useState(false);
|
||||
const [hasCheckedManualMode, setHasCheckedManualMode] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
formState: { },
|
||||
reset,
|
||||
getValues,
|
||||
setValue,
|
||||
|
|
@ -207,10 +214,24 @@ export default function ServiceConfiguration() {
|
|||
embeddings: response.data.default_providers.embeddings
|
||||
};
|
||||
|
||||
const loadedApiKeys: Record<ServiceSegment, string[]> = {
|
||||
llm: [""],
|
||||
tts: [""],
|
||||
stt: [""],
|
||||
embeddings: [""],
|
||||
};
|
||||
|
||||
const setServicePropertyValues = (service: ServiceSegment) => {
|
||||
if (userConfig?.[service]?.provider) {
|
||||
Object.entries(userConfig?.[service]).forEach(([field, value]) => {
|
||||
if (field !== "provider") {
|
||||
if (field === "api_key") {
|
||||
// Handle api_key separately — it can be string or string[]
|
||||
if (Array.isArray(value)) {
|
||||
loadedApiKeys[service] = value.length > 0 ? value : [""];
|
||||
} else {
|
||||
loadedApiKeys[service] = value ? [value as string] : [""];
|
||||
}
|
||||
} else if (field !== "provider") {
|
||||
defaultValues[`${service}_${field}`] = value;
|
||||
}
|
||||
});
|
||||
|
|
@ -236,6 +257,7 @@ export default function ServiceConfiguration() {
|
|||
// Otherwise, Radix Select sees old values that don't match new provider's enum
|
||||
// and calls onValueChange('') to clear "invalid" values
|
||||
reset(defaultValues);
|
||||
setApiKeys(loadedApiKeys);
|
||||
setServiceProviders(selectedProviders);
|
||||
};
|
||||
fetchConfigurations();
|
||||
|
|
@ -320,6 +342,7 @@ export default function ServiceConfiguration() {
|
|||
preservedValues[`${service}_provider`] = providerName;
|
||||
reset(preservedValues);
|
||||
setServiceProviders(prev => ({ ...prev, [service]: providerName }));
|
||||
setApiKeys(prev => ({ ...prev, [service]: [""] }));
|
||||
|
||||
// Reset manual model input when LLM provider changes
|
||||
if (service === "llm") {
|
||||
|
|
@ -332,23 +355,27 @@ export default function ServiceConfiguration() {
|
|||
setApiError(null);
|
||||
setIsSaving(true);
|
||||
|
||||
const userConfig: Record<ServiceSegment, Record<string, string | number>> = {
|
||||
// Collect non-empty API keys per service
|
||||
const getServiceApiKeys = (service: ServiceSegment): string[] =>
|
||||
apiKeys[service].map(k => k.trim()).filter(k => k.length > 0);
|
||||
|
||||
const userConfig: Record<ServiceSegment, Record<string, string | number | string[]>> = {
|
||||
llm: {
|
||||
provider: serviceProviders.llm,
|
||||
api_key: data.llm_api_key as string,
|
||||
api_key: getServiceApiKeys("llm"),
|
||||
model: data.llm_model as string
|
||||
},
|
||||
tts: {
|
||||
provider: serviceProviders.tts,
|
||||
api_key: data.tts_api_key as string
|
||||
api_key: getServiceApiKeys("tts"),
|
||||
},
|
||||
stt: {
|
||||
provider: serviceProviders.stt,
|
||||
api_key: data.stt_api_key as string
|
||||
api_key: getServiceApiKeys("stt"),
|
||||
},
|
||||
embeddings: {
|
||||
provider: serviceProviders.embeddings,
|
||||
api_key: data.embeddings_api_key as string,
|
||||
api_key: getServiceApiKeys("embeddings"),
|
||||
model: data.embeddings_model as string
|
||||
}
|
||||
};
|
||||
|
|
@ -359,6 +386,7 @@ export default function ServiceConfiguration() {
|
|||
const service = parts[0] as ServiceSegment;
|
||||
const field = parts.slice(1).join('_');
|
||||
|
||||
if (field === "api_key") return; // handled via apiKeys state
|
||||
if (userConfig[service] && !(field in userConfig[service])) {
|
||||
(userConfig[service] as Record<string, string>)[field] = value as string;
|
||||
}
|
||||
|
|
@ -366,10 +394,10 @@ export default function ServiceConfiguration() {
|
|||
|
||||
// Build save config - only include embeddings if api_key is provided
|
||||
const saveConfig: {
|
||||
llm: Record<string, string | number>;
|
||||
tts: Record<string, string | number>;
|
||||
stt: Record<string, string | number>;
|
||||
embeddings?: Record<string, string | number>;
|
||||
llm: Record<string, string | number | string[]>;
|
||||
tts: Record<string, string | number | string[]>;
|
||||
stt: Record<string, string | number | string[]>;
|
||||
embeddings?: Record<string, string | number | string[]>;
|
||||
} = {
|
||||
llm: userConfig.llm,
|
||||
tts: userConfig.tts,
|
||||
|
|
@ -377,7 +405,8 @@ export default function ServiceConfiguration() {
|
|||
};
|
||||
|
||||
// Only include embeddings if user has configured it (has api_key)
|
||||
if (userConfig.embeddings.api_key) {
|
||||
const embeddingsKeys = getServiceApiKeys("embeddings");
|
||||
if (embeddingsKeys.length > 0) {
|
||||
saveConfig.embeddings = userConfig.embeddings;
|
||||
}
|
||||
|
||||
|
|
@ -459,25 +488,53 @@ export default function ServiceConfiguration() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* API Key in bottom row */}
|
||||
{/* API Key(s) */}
|
||||
{currentProvider && providerSchema && providerSchema.properties.api_key && (
|
||||
<div className="space-y-2">
|
||||
<Label>API Key</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter API key"
|
||||
{...register(`${service}_api_key`, {
|
||||
// Embeddings is optional, so don't require its api_key
|
||||
required: service !== "embeddings" && providerSchema.required?.includes("api_key"),
|
||||
})}
|
||||
/>
|
||||
{errors[`${service}_api_key`] && (
|
||||
<p className="text-sm text-red-500">
|
||||
{typeof errors[`${service}_api_key`]?.message === 'string'
|
||||
? String(errors[`${service}_api_key`]?.message)
|
||||
: "This field is required"}
|
||||
</p>
|
||||
)}
|
||||
<Label>API Key(s)</Label>
|
||||
{apiKeys[service].map((key, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter API key"
|
||||
value={key}
|
||||
onChange={(e) => {
|
||||
const newKeys = [...apiKeys[service]];
|
||||
newKeys[index] = e.target.value;
|
||||
setApiKeys(prev => ({ ...prev, [service]: newKeys }));
|
||||
}}
|
||||
/>
|
||||
{apiKeys[service].length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0"
|
||||
onClick={() => {
|
||||
setApiKeys(prev => ({
|
||||
...prev,
|
||||
[service]: prev[service].filter((_, i) => i !== index),
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setApiKeys(prev => ({
|
||||
...prev,
|
||||
[service]: [...prev[service], ""],
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add API Key
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,24 +10,6 @@ import type { AuthUser } from '@/lib/auth';
|
|||
import { useAuth } from '@/lib/auth';
|
||||
|
||||
|
||||
export type SaveUserConfigFunctionParams = {
|
||||
llm?: {
|
||||
[key: string]: string | number;
|
||||
} | null;
|
||||
tts?: {
|
||||
[key: string]: string | number;
|
||||
} | null;
|
||||
stt?: {
|
||||
[key: string]: string | number;
|
||||
} | null;
|
||||
embeddings?: {
|
||||
[key: string]: string | number;
|
||||
} | null;
|
||||
test_phone_number?: string | null;
|
||||
timezone?: string | null;
|
||||
};
|
||||
|
||||
|
||||
interface TeamPermission {
|
||||
id: string;
|
||||
}
|
||||
|
|
@ -40,7 +22,7 @@ interface OrganizationPricing {
|
|||
|
||||
interface UserConfigContextType {
|
||||
userConfig: UserConfigurationRequestResponseSchema | null;
|
||||
saveUserConfig: (userConfig: SaveUserConfigFunctionParams) => Promise<void>;
|
||||
saveUserConfig: (userConfig: UserConfigurationRequestResponseSchema) => Promise<void>;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
refreshConfig: () => Promise<void>;
|
||||
|
|
@ -139,7 +121,7 @@ export function UserConfigProvider({ children }: { children: ReactNode }) {
|
|||
fetchUserConfig();
|
||||
}, [auth.loading, auth.isAuthenticated]);
|
||||
|
||||
const saveUserConfig = useCallback(async (userConfigRequest: SaveUserConfigFunctionParams) => {
|
||||
const saveUserConfig = useCallback(async (userConfigRequest: UserConfigurationRequestResponseSchema) => {
|
||||
if (!authRef.current.isAuthenticated) throw new Error('No authentication available');
|
||||
const response = await updateUserConfigurationsApiV1UserConfigurationsUserPut({
|
||||
body: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue