Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions backend/app/contracts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@
ApiKeyStatus,
ApiKeysListResponse,
)
from .provider_types import (
ALL_PROVIDER_TYPES,
LEGACY_GEMINI,
PROVIDER_ANTHROPIC,
PROVIDER_CUSTOM,
PROVIDER_GITHUB_COPILOT,
PROVIDER_GOOGLE,
PROVIDER_OLLAMA,
PROVIDER_OLLAMA_CLOUD,
PROVIDER_OPENAI,
PROVIDER_OPENAI_COMPATIBLE,
ProviderType,
)
from .report_v3 import (
Claim,
ChangeRecommendation,
Expand Down Expand Up @@ -136,6 +149,17 @@
"RunSummary",
"SegmentReach",
"VoiceRegister",
"ALL_PROVIDER_TYPES",
"LEGACY_GEMINI",
"PROVIDER_ANTHROPIC",
"PROVIDER_CUSTOM",
"PROVIDER_GITHUB_COPILOT",
"PROVIDER_GOOGLE",
"PROVIDER_OLLAMA",
"PROVIDER_OLLAMA_CLOUD",
"PROVIDER_OPENAI",
"PROVIDER_OPENAI_COMPATIBLE",
"ProviderType",
# ReportV3 — 11 Pflichtabschnitt-DTOs
"Claim",
"ChangeRecommendation",
Expand Down
10 changes: 5 additions & 5 deletions backend/app/contracts/llm_profile_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@
from __future__ import annotations

from datetime import datetime
from typing import Literal, Optional
from typing import Optional

from pydantic import BaseModel, ConfigDict, Field

_STRICT = ConfigDict(extra="forbid", populate_by_name=True)
from .provider_types import ProviderType

ProviderLiteral = Literal["ollama", "openai", "gemini", "anthropic", "custom"]
_STRICT = ConfigDict(extra="forbid", populate_by_name=True)


class LlmProfile(BaseModel):
model_config = _STRICT

id: str = Field(..., description="UUID, server-generiert")
name: str = Field(..., min_length=1, max_length=80)
provider: ProviderLiteral
provider: ProviderType
base_url: str = Field(..., min_length=1)
model_name: str = Field(..., min_length=1)
# None = nicht gesetzt (Update lässt Feld weg). "" = explizit geleert.
Expand All @@ -39,7 +39,7 @@ class LlmProfileCreateRequest(BaseModel):
model_config = _STRICT

name: str = Field(..., min_length=1, max_length=80)
provider: ProviderLiteral
provider: ProviderType
base_url: str = Field(..., min_length=1)
model_name: str = Field(..., min_length=1)
# None = nicht gesetzt (Update lässt Feld weg). "" = explizit geleert.
Expand Down
3 changes: 2 additions & 1 deletion backend/app/contracts/llm_routing_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from typing import Dict, Literal, Optional, Any, List
from pydantic import BaseModel, ConfigDict, Field, model_validator
from .provider_types import ProviderType

_STRICT = ConfigDict(extra="forbid")

Expand Down Expand Up @@ -71,7 +72,7 @@ class ProviderDescriptor(BaseModel):

id: str
label: str
type: Literal["ollama_cloud", "openai", "google", "openai_compatible", "github_copilot"]
type: ProviderType
base_url: Optional[str] = None
api_key_ref: Optional[str] = None
supports_models_endpoint: bool = False
Expand Down
37 changes: 37 additions & 0 deletions backend/app/contracts/provider_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from typing import Literal

PROVIDER_OLLAMA = "ollama"
PROVIDER_OPENAI = "openai"
PROVIDER_GOOGLE = "google"
PROVIDER_ANTHROPIC = "anthropic"
PROVIDER_CUSTOM = "custom"
PROVIDER_OLLAMA_CLOUD = "ollama_cloud"
PROVIDER_OPENAI_COMPATIBLE = "openai_compatible"
PROVIDER_GITHUB_COPILOT = "github_copilot"

# Legacy alias
LEGACY_GEMINI = "gemini"

ProviderType = Literal[
"ollama",
"openai",
"google",
"anthropic",
"custom",
"ollama_cloud",
"openai_compatible",
"github_copilot",
"cloud",
"unknown",
]

ALL_PROVIDER_TYPES: tuple[ProviderType, ...] = (
PROVIDER_OLLAMA,
PROVIDER_OPENAI,
PROVIDER_GOOGLE,
PROVIDER_ANTHROPIC,
PROVIDER_CUSTOM,
PROVIDER_OLLAMA_CLOUD,
PROVIDER_OPENAI_COMPATIBLE,
PROVIDER_GITHUB_COPILOT,
)
Comment thread
arn0ld87 marked this conversation as resolved.
4 changes: 3 additions & 1 deletion backend/app/contracts/report_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

from pydantic import BaseModel, ConfigDict, Field

from .provider_types import ProviderType


_STRICT = ConfigDict(extra="forbid", str_strip_whitespace=True)

Expand Down Expand Up @@ -236,7 +238,7 @@ class ModelAttribution(BaseModel):
model_config = _STRICT

stage: ModelAttributionStage
provider: str = Field(min_length=1, description="z. B. 'ollama', 'openai', 'gemini'")
provider: ProviderType = Field(description="z. B. 'ollama', 'openai', 'google'")
model_id: str = Field(min_length=1, description="Backend-Modell-ID, z. B. 'qwen2.5:32b'")
prompt_tokens: int | None = Field(default=None, ge=0)
completion_tokens: int | None = Field(default=None, ge=0)
Expand Down
20 changes: 14 additions & 6 deletions backend/app/services/llm_profiles_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@
from pathlib import Path
from typing import Optional

from ..contracts.llm_profile_contract import LlmProfile, LlmProfileCreateRequest
from ..contracts import (
PROVIDER_ANTHROPIC,
PROVIDER_CUSTOM,
PROVIDER_GOOGLE,
PROVIDER_OLLAMA,
PROVIDER_OPENAI,
LlmProfile,
LlmProfileCreateRequest,
)


def _now() -> datetime:
Expand Down Expand Up @@ -77,15 +85,15 @@ def _bootstrap_profile() -> Optional[dict]:
if not model:
return None
if "openai.com" in base_url:
provider = "openai"
provider = PROVIDER_OPENAI
elif "googleapis.com" in base_url:
provider = "gemini"
provider = PROVIDER_GOOGLE
elif "anthropic.com" in base_url:
provider = "anthropic"
provider = PROVIDER_ANTHROPIC
elif any(h in base_url for h in ("localhost", "127.0.0.1", "host.docker.internal")):
provider = "ollama"
provider = PROVIDER_OLLAMA
else:
provider = "custom"
provider = PROVIDER_CUSTOM
return dict(
id=uuid.uuid4().hex,
name="Standard",
Expand Down
17 changes: 12 additions & 5 deletions backend/app/services/llm_provider_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
"""

from typing import List, Optional
from ..contracts import (
PROVIDER_GITHUB_COPILOT,
PROVIDER_GOOGLE,
PROVIDER_OLLAMA_CLOUD,
PROVIDER_OPENAI,
PROVIDER_OPENAI_COMPATIBLE,
)
from ..contracts.llm_routing_contract import ProviderDescriptor

class LlmProviderRegistry:
Expand All @@ -20,7 +27,7 @@ def get_providers(self, session_api_keys: Optional[dict] = None) -> List[Provide
ProviderDescriptor(
id="ollama_cloud",
label="Ollama (Cloud)",
type="ollama_cloud",
type=PROVIDER_OLLAMA_CLOUD,
# OpenAI-kompatibler Ollama-Cloud-Endpoint. Auth via Bearer-Token
# (``OLLAMA_API_KEY``). Doku: https://docs.ollama.com/cloud
base_url="https://ollama.com/v1",
Expand All @@ -36,7 +43,7 @@ def get_providers(self, session_api_keys: Optional[dict] = None) -> List[Provide
ProviderDescriptor(
id="openai",
label="OpenAI",
type="openai",
type=PROVIDER_OPENAI,
base_url="https://api.openai.com/v1",
api_key_ref="OPENAI_API_KEY",
supports_models_endpoint=True,
Expand All @@ -45,7 +52,7 @@ def get_providers(self, session_api_keys: Optional[dict] = None) -> List[Provide
ProviderDescriptor(
id="google",
label="Google Gemini",
type="google",
type=PROVIDER_GOOGLE,
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
api_key_ref="GOOGLE_API_KEY",
supports_models_endpoint=True,
Expand All @@ -54,15 +61,15 @@ def get_providers(self, session_api_keys: Optional[dict] = None) -> List[Provide
ProviderDescriptor(
id="openai_compatible",
label="OpenAI Compatible",
type="openai_compatible",
type=PROVIDER_OPENAI_COMPATIBLE,
api_key_ref="LLM_API_KEY",
supports_models_endpoint=True,
fallback_models=[],
),
ProviderDescriptor(
id="github_copilot",
label="GitHub Copilot",
type="github_copilot",
type=PROVIDER_GITHUB_COPILOT,
base_url="https://api.githubcopilot.com",
api_key_ref="GH_AUTH_TOKEN",
supports_models_endpoint=False,
Expand Down
23 changes: 15 additions & 8 deletions backend/app/services/llm_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,25 @@
from typing import Any, Mapping, Optional
from urllib.parse import urlparse

from ..contracts import (
LEGACY_GEMINI,
PROVIDER_CUSTOM,
PROVIDER_GOOGLE,
PROVIDER_OPENAI,
PROVIDER_OPENAI_COMPATIBLE,
)

PROVIDER_DEFAULT_BASE_URLS = {
"google": "https://generativelanguage.googleapis.com/v1beta/openai/",
"openai": "https://api.openai.com/v1",
PROVIDER_GOOGLE: "https://generativelanguage.googleapis.com/v1beta/openai/",
PROVIDER_OPENAI: "https://api.openai.com/v1",
}

_PROVIDER_ALIASES = {
"default": "default",
"server": "default",
"google": "google",
"gemini": "google",
"openai": "openai",
"google": PROVIDER_GOOGLE,
LEGACY_GEMINI: PROVIDER_GOOGLE,
"openai": PROVIDER_OPENAI,
"custom": "custom_openai",
"custom_openai": "custom_openai",
"openai_compatible": "custom_openai",
Expand All @@ -33,8 +40,8 @@
# beliebigem Format (``custom_openai``, ``ollama_cloud``, ``github_copilot``)
# stehen NICHT drin — die werden nicht validiert.
_KEY_PREFIX_BY_PROVIDER: dict[str, tuple[str, ...]] = {
"openai": ("sk-",),
"google": ("AIzaSy",),
PROVIDER_OPENAI: ("sk-",),
PROVIDER_GOOGLE: ("AIzaSy",),
}


Expand Down Expand Up @@ -116,7 +123,7 @@ def parse_runtime_llm_config(data: Mapping[str, Any]) -> RuntimeLlmConfig:
provider = _PROVIDER_ALIASES.get(provider_raw)
if provider is None:
raise ValueError(
"llm_provider.provider must be one of: default, google, openai, custom_openai"
f"llm_provider.provider must be one of: default, {PROVIDER_GOOGLE}, {PROVIDER_OPENAI}, custom_openai"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error message still contains a hardcoded string custom_openai. Since the goal of this PR is to centralize provider types, consider using a constant for this internal runtime identifier as well to ensure consistency across the codebase.

References
  1. Use a single canonical source or function to determine entity types or categories across all database operations to ensure consistency and avoid discrepancies.

)
if provider == "default":
return RuntimeLlmConfig()
Expand Down
22 changes: 15 additions & 7 deletions backend/app/services/model_catalog_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
import urllib3
from pydantic import BaseModel, ConfigDict

from ..contracts import (
PROVIDER_GOOGLE,
PROVIDER_OLLAMA_CLOUD,
PROVIDER_OPENAI,
PROVIDER_OPENAI_COMPATIBLE,
PROVIDER_GITHUB_COPILOT,
)

from ..utils.logger import get_logger

logger = get_logger("agora.model_catalog")
Expand Down Expand Up @@ -122,11 +130,11 @@ def get_models(self, provider_id: str, provider_type: str, base_url: str, api_ke

def _fetch_live(self, provider_type: str, base_url: str, api_key: Optional[str]) -> List[str]:
"""Discovery implementation per provider type."""
if provider_type == "github_copilot":
if provider_type == PROVIDER_GITHUB_COPILOT:
# Phase 1: kein Live-Discovery — statische Liste über _get_fallbacks.
return []

if provider_type == "ollama_cloud":
if provider_type == PROVIDER_OLLAMA_CLOUD:
# Ollama Cloud (ollama.com) BRAUCHT Bearer-Token am /v1/models-Endpoint.
# Vorher fehlte der Header — Ergebnis war ein stiller 401 → leere Liste
# (PR #528 Follow-up).
Expand All @@ -143,7 +151,7 @@ def _fetch_live(self, provider_type: str, base_url: str, api_key: Optional[str])
return [m["name"] for m in data.get("models", [])]
return []

if provider_type in ("openai", "google", "openai_compatible"):
if provider_type in (PROVIDER_OPENAI, PROVIDER_GOOGLE, PROVIDER_OPENAI_COMPATIBLE):
# OpenAI-shape: data[].id. Erst /models, dann /v1/models als Fallback,
# falls die base_url nicht schon /v1 enthält.
data = _http_get_json(f"{base_url.rstrip('/')}/models", api_key=api_key)
Expand All @@ -161,13 +169,13 @@ def _get_fallbacks(self, provider_type: str) -> List[str]:
# zwingend installiert (User-Bericht 2026-05-16: halluzinierte Einträge
# im Dashboard-Picker). Lieber leeres Catalog + sichtbarer Fehlerzustand
# als Ehrlichkeits-Lüge mit nicht-existenten Modellen.
if provider_type == "ollama_cloud":
if provider_type == PROVIDER_OLLAMA_CLOUD:
return []
if provider_type == "openai":
if provider_type == PROVIDER_OPENAI:
return ["gpt-4o", "gpt-4o-mini", "o1-preview"]
if provider_type == "google":
if provider_type == PROVIDER_GOOGLE:
return ["gemini-1.5-pro", "gemini-1.5-flash"]
if provider_type == "github_copilot":
if provider_type == PROVIDER_GITHUB_COPILOT:
from .llm_providers.github_copilot import GITHUB_COPILOT_MODELS
return list(GITHUB_COPILOT_MODELS)
return []
12 changes: 2 additions & 10 deletions backend/app/services/model_event_bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from typing import Any, Generator, Iterator, Literal

from pydantic import BaseModel, ConfigDict
from ..contracts import ProviderType

from ..utils.logger import get_logger

Expand Down Expand Up @@ -61,16 +62,7 @@ class ModelActiveEvent(BaseModel):
"graph",
"unknown",
]
provider: Literal[
"ollama",
"cloud",
"openai",
"google",
"ollama_cloud",
"openai_compatible",
"github_copilot",
"unknown",
]
provider: ProviderType
ts: float
extra: dict[str, Any] | None = None

Expand Down
Loading