mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-22 12:22:20 +00:00
Compare commits
2 Commits
bd1edcef26
...
fix/azure-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83244489b3 | ||
|
|
cb1abc7d26 |
@@ -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"]] = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user