280 lines
9.1 KiB
Python
280 lines
9.1 KiB
Python
"""File for handling API key"""
|
|
|
|
import os
|
|
import json
|
|
import logging
|
|
|
|
from enum import Enum
|
|
from functools import lru_cache
|
|
from pydantic import SecretStr
|
|
from typing import get_args, get_origin, Union
|
|
|
|
from .constants import KEY_FILE, HIDDEN_DIR
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _secret_env_keys() -> frozenset[str]:
|
|
# Lazy import avoids cycles at import time
|
|
from deepeval.config.settings import Settings
|
|
|
|
secret_keys: set[str] = set()
|
|
for env_key, field in Settings.model_fields.items():
|
|
ann = field.annotation
|
|
if ann is SecretStr:
|
|
secret_keys.add(env_key)
|
|
continue
|
|
|
|
origin = get_origin(ann)
|
|
if origin is Union and any(a is SecretStr for a in get_args(ann)):
|
|
secret_keys.add(env_key)
|
|
|
|
return frozenset(secret_keys)
|
|
|
|
|
|
def _env_key_for_legacy_enum(key) -> str:
|
|
return getattr(key, "name", str(key))
|
|
|
|
|
|
def _is_secret_key(key) -> bool:
|
|
return _env_key_for_legacy_enum(key) in _secret_env_keys()
|
|
|
|
|
|
_WARNED_SECRET_KEYS = set()
|
|
|
|
|
|
class KeyValues(Enum):
|
|
# Confident AI
|
|
CONFIDENT_API_KEY = "confident_api_key"
|
|
CONFIDENT_BASE_URL = "confident_base_url"
|
|
CONFIDENT_REGION = "confident_region"
|
|
|
|
# Cache
|
|
LAST_TEST_RUN_LINK = "last_test_run_link"
|
|
LAST_TEST_RUN_DATA = "last_test_run_data"
|
|
|
|
|
|
class ModelKeyValues(Enum):
|
|
# General
|
|
TEMPERATURE = "TEMPERATURE"
|
|
|
|
# Anthropic
|
|
USE_ANTHROPIC_MODEL = "USE_ANTHROPIC_MODEL"
|
|
ANTHROPIC_API_KEY = "ANTHROPIC_API_KEY"
|
|
ANTHROPIC_MODEL_NAME = "ANTHROPIC_MODEL_NAME"
|
|
ANTHROPIC_COST_PER_INPUT_TOKEN = "ANTHROPIC_COST_PER_INPUT_TOKEN"
|
|
ANTHROPIC_COST_PER_OUTPUT_TOKEN = "ANTHROPIC_COST_PER_OUTPUT_TOKEN"
|
|
|
|
# AWS
|
|
AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"
|
|
AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"
|
|
# AWS Bedrock
|
|
USE_AWS_BEDROCK_MODEL = "USE_AWS_BEDROCK_MODEL"
|
|
AWS_BEDROCK_MODEL_NAME = "AWS_BEDROCK_MODEL_NAME"
|
|
AWS_BEDROCK_REGION = "AWS_BEDROCK_REGION"
|
|
AWS_BEDROCK_COST_PER_INPUT_TOKEN = "AWS_BEDROCK_COST_PER_INPUT_TOKEN"
|
|
AWS_BEDROCK_COST_PER_OUTPUT_TOKEN = "AWS_BEDROCK_COST_PER_OUTPUT_TOKEN"
|
|
|
|
# Azure Open AI
|
|
AZURE_OPENAI_API_KEY = "AZURE_OPENAI_API_KEY"
|
|
AZURE_OPENAI_ENDPOINT = "AZURE_OPENAI_ENDPOINT"
|
|
OPENAI_API_VERSION = "OPENAI_API_VERSION"
|
|
AZURE_DEPLOYMENT_NAME = "AZURE_DEPLOYMENT_NAME"
|
|
AZURE_MODEL_NAME = "AZURE_MODEL_NAME"
|
|
AZURE_MODEL_VERSION = "AZURE_MODEL_VERSION"
|
|
USE_AZURE_OPENAI = "USE_AZURE_OPENAI"
|
|
|
|
# DeepSeek
|
|
USE_DEEPSEEK_MODEL = "USE_DEEPSEEK_MODEL"
|
|
DEEPSEEK_API_KEY = "DEEPSEEK_API_KEY"
|
|
DEEPSEEK_MODEL_NAME = "DEEPSEEK_MODEL_NAME"
|
|
DEEPSEEK_COST_PER_INPUT_TOKEN = "DEEPSEEK_COST_PER_INPUT_TOKEN"
|
|
DEEPSEEK_COST_PER_OUTPUT_TOKEN = "DEEPSEEK_COST_PER_OUTPUT_TOKEN"
|
|
|
|
# Gemini
|
|
USE_GEMINI_MODEL = "USE_GEMINI_MODEL"
|
|
GOOGLE_API_KEY = "GOOGLE_API_KEY"
|
|
GEMINI_MODEL_NAME = "GEMINI_MODEL_NAME"
|
|
GOOGLE_GENAI_USE_VERTEXAI = "GOOGLE_GENAI_USE_VERTEXAI"
|
|
GOOGLE_CLOUD_PROJECT = "GOOGLE_CLOUD_PROJECT"
|
|
GOOGLE_CLOUD_LOCATION = "GOOGLE_CLOUD_LOCATION"
|
|
GOOGLE_SERVICE_ACCOUNT_KEY = "GOOGLE_SERVICE_ACCOUNT_KEY"
|
|
|
|
# Grok
|
|
USE_GROK_MODEL = "USE_GROK_MODEL"
|
|
GROK_API_KEY = "GROK_API_KEY"
|
|
GROK_MODEL_NAME = "GROK_MODEL_NAME"
|
|
GROK_COST_PER_INPUT_TOKEN = "GROK_COST_PER_INPUT_TOKEN"
|
|
GROK_COST_PER_OUTPUT_TOKEN = "GROK_COST_PER_OUTPUT_TOKEN"
|
|
|
|
# LiteLLM
|
|
USE_LITELLM = "USE_LITELLM"
|
|
LITELLM_API_KEY = "LITELLM_API_KEY"
|
|
LITELLM_MODEL_NAME = "LITELLM_MODEL_NAME"
|
|
LITELLM_API_BASE = "LITELLM_API_BASE"
|
|
LITELLM_PROXY_API_BASE = "LITELLM_PROXY_API_BASE"
|
|
LITELLM_PROXY_API_KEY = "LITELLM_PROXY_API_KEY"
|
|
|
|
# LM Studio
|
|
LM_STUDIO_API_KEY = "LM_STUDIO_API_KEY"
|
|
LM_STUDIO_MODEL_NAME = "LM_STUDIO_MODEL_NAME"
|
|
|
|
# Local Model
|
|
USE_LOCAL_MODEL = "USE_LOCAL_MODEL"
|
|
LOCAL_MODEL_API_KEY = "LOCAL_MODEL_API_KEY"
|
|
LOCAL_MODEL_NAME = "LOCAL_MODEL_NAME"
|
|
LOCAL_MODEL_BASE_URL = "LOCAL_MODEL_BASE_URL"
|
|
LOCAL_MODEL_FORMAT = "LOCAL_MODEL_FORMAT"
|
|
|
|
# Moonshot
|
|
USE_MOONSHOT_MODEL = "USE_MOONSHOT_MODEL"
|
|
MOONSHOT_API_KEY = "MOONSHOT_API_KEY"
|
|
MOONSHOT_MODEL_NAME = "MOONSHOT_MODEL_NAME"
|
|
MOONSHOT_COST_PER_INPUT_TOKEN = "MOONSHOT_COST_PER_INPUT_TOKEN"
|
|
MOONSHOT_COST_PER_OUTPUT_TOKEN = "MOONSHOT_COST_PER_OUTPUT_TOKEN"
|
|
|
|
# Ollama
|
|
OLLAMA_MODEL_NAME = "OLLAMA_MODEL_NAME"
|
|
|
|
# OpenAI
|
|
USE_OPENAI_MODEL = "USE_OPENAI_MODEL"
|
|
OPENAI_API_KEY = "OPENAI_API_KEY"
|
|
OPENAI_MODEL_NAME = "OPENAI_MODEL_NAME"
|
|
OPENAI_COST_PER_INPUT_TOKEN = "OPENAI_COST_PER_INPUT_TOKEN"
|
|
OPENAI_COST_PER_OUTPUT_TOKEN = "OPENAI_COST_PER_OUTPUT_TOKEN"
|
|
|
|
# PortKey
|
|
USE_PORTKEY_MODEL = "USE_PORTKEY_MODEL"
|
|
PORTKEY_API_KEY = "PORTKEY_API_KEY"
|
|
PORTKEY_MODEL_NAME = "PORTKEY_MODEL_NAME"
|
|
PORTKEY_BASE_URL = "PORTKEY_BASE_URL"
|
|
PORTKEY_PROVIDER_NAME = "PORTKEY_PROVIDER_NAME"
|
|
|
|
# Vertex AI
|
|
VERTEX_AI_MODEL_NAME = "VERTEX_AI_MODEL_NAME"
|
|
|
|
# VLLM
|
|
VLLM_API_KEY = "VLLM_API_KEY"
|
|
VLLM_MODEL_NAME = "VLLM_MODEL_NAME"
|
|
|
|
# OpenRouter
|
|
USE_OPENROUTER_MODEL = "USE_OPENROUTER_MODEL"
|
|
OPENROUTER_MODEL_NAME = "OPENROUTER_MODEL_NAME"
|
|
OPENROUTER_COST_PER_INPUT_TOKEN = "OPENROUTER_COST_PER_INPUT_TOKEN"
|
|
OPENROUTER_COST_PER_OUTPUT_TOKEN = "OPENROUTER_COST_PER_OUTPUT_TOKEN"
|
|
OPENROUTER_API_KEY = "OPENROUTER_API_KEY"
|
|
|
|
|
|
class EmbeddingKeyValues(Enum):
|
|
# Azure OpenAI
|
|
USE_AZURE_OPENAI_EMBEDDING = "USE_AZURE_OPENAI_EMBEDDING"
|
|
# Azure OpenAI
|
|
AZURE_EMBEDDING_MODEL_NAME = "AZURE_EMBEDDING_MODEL_NAME"
|
|
AZURE_EMBEDDING_DEPLOYMENT_NAME = "AZURE_EMBEDDING_DEPLOYMENT_NAME"
|
|
|
|
# Local
|
|
USE_LOCAL_EMBEDDINGS = "USE_LOCAL_EMBEDDINGS"
|
|
LOCAL_EMBEDDING_MODEL_NAME = "LOCAL_EMBEDDING_MODEL_NAME"
|
|
LOCAL_EMBEDDING_BASE_URL = "LOCAL_EMBEDDING_BASE_URL"
|
|
LOCAL_EMBEDDING_API_KEY = ("LOCAL_EMBEDDING_API_KEY",)
|
|
|
|
|
|
class KeyFileHandler:
|
|
def __init__(self):
|
|
self.data = {}
|
|
|
|
def _ensure_dir(self):
|
|
os.makedirs(HIDDEN_DIR, exist_ok=True)
|
|
|
|
def write_key(
|
|
self, key: Union[KeyValues, ModelKeyValues, EmbeddingKeyValues], value
|
|
):
|
|
"""Appends or updates data in the hidden file"""
|
|
|
|
# hard stop on secrets: never write to disk
|
|
if _is_secret_key(key):
|
|
logger.warning(
|
|
"%s is a secret setting, refusing to persist. "
|
|
"Keep your secrets in .env or .env.local instead.",
|
|
_env_key_for_legacy_enum(key),
|
|
)
|
|
return
|
|
|
|
try:
|
|
with open(f"{HIDDEN_DIR}/{KEY_FILE}", "r") as f:
|
|
# Load existing data
|
|
try:
|
|
self.data = json.load(f)
|
|
except json.JSONDecodeError:
|
|
# Handle corrupted JSON file
|
|
self.data = {}
|
|
except FileNotFoundError:
|
|
# If file doesn't exist, start with an empty dictionary
|
|
self.data = {}
|
|
|
|
# Update the data with the new key-value pair
|
|
self.data[key.value] = value
|
|
|
|
# Write the updated data back to the file
|
|
self._ensure_dir()
|
|
with open(f"{HIDDEN_DIR}/{KEY_FILE}", "w") as f:
|
|
json.dump(self.data, f)
|
|
|
|
def fetch_data(
|
|
self, key: Union[KeyValues, ModelKeyValues, EmbeddingKeyValues]
|
|
):
|
|
"""Fetches the data from the hidden file.
|
|
NOTE: secrets in this file are deprecated; prefer env/.env."""
|
|
try:
|
|
with open(f"{HIDDEN_DIR}/{KEY_FILE}", "r") as f:
|
|
try:
|
|
self.data = json.load(f)
|
|
except json.JSONDecodeError:
|
|
# Handle corrupted JSON file
|
|
self.data = {}
|
|
except FileNotFoundError:
|
|
# Handle the case when the file doesn't exist
|
|
self.data = {}
|
|
|
|
value = self.data.get(key.value)
|
|
|
|
# Deprecation: warn only if we're actually returning a secret
|
|
if (
|
|
value is not None
|
|
and _is_secret_key(key)
|
|
and _env_key_for_legacy_enum(key) not in _WARNED_SECRET_KEYS
|
|
):
|
|
logger.warning(
|
|
"Reading secret '%s' from legacy %s/%s. Persisting API keys in plaintext is deprecated. "
|
|
"Move this to your environment (.env / .env.local). This fallback will be removed in a future release.",
|
|
_env_key_for_legacy_enum(key),
|
|
HIDDEN_DIR,
|
|
KEY_FILE,
|
|
)
|
|
_WARNED_SECRET_KEYS.add(_env_key_for_legacy_enum(key))
|
|
|
|
return value
|
|
|
|
def remove_key(
|
|
self, key: Union[KeyValues, ModelKeyValues, EmbeddingKeyValues]
|
|
):
|
|
"""Removes the specified key from the data."""
|
|
try:
|
|
with open(f"{HIDDEN_DIR}/{KEY_FILE}", "r") as f:
|
|
try:
|
|
self.data = json.load(f)
|
|
except json.JSONDecodeError:
|
|
# Handle corrupted JSON file
|
|
self.data = {}
|
|
self.data.pop(key.value, None) # Remove the key if it exists
|
|
self._ensure_dir()
|
|
with open(f"{HIDDEN_DIR}/{KEY_FILE}", "w") as f:
|
|
json.dump(self.data, f)
|
|
except FileNotFoundError:
|
|
# Handle the case when the file doesn't exist
|
|
pass # No action needed if the file doesn't exist
|
|
|
|
|
|
KEY_FILE_HANDLER = KeyFileHandler()
|