Files

1 line
33 KiB
JSON

{"repo": "calliopeai/zentinelle", "files": {"backend/zentinelle/models/ai_provider.py": {"f894fbf7204c6673d1f58cbb9166aca0daade718": {"t": 1785674492.5810127, "n": 15886, "h": "4ff31d48a8103d18", "c": "\"\"\"\nAI Provider Registry - Pre-registered vendors with capabilities.\n\nEach provider has different capabilities:\n- Managed Keys: Can we create/manage API keys via their Admin API?\n- Usage API: Can we query usage statistics?\n- Per-Key Stats: Does usage API provide per-key breakdowns?\n- Admin API: Full admin API for key lifecycle management?\n\"\"\"\nimport uuid\nfrom django.db import models\nfrom zentinelle.models.base import Tracking\n\n\nclass AIProvider(Tracking):\n \"\"\"\n Registry of AI providers and their capabilities.\n\n This is a reference table - providers are pre-registered with their\n known capabilities. When capabilities change (new API features),\n update this table.\n \"\"\"\n\n id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)\n\n # Identity\n slug = models.SlugField(unique=True, help_text='Unique identifier: openai, anthropic, etc.')\n name = models.CharField(max_length=100, help_text='Display name: OpenAI, Anthropic, etc.')\n logo_url = models.URLField(blank=True, help_text='Provider logo for UI')\n\n # Capabilities - Key Management\n supports_managed_keys = models.BooleanField(\n default=False,\n help_text='Can create/manage API keys via Admin API'\n )\n supports_key_creation = models.BooleanField(\n default=False,\n help_text='Can programmatically create new API keys'\n )\n supports_key_deletion = models.BooleanField(\n default=False,\n help_text='Can programmatically delete/revoke API keys'\n )\n supports_key_rotation = models.BooleanField(\n default=False,\n help_text='Can rotate keys without manual intervention'\n )\n supports_per_key_limits = models.BooleanField(\n default=False,\n help_text='Can set usage/budget limits per API key'\n )\n\n # Capabilities - Usage & Stats\n supports_usage_api = models.BooleanField(\n default=False,\n help_text='Provides API to query usage statistics'\n )\n supports_per_key_stats = models.BooleanField(\n default=False,\n help_text='Usage API provides per-key breakdowns'\n )\n supports_realtime_usage = models.BooleanField(\n default=False,\n help_text='Usage stats available in near real-time'\n )\n usage_delay_minutes = models.IntegerField(\n default=60,\n help_text='Typical delay before usage data is available'\n )\n\n # API Configuration\n admin_api_base_url = models.URLField(\n blank=True,\n help_text='Base URL for Admin API'\n )\n usage_api_base_url = models.URLField(\n blank=True,\n help_text='Base URL for Usage API (if different)'\n )\n api_docs_url = models.URLField(\n blank=True,\n help_text='Link to API documentation'\n )\n\n # Key format hints\n key_prefix = models.CharField(\n max_length=20,\n blank=True,\n help_text='Expected key prefix for validation: sk-, sk-ant-, etc.'\n )\n key_env_var = models.CharField(\n max_length=50,\n blank=True,\n help_text='Standard env var name: OPENAI_API_KEY, ANTHROPIC_API_KEY'\n )\n\n # Status\n is_active = models.BooleanField(\n default=True,\n help_text='Is this provider available for configuration?'\n )\n\n # Additional metadata\n notes = models.TextField(\n blank=True,\n help_text='Implementation notes, quirks, limitations'\n )\n config_schema = models.JSONField(\n default=dict,\n blank=True,\n help_text='JSON schema for provider-specific config options'\n )\n\n class Meta:\n verbose_name = 'AI Provider'\n verbose_name_plural = 'AI Providers'\n ordering = ['name']\n\n def __str__(self):\n return self.name\n\n @classmethod\n def get_by_slug(cls, slug: str) -> 'AIProvider':\n \"\"\"Get provider by slug, returns None if not found.\"\"\"\n return cls.objects.filter(slug=slug, is_active=True).first()\n\n @classmethod\n def get_managed_providers(cls):\n \"\"\"Get all providers that support managed keys.\"\"\"\n return cls.objects.filter(\n is_active=True,\n supports_managed_keys=True\n )\n\n @classmethod\n def get_usage_trackable_providers(cls):\n \"\"\"Get all providers that support usage API queries.\"\"\"\n return cls.objects.filter(\n is_active=True,\n supports_usage_api=True\n )\n\n\n# ============================================================================\n# Fixture data for initial provider setup\n# ============================================================================\n\nPROVIDER_FIXTURES = [\n {\n 'slug': 'openai',\n 'name': 'OpenAI',\n 'supports_managed_keys': True,\n 'supports_key_creation': True,\n 'supports_key_deletion': True,\n 'supports_key_rotation': True,\n 'supports_per_key_limits': True,\n 'supports_usage_api': True,\n 'supports_per_key_stats': True,\n 'supports_realtime_usage': False,\n 'usage_delay_minutes': 5,\n 'admin_api_base_url': 'https://api.openai.com/v1/organization',\n 'usage_api_base_url': 'https://api.openai.com/v1/organization/usage',\n 'api_docs_url': 'https://platform.openai.com/docs/api-reference',\n 'key_prefix': 'sk-',\n 'key_env_var': 'OPENAI_API_KEY',\n 'notes': 'Full Admin API support. Keys can have project-level isolation.',\n },\n {\n 'slug': 'anthropic',\n 'name': 'Anthropic',\n 'supports_managed_keys': True,\n 'supports_key_creation': True,\n 'supports_key_deletion': True,\n 'supports_key_rotation': True,\n 'supports_per_key_limits': True,\n 'supports_usage_api': True,\n 'supports_per_key_stats': True,\n 'supports_realtime_usage': False,\n 'usage_delay_minutes': 15,\n 'admin_api_base_url': 'https://api.anthropic.com/v1/admin',\n 'usage_api_base_url': 'https://api.anthropic.com/v1/usage',\n 'api_docs_url': 'https://docs.anthropic.com/en/api',\n 'key_prefix': 'sk-ant-',\n 'key_env_var': 'ANTHROPIC_API_KEY',\n 'notes': 'Admin API available. Workspace-level key management.',\n },\n {\n 'slug': 'together',\n 'name': 'Together AI',\n 'supports_managed_keys': True,\n 'supports_key_creation': True,\n 'supports_key_deletion': True,\n 'supports_key_rotation': False,\n 'supports_per_key_limits': True,\n 'supports_usage_api': True,\n 'supports_per_key_stats': True,\n 'supports_realtime_usage': False,\n 'usage_delay_minutes': 30,\n 'admin_api_base_url': 'https://api.together.xyz/v1',\n 'usage_api_base_url': 'https://api.together.xyz/v1/usage',\n 'api_docs_url': 'https://docs.together.ai/reference',\n 'key_prefix': '',\n 'key_env_var': 'TOGETHER_API_KEY',\n 'notes': 'API key management available. Good for open-source models.',\n },\n {\n 'slug': 'google',\n 'name': 'Google AI (Gemini)',\n 'supports_managed_keys': False,\n 'supports_key_creation': False,\n 'supports_key_deletion': False,\n 'supports_key_rotation': False,\n 'supports_per_key_limits': False,\n 'supports_usage_api': True,\n 'supports_per_key_stats': False,\n 'supports_realtime_usage': False,\n 'usage_delay_minutes': 60,\n 'admin_api_base_url': '',\n 'usage_api_base_url': '',\n 'api_docs_url': 'https://ai.google.dev/docs',\n 'key_prefix': 'AIza',\n 'key_env_var': 'GOOGLE_API_KEY',\n 'notes': 'Key management via Google Cloud Console only. Usage via Cloud Monitoring.',\n },\n {\n 'slug': 'azure_openai',\n 'name': 'Azure OpenAI',\n 'supports_managed_keys': False,\n 'supports_key_creation': False,\n 'supports_key_deletion': False,\n 'supports_key_rotation': True, # Via Azure Key Vault\n 'supports_per_key_limits': False,\n 'supports_usage_api': True,\n 'supports_per_key_stats': False,\n 'supports_realtime_usage': False,\n 'usage_delay_minutes': 60,\n 'admin_api_base_url': '',\n 'usage_api_base_url': '',\n 'api_docs_url': 'https://learn.microsoft.com/en-us/azure/ai-services/openai/',\n 'key_prefix': '',\n 'key_env_var': 'AZURE_OPENAI_API_KEY',\n 'notes': 'Key management via Azure Portal. Usage via Azure Monitor/Cost Management.',\n },\n {\n 'slug': 'aws_bedrock',\n 'name': 'AWS Bedrock',\n 'supports_managed_keys': False,\n 'supports_key_creation': False, # Uses IAM, not API keys\n 'supports_key_deletion': False,\n 'supports_key_rotation': True, # Via IAM credential rotation\n 'supports_per_key_limits': False,\n 'supports_usage_api': True,\n 'supports_per_key_stats': False,\n 'supports_realtime_usage': False,\n 'usage_delay_minutes': 60,\n 'admin_api_base_url': '',\n 'usage_api_base_url': '',\n 'api_docs_url': 'https://docs.aws.amazon.com/bedrock/',\n 'key_prefix': '',\n 'key_env_var': '',\n 'notes': 'Uses IAM authentication, not API keys. Usage via CloudWatch.',\n },\n {\n 'slug': 'cohere',\n 'name': 'Cohere',\n 'supports_managed_keys': False,\n 'supports_key_creation': False,\n 'supports_key_deletion': False,\n 'supports_key_rotation': False,\n 'supports_per_key_limits': False,\n 'supports_usage_api': True,\n 'supports_per_key_stats': False,\n 'supports_realtime_usage': False,\n 'usage_delay_minutes': 60,\n 'admin_api_base_url': '',\n 'usage_api_base_url': '',\n 'api_docs_url': 'https://docs.cohere.com/',\n 'key_prefix': '',\n 'key_env_var': 'COHERE_API_KEY',\n 'notes': 'Key management via dashboard. Usage API available.',\n },\n {\n 'slug': 'mistral',\n 'name': 'Mistral AI',\n 'supports_managed_keys': False,\n 'supports_key_creation': False,\n 'supports_key_deletion': False,\n 'supports_key_rotation': False,\n 'supports_per_key_limits': False,\n 'supports_usage_api': True,\n 'supports_per_key_stats': False,\n 'supports_realtime_usage': False,\n 'usage_delay_minutes': 30,\n 'admin_api_base_url': '',\n 'usage_api_base_url': '',\n 'api_docs_url': 'https://docs.mistral.ai/',\n 'key_prefix': '',\n 'key_env_var': 'MISTRAL_API_KEY',\n 'notes': 'Key management via console. Usage available via API.',\n },\n {\n 'slug': 'groq',\n 'name': 'Groq',\n 'supports_managed_keys': False,\n 'supports_key_creation': False,\n 'supports_key_deletion': False,\n 'supports_key_rotation': False,\n 'supports_per_key_limits': False,\n 'supports_usage_api': True,\n 'supports_per_key_stats': False,\n 'supports_realtime_usage': False,\n 'usage_delay_minutes': 30,\n 'admin_api_base_url': '',\n 'usage_api_base_url': '',\n 'api_docs_url': 'https://console.groq.com/docs/',\n 'key_prefix': 'gsk_',\n 'key_env_var': 'GROQ_API_KEY',\n 'notes': 'Fast inference. Key management via console.',\n },\n {\n 'slug': 'deepseek',\n 'name': 'DeepSeek',\n 'supports_managed_keys': False,\n 'supports_key_creation': False,\n 'supports_key_deletion': False,\n 'supports_key_rotation': False,\n 'supports_per_key_limits': False,\n 'supports_usage_api': True,\n 'supports_per_key_stats': False,\n 'supports_realtime_usage': False,\n 'usage_delay_minutes': 60,\n 'admin_api_base_url': '',\n 'usage_api_base_url': '',\n 'api_docs_url': 'https://platform.deepseek.com/docs',\n 'key_prefix': 'sk-',\n 'key_env_var': 'DEEPSEEK_API_KEY',\n 'notes': 'Cost-effective reasoning models. Basic usage API.',\n },\n {\n 'slug': 'fireworks',\n 'name': 'Fireworks AI',\n 'supports_managed_keys': True,\n 'supports_key_creation': True,\n 'supports_key_deletion': True,\n 'supports_key_rotation': False,\n 'supports_per_key_limits': True,\n 'supports_usage_api': True,\n 'supports_per_key_stats': True,\n 'supports_realtime_usage': False,\n 'usage_delay_minutes': 15,\n 'admin_api_base_url': 'https://api.fireworks.ai/v1',\n 'usage_api_base_url': 'https://api.fireworks.ai/v1/usage',\n 'api_docs_url': 'https://docs.fireworks.ai/',\n 'key_prefix': '',\n 'key_env_var': 'FIREWORKS_API_KEY',\n 'notes': 'Good API key management. Supports fine-tuned model deployment.',\n },\n {\n 'slug': 'huggingface',\n 'name': 'Hugging Face',\n 'supports_managed_keys': True,\n 'supports_key_creation': True,\n 'supports_key_deletion': True,\n 'supports_key_rotation': False,\n 'supports_per_key_limits': False,\n 'supports_usage_api': False,\n 'supports_per_key_stats': False,\n 'supports_realtime_usage': False,\n 'usage_delay_minutes': 0,\n 'admin_api_base_url': 'https://huggingface.co/api',\n 'usage_api_base_url': '',\n 'api_docs_url': 'https://huggingface.co/docs/hub/api',\n 'key_prefix': 'hf_',\n 'key_env_var': 'HUGGINGFACE_API_KEY',\n 'notes': 'Token management via API. Supports Inference Endpoints and Hub API.',\n },\n {\n 'slug': 'ai21',\n 'name': 'AI21 Labs',\n 'supports_managed_keys': False,\n 'supports_key_creation': False,\n 'supports_key_deletion': False,\n 'supports_key_rotation': False,\n 'supports_per_key_limits': False,\n 'supports_usage_api': True,\n 'supports_per_key_stats': False,\n 'supports_realtime_usage': False,\n 'usage_delay_minutes': 60,\n 'admin_api_base_url': '',\n 'usage_api_base_url': 'https://api.ai21.com/studio/v1',\n 'api_docs_url': 'https://docs.ai21.com/',\n 'key_prefix': '',\n 'key_env_var': 'AI21_API_KEY',\n 'notes': 'Key management via dashboard only. Usage API available.',\n },\n {\n 'slug': 'openrouter',\n 'name': 'OpenRouter',\n 'supports_managed_keys': True,\n 'supports_key_creation': True,\n 'supports_key_deletion': True,\n 'supports_key_rotation': False,\n 'supports_per_key_limits': True,\n 'supports_usage_api': True,\n 'supports_per_key_stats': True,\n 'supports_realtime_usage': False,\n 'usage_delay_minutes': 5,\n 'admin_api_base_url': 'https://openrouter.ai/api/v1',\n 'usage_api_base_url': 'https://openrouter.ai/api/v1',\n 'api_docs_url': 'https://openrouter.ai/docs',\n 'key_prefix': 'sk-or-',\n 'key_env_var': 'OPENROUTER_API_KEY',\n 'notes': 'Multi-provider router. Good key management and per-key usage tracking.',\n },\n {\n 'slug': 'litellm',\n 'name': 'LiteLLM Proxy',\n 'supports_managed_keys': True,\n 'supports_key_creation': True,\n 'supports_key_deletion': True,\n 'supports_key_rotation': True,\n 'supports_per_key_limits': True,\n 'supports_usage_api': True,\n 'supports_per_key_stats': True,\n 'supports_realtime_usage': True,\n 'usage_delay_minutes': 0,\n 'admin_api_base_url': '', # Self-hosted, configured at runtime\n 'usage_api_base_url': '',\n 'api_docs_url': 'https://docs.litellm.ai/',\n 'key_prefix': 'sk-',\n 'key_env_var': 'LITELLM_API_KEY',\n 'notes': 'Self-hosted proxy. Full key management via Admin API. URL configured per deployment.',\n },\n]\n\n\ndef load_provider_fixtures():\n \"\"\"Load or update provider fixtures.\"\"\"\n for data in PROVIDER_FIXTURES:\n AIProvider.objects.update_or_create(\n slug=data['slug'],\n defaults=data\n )\n"}}, "backend/zentinelle/services/provider_admin_api.py": {"f894fbf7204c6673d1f58cbb9166aca0daade718": {"t": 1785674496.1829388, "n": 16863, "h": "5c541545dbff3ee2", "c": "\"\"\"\nProvider Admin API Services - Manage API keys via provider Admin APIs.\n\nSupports:\n- OpenAI: Create/delete/list API keys, get usage per key\n- Anthropic: Create/delete/list API keys, get usage\n- Together AI: Create/delete API keys\n- Fireworks: Create/delete API keys\n\nFor providers in MANAGED mode, this service handles:\n1. Creating per-user API keys when user is provisioned\n2. Revoking keys when user is offboarded\n3. Rotating keys on schedule\n4. Querying per-key usage stats\n\"\"\"\nimport logging\nfrom abc import ABC, abstractmethod\nfrom typing import Optional, Dict, List, Any\nfrom datetime import datetime\nfrom django.conf import settings\nfrom django.utils import timezone\n\nlogger = logging.getLogger(__name__)\n\n\nclass ProviderAdminClient(ABC):\n \"\"\"Abstract base class for provider admin API clients.\"\"\"\n\n def __init__(self, admin_api_key: str):\n self.admin_api_key = admin_api_key\n\n @abstractmethod\n def create_api_key(\n self,\n name: str,\n permissions: List[str] = None,\n rate_limit: int = None,\n budget_limit: float = None,\n ) -> Dict[str, Any]:\n \"\"\"\n Create a new API key.\n\n Returns:\n {\n 'key_id': 'provider-key-id',\n 'key_value': 'sk-...', # The actual key (only returned once)\n 'key_prefix': 'sk-abc...',\n 'name': 'key-name',\n 'created_at': datetime,\n }\n \"\"\"\n pass\n\n @abstractmethod\n def delete_api_key(self, key_id: str) -> bool:\n \"\"\"Delete/revoke an API key.\"\"\"\n pass\n\n @abstractmethod\n def list_api_keys(self) -> List[Dict[str, Any]]:\n \"\"\"List all API keys.\"\"\"\n pass\n\n @abstractmethod\n def get_key_usage(\n self,\n key_id: str,\n start_date: datetime,\n end_date: datetime,\n ) -> Dict[str, Any]:\n \"\"\"\n Get usage statistics for a specific key.\n\n Returns:\n {\n 'requests': int,\n 'input_tokens': int,\n 'output_tokens': int,\n 'cost_usd': float,\n }\n \"\"\"\n pass\n\n @abstractmethod\n def get_total_usage(\n self,\n start_date: datetime,\n end_date: datetime,\n ) -> Dict[str, Any]:\n \"\"\"Get total usage across all keys.\"\"\"\n pass\n\n\nclass OpenAIAdminClient(ProviderAdminClient):\n \"\"\"\n OpenAI Admin API client.\n\n Docs: https://platform.openai.com/docs/api-reference/organization\n \"\"\"\n\n BASE_URL = 'https://api.openai.com/v1'\n\n def create_api_key(\n self,\n name: str,\n permissions: List[str] = None,\n rate_limit: int = None,\n budget_limit: float = None,\n ) -> Dict[str, Any]:\n import requests\n\n # OpenAI uses project-based API keys\n # First, we may need to create a project or use an existing one\n\n headers = {\n 'Authorization': f'Bearer {self.admin_api_key}',\n 'Content-Type': 'application/json',\n 'OpenAI-Organization': self._get_org_id(),\n }\n\n # Create API key\n payload = {\n 'name': name,\n }\n\n if permissions:\n payload['permissions'] = permissions\n\n response = requests.post(\n f'{self.BASE_URL}/organization/api_keys',\n json=payload,\n headers=headers,\n timeout=30,\n )\n response.raise_for_status()\n data = response.json()\n\n return {\n 'key_id': data['id'],\n 'key_value': data.get('key'), # Only returned on creation\n 'key_prefix': data.get('redacted_key', data.get('key', '')[:10] + '...'),\n 'name': data['name'],\n 'created_at': datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')),\n }\n\n def delete_api_key(self, key_id: str) -> bool:\n import requests\n\n headers = {\n 'Authorization': f'Bearer {self.admin_api_key}',\n 'OpenAI-Organization': self._get_org_id(),\n }\n\n response = requests.delete(\n f'{self.BASE_URL}/organization/api_keys/{key_id}',\n headers=headers,\n timeout=30,\n )\n\n return response.status_code == 200\n\n def list_api_keys(self) -> List[Dict[str, Any]]:\n import requests\n\n headers = {\n 'Authorization': f'Bearer {self.admin_api_key}',\n 'OpenAI-Organization': self._get_org_id(),\n }\n\n response = requests.get(\n f'{self.BASE_URL}/organization/api_keys',\n headers=headers,\n timeout=30,\n )\n response.raise_for_status()\n data = response.json()\n\n return [\n {\n 'key_id': key['id'],\n 'name': key['name'],\n 'key_prefix': key.get('redacted_key', ''),\n 'created_at': datetime.fromisoformat(key['created_at'].replace('Z', '+00:00')),\n }\n for key in data.get('data', [])\n ]\n\n def get_key_usage(\n self,\n key_id: str,\n start_date: datetime,\n end_date: datetime,\n ) -> Dict[str, Any]:\n import requests\n\n headers = {\n 'Authorization': f'Bearer {self.admin_api_key}',\n 'OpenAI-Organization': self._get_org_id(),\n }\n\n params = {\n 'start_date': start_date.strftime('%Y-%m-%d'),\n 'end_date': end_date.strftime('%Y-%m-%d'),\n 'api_key_id': key_id,\n }\n\n response = requests.get(\n f'{self.BASE_URL}/organization/usage',\n params=params,\n headers=headers,\n timeout=30,\n )\n response.raise_for_status()\n data = response.json()\n\n # Aggregate usage data\n total_requests = 0\n total_input_tokens = 0\n total_output_tokens = 0\n total_cost = 0.0\n\n for bucket in data.get('data', []):\n total_requests += bucket.get('n_requests', 0)\n total_input_tokens += bucket.get('n_context_tokens_total', 0)\n total_output_tokens += bucket.get('n_generated_tokens_total', 0)\n total_cost += bucket.get('cost_in_major_unit', 0.0)\n\n return {\n 'requests': total_requests,\n 'input_tokens': total_input_tokens,\n 'output_tokens': total_output_tokens,\n 'cost_usd': total_cost,\n }\n\n def get_total_usage(\n self,\n start_date: datetime,\n end_date: datetime,\n ) -> Dict[str, Any]:\n import requests\n\n headers = {\n 'Authorization': f'Bearer {self.admin_api_key}',\n 'OpenAI-Organization': self._get_org_id(),\n }\n\n params = {\n 'start_date': start_date.strftime('%Y-%m-%d'),\n 'end_date': end_date.strftime('%Y-%m-%d'),\n }\n\n response = requests.get(\n f'{self.BASE_URL}/organization/usage',\n params=params,\n headers=headers,\n timeout=30,\n )\n response.raise_for_status()\n data = response.json()\n\n total_requests = 0\n total_input_tokens = 0\n total_output_tokens = 0\n total_cost = 0.0\n\n for bucket in data.get('data', []):\n total_requests += bucket.get('n_requests', 0)\n total_input_tokens += bucket.get('n_context_tokens_total', 0)\n total_output_tokens += bucket.get('n_generated_tokens_total', 0)\n total_cost += bucket.get('cost_in_major_unit', 0.0)\n\n return {\n 'requests': total_requests,\n 'input_tokens': total_input_tokens,\n 'output_tokens': total_output_tokens,\n 'cost_usd': total_cost,\n }\n\n def _get_org_id(self) -> str:\n \"\"\"Get OpenAI organization ID from settings or key.\"\"\"\n return getattr(settings, 'OPENAI_ORG_ID', '')\n\n\nclass AnthropicAdminClient(ProviderAdminClient):\n \"\"\"\n Anthropic Admin API client.\n\n Docs: https://docs.anthropic.com/en/api/admin-api\n \"\"\"\n\n BASE_URL = 'https://api.anthropic.com/v1'\n\n def create_api_key(\n self,\n name: str,\n permissions: List[str] = None,\n rate_limit: int = None,\n budget_limit: float = None,\n ) -> Dict[str, Any]:\n import requests\n\n headers = {\n 'x-api-key': self.admin_api_key,\n 'anthropic-version': '2023-06-01',\n 'Content-Type': 'application/json',\n }\n\n payload = {\n 'name': name,\n }\n\n # Anthropic supports workspace-level limits\n if rate_limit:\n payload['rate_limit'] = rate_limit\n if budget_limit:\n payload['spend_limit'] = budget_limit\n\n response = requests.post(\n f'{self.BASE_URL}/admin/api_keys',\n json=payload,\n headers=headers,\n timeout=30,\n )\n response.raise_for_status()\n data = response.json()\n\n return {\n 'key_id': data['id'],\n 'key_value': data.get('key'),\n 'key_prefix': data.get('partial_key', data.get('key', '')[:15] + '...'),\n 'name': data['name'],\n 'created_at': datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')),\n }\n\n def delete_api_key(self, key_id: str) -> bool:\n import requests\n\n headers = {\n 'x-api-key': self.admin_api_key,\n 'anthropic-version': '2023-06-01',\n }\n\n response = requests.delete(\n f'{self.BASE_URL}/admin/api_keys/{key_id}',\n headers=headers,\n timeout=30,\n )\n\n return response.status_code in (200, 204)\n\n def list_api_keys(self) -> List[Dict[str, Any]]:\n import requests\n\n headers = {\n 'x-api-key': self.admin_api_key,\n 'anthropic-version': '2023-06-01',\n }\n\n response = requests.get(\n f'{self.BASE_URL}/admin/api_keys',\n headers=headers,\n timeout=30,\n )\n response.raise_for_status()\n data = response.json()\n\n return [\n {\n 'key_id': key['id'],\n 'name': key['name'],\n 'key_prefix': key.get('partial_key', ''),\n 'created_at': datetime.fromisoformat(key['created_at'].replace('Z', '+00:00')),\n }\n for key in data.get('data', [])\n ]\n\n def get_key_usage(\n self,\n key_id: str,\n start_date: datetime,\n end_date: datetime,\n ) -> Dict[str, Any]:\n import requests\n\n headers = {\n 'x-api-key': self.admin_api_key,\n 'anthropic-version': '2023-06-01',\n }\n\n params = {\n 'start_time': start_date.isoformat(),\n 'end_time': end_date.isoformat(),\n 'api_key_id': key_id,\n }\n\n response = requests.get(\n f'{self.BASE_URL}/admin/usage',\n params=params,\n headers=headers,\n timeout=30,\n )\n response.raise_for_status()\n data = response.json()\n\n return {\n 'requests': data.get('request_count', 0),\n 'input_tokens': data.get('input_tokens', 0),\n 'output_tokens': data.get('output_tokens', 0),\n 'cost_usd': data.get('cost', 0.0),\n }\n\n def get_total_usage(\n self,\n start_date: datetime,\n end_date: datetime,\n ) -> Dict[str, Any]:\n import requests\n\n headers = {\n 'x-api-key': self.admin_api_key,\n 'anthropic-version': '2023-06-01',\n }\n\n params = {\n 'start_time': start_date.isoformat(),\n 'end_time': end_date.isoformat(),\n }\n\n response = requests.get(\n f'{self.BASE_URL}/admin/usage',\n params=params,\n headers=headers,\n timeout=30,\n )\n response.raise_for_status()\n data = response.json()\n\n return {\n 'requests': data.get('request_count', 0),\n 'input_tokens': data.get('input_tokens', 0),\n 'output_tokens': data.get('output_tokens', 0),\n 'cost_usd': data.get('cost', 0.0),\n }\n\n\nclass TogetherAIAdminClient(ProviderAdminClient):\n \"\"\"\n Together AI API client for key management.\n\n Docs: https://docs.together.ai/reference\n \"\"\"\n\n BASE_URL = 'https://api.together.xyz/v1'\n\n def create_api_key(\n self,\n name: str,\n permissions: List[str] = None,\n rate_limit: int = None,\n budget_limit: float = None,\n ) -> Dict[str, Any]:\n import requests\n\n headers = {\n 'Authorization': f'Bearer {self.admin_api_key}',\n 'Content-Type': 'application/json',\n }\n\n payload = {'name': name}\n\n response = requests.post(\n f'{self.BASE_URL}/api-keys',\n json=payload,\n headers=headers,\n timeout=30,\n )\n response.raise_for_status()\n data = response.json()\n\n return {\n 'key_id': data['id'],\n 'key_value': data.get('key'),\n 'key_prefix': data.get('key', '')[:10] + '...',\n 'name': data['name'],\n 'created_at': timezone.now(),\n }\n\n def delete_api_key(self, key_id: str) -> bool:\n import requests\n\n headers = {\n 'Authorization': f'Bearer {self.admin_api_key}',\n }\n\n response = requests.delete(\n f'{self.BASE_URL}/api-keys/{key_id}',\n headers=headers,\n timeout=30,\n )\n\n return response.status_code in (200, 204)\n\n def list_api_keys(self) -> List[Dict[str, Any]]:\n import requests\n\n headers = {\n 'Authorization': f'Bearer {self.admin_api_key}',\n }\n\n response = requests.get(\n f'{self.BASE_URL}/api-keys',\n headers=headers,\n timeout=30,\n )\n response.raise_for_status()\n data = response.json()\n\n return [\n {\n 'key_id': key['id'],\n 'name': key['name'],\n 'key_prefix': key.get('prefix', ''),\n 'created_at': timezone.now(),\n }\n for key in data.get('data', [])\n ]\n\n def get_key_usage(\n self,\n key_id: str,\n start_date: datetime,\n end_date: datetime,\n ) -> Dict[str, Any]:\n # Together AI usage API - query by key\n import requests\n\n headers = {\n 'Authorization': f'Bearer {self.admin_api_key}',\n }\n\n params = {\n 'start_date': start_date.strftime('%Y-%m-%d'),\n 'end_date': end_date.strftime('%Y-%m-%d'),\n 'api_key_id': key_id,\n }\n\n response = requests.get(\n f'{self.BASE_URL}/usage',\n params=params,\n headers=headers,\n timeout=30,\n )\n response.raise_for_status()\n data = response.json()\n\n return {\n 'requests': data.get('requests', 0),\n 'input_tokens': data.get('prompt_tokens', 0),\n 'output_tokens': data.get('completion_tokens', 0),\n 'cost_usd': data.get('cost', 0.0),\n }\n\n def get_total_usage(\n self,\n start_date: datetime,\n end_date: datetime,\n ) -> Dict[str, Any]:\n import requests\n\n headers = {\n 'Authorization': f'Bearer {self.admin_api_key}',\n }\n\n params = {\n 'start_date': start_date.strftime('%Y-%m-%d'),\n 'end_date': end_date.strftime('%Y-%m-%d'),\n }\n\n response = requests.get(\n f'{self.BASE_URL}/usage',\n params=params,\n headers=headers,\n timeout=30,\n )\n response.raise_for_status()\n data = response.json()\n\n return {\n 'requests': data.get('requests', 0),\n 'input_tokens': data.get('prompt_tokens', 0),\n 'output_tokens': data.get('completion_tokens', 0),\n 'cost_usd': data.get('cost', 0.0),\n }\n\n\n# =============================================================================\n# Factory and Service Layer\n# =============================================================================\n\ndef get_provider_client(provider_slug: str, admin_api_key: str) -> Optional[ProviderAdminClient]:\n \"\"\"\n Factory function to get the appropriate provider client.\n \"\"\"\n clients = {\n 'openai': OpenAIAdminClient,\n 'anthropic': AnthropicAdminClient,\n 'together': TogetherAIAdminClient,\n }\n\n client_class = clients.get(provider_slug)\n if client_class:\n return client_class(admin_api_key)\n\n logger.warning(f\"No admin client available for provider: {provider_slug}\")\n return None\n"}}}}