Compare commits

...

2 Commits

Author SHA1 Message Date
9qeklajc
83244489b3 Fix Azure Kimi routing and DB override model mapping 2026-02-10 07:25:13 +00:00
9qeklajc
cb1abc7d26 draft to fix azure issue 2026-02-09 22:15:15 +01:00
3 changed files with 174 additions and 12 deletions

View File

@@ -118,6 +118,13 @@ def create_model_mappings(
candidates: dict[str, list[tuple["Model", "BaseUpstreamProvider"]]] = {}
unique_models: dict[str, "Model"] = {}
seen_model_provider: set[tuple[str, str]] = set()
providers_by_db_id: dict[int, "BaseUpstreamProvider"] = {}
for upstream in upstreams:
db_id = getattr(upstream, "db_id", None)
if isinstance(db_id, int):
providers_by_db_id[db_id] = upstream
# Separate OpenRouter from other providers
openrouter: "BaseUpstreamProvider" | None = None
@@ -148,6 +155,9 @@ def create_model_mappings(
) -> None:
"""Process all models from a given provider."""
upstream_prefix = getattr(upstream, "upstream_name", None)
provider_key = getattr(upstream, "provider_type", "") or getattr(
upstream, "base_url", ""
)
for model in upstream.get_cached_models():
if not model.enabled or model.id in disabled_model_ids:
@@ -189,6 +199,7 @@ def create_model_mappings(
# Try to set each alias
for alias in aliases:
_add_candidate(alias, model_to_use, upstream)
seen_model_provider.add((model_to_use.id.lower(), provider_key.lower()))
# Process non-OpenRouter providers first
for upstream in other_upstreams:
@@ -198,6 +209,65 @@ def create_model_mappings(
if openrouter:
process_provider_models(openrouter, is_openrouter=True)
# Include enabled DB overrides even when provider discovery misses models.
# This is important for deployment-based providers like Azure.
for model_id, override_data in overrides_by_id.items():
if model_id in disabled_model_ids:
continue
try:
override_row, provider_fee = override_data
upstream_provider_id = getattr(override_row, "upstream_provider_id", None)
if not isinstance(upstream_provider_id, int):
continue
upstream = providers_by_db_id.get(upstream_provider_id)
if upstream is None:
continue
provider_key = getattr(upstream, "provider_type", "") or getattr(
upstream, "base_url", ""
)
dedupe_key = (model_id.lower(), provider_key.lower())
if dedupe_key in seen_model_provider:
continue
model_to_use = _row_to_model(
override_row, apply_provider_fee=True, provider_fee=provider_fee
)
if not model_to_use.enabled:
continue
base_id = get_base_model_id(model_to_use.id)
is_openrouter = (
getattr(upstream, "base_url", "") == "https://openrouter.ai/api/v1"
)
if not is_openrouter or base_id not in unique_models:
unique_model = model_to_use.copy(
update={
"id": base_id,
"upstream_provider_id": upstream.provider_type,
}
)
unique_models[base_id] = unique_model
aliases = resolve_model_alias(
model_to_use.id,
model_to_use.canonical_slug,
alias_ids=model_to_use.alias_ids,
)
upstream_prefix = getattr(upstream, "upstream_name", None)
if upstream_prefix and "/" not in model_to_use.id:
prefixed_id = f"{upstream_prefix}/{model_to_use.id}"
if prefixed_id not in aliases:
aliases.append(prefixed_id)
for alias in aliases:
_add_candidate(alias, model_to_use, upstream)
seen_model_provider.add(dedupe_key)
except Exception:
# Keep model map creation resilient to malformed overrides.
continue
# Sort candidates and build final maps
model_instances: dict[str, "Model"] = {}
provider_map: dict[str, list["BaseUpstreamProvider"]] = {}

View File

@@ -1,9 +1,14 @@
from typing import TYPE_CHECKING, Mapping
from fastapi import Request
from fastapi.responses import Response, StreamingResponse
from .base import BaseUpstreamProvider
if TYPE_CHECKING:
from ..core.db import UpstreamProviderRow
from ..auth import ApiKey
from ..core.db import AsyncSession, UpstreamProviderRow
from ..payment.models import Model
class AzureUpstreamProvider(BaseUpstreamProvider):
@@ -58,19 +63,103 @@ class AzureUpstreamProvider(BaseUpstreamProvider):
"platform_url": cls.platform_url,
}
def prepare_headers(self, request_headers: dict) -> dict:
"""Prepare headers for Azure OpenAI, adding api-key."""
headers = super().prepare_headers(request_headers)
if self.api_key:
headers["api-key"] = self.api_key
headers.pop("Authorization", None)
headers.pop("authorization", None)
return headers
def prepare_params(
self, path: str, query_params: Mapping[str, str] | None
) -> Mapping[str, str]:
"""Prepare query parameters for Azure OpenAI, adding API version.
Args:
path: Request path
query_params: Original query parameters from the client
Returns:
Query parameters dict with Azure API version added for chat completions
"""
"""Prepare query parameters for Azure OpenAI, adding API version."""
params = dict(query_params or {})
if path.endswith("chat/completions"):
params["api-version"] = self.api_version
# Ensure we use a valid Azure API version format
# Strip any hidden characters like Byte Order Marks (BOM) or whitespace
version = self.api_version.strip().replace("\ufeff", "")
if version == "v1":
version = "2024-02-15-preview"
params["api-version"] = version
return params
async def forward_request(
self,
request: Request,
path: str,
headers: dict,
request_body: bytes | None,
key: "ApiKey",
max_cost_for_model: int,
session: "AsyncSession",
model_obj: "Model",
) -> Response | StreamingResponse:
"""Forward request to Azure OpenAI."""
# Fix: If base_url contains /openai/v1, remove it
actual_base_url = self.base_url
if "/openai/v1" in actual_base_url:
actual_base_url = actual_base_url.split("/openai/v1")[0]
# Use canonical_slug as it often stores the deployment name in Azure setups
# otherwise fallback to transform_model_name
deployment_id = getattr(
model_obj, "canonical_slug", None
) or self.transform_model_name(model_obj.id)
# Ensure deployment_id doesn't contain a provider prefix (e.g., 'openai/' or 'azure/')
if "/" in deployment_id:
deployment_id = deployment_id.split("/")[-1]
# Azure format: openai/deployments/{deployment-id}/chat/completions
clean_path = path.lstrip("/")
if clean_path.startswith("v1/"):
clean_path = clean_path[3:]
azure_path = f"openai/deployments/{deployment_id}/{clean_path}"
# Temporary backup and restore base_url to use cleaned version
original_base = self.base_url
self.base_url = actual_base_url
# The query params are handled by super().forward_request via prepare_params
# We don't need to manually append them to full_url for the print if we want to be accurate
params = self.prepare_params(path, {})
full_url = (
f"{actual_base_url}/{azure_path}?api-version={params.get('api-version')}"
)
print(f"\n[DEBUG] Azure Forwarding URL: {full_url}")
print(f"[DEBUG] Deployment ID: {deployment_id}")
try:
response = await super().forward_request(
request,
azure_path,
headers,
request_body,
key,
max_cost_for_model,
session,
model_obj,
)
# Check if it's an error response to print details
if hasattr(response, "status_code") and response.status_code != 200:
print(f"[DEBUG] Azure Error Status: {response.status_code}")
if hasattr(response, "body"):
print(
f"[DEBUG] Azure Error Body: {response.body.decode() if isinstance(response.body, bytes) else response.body}"
)
return response
except Exception as e:
print(f"[DEBUG] Azure Exception: {str(e)}")
raise
finally:
self.base_url = original_base
def transform_model_name(self, model_id: str) -> str:
"""Extract deployment name from model ID."""
if "/" in model_id:
return model_id.split("/")[-1]
return model_id

View File

@@ -205,6 +205,9 @@ async def init_upstreams() -> list[BaseUpstreamProvider]:
provider = _instantiate_provider(provider_row)
if provider:
# Keep provider DB id on runtime instance so model mapping can
# bind DB overrides to the correct upstream.
setattr(provider, "db_id", provider_row.id)
await provider.refresh_models_cache()
logger.debug(
f"Initialized {provider_row.provider_type} provider",