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
1 change: 1 addition & 0 deletions core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ class ProvidersConfig(_Base):
and adding the matching :class:`~core.providers.registry.ProviderSpec`.
"""

aimlapi: ProviderConfig = Field(default_factory=ProviderConfig)
custom: ProviderConfig = Field(default_factory=ProviderConfig)
openrouter: ProviderConfig = Field(default_factory=ProviderConfig)
forge: ProviderConfig = Field(default_factory=ProviderConfig)
Expand Down
19 changes: 18 additions & 1 deletion core/providers/catalog_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,12 @@ def _catalog_request(connection: ResolvedConnection) -> tuple[str, dict[str, str
base = connection.api_base.rstrip("/")
if connection.api_key:
headers["Authorization"] = f"Bearer {connection.api_key}"
if connection.provider_name == "aimlapi":
# AI/ML API serves one directory for every endpoint family it hosts:
# 936 rows, of which only 353 are chat models — the rest are image,
# video, speech and embedding endpoints that a chat request rejects,
# and ids repeat across families. Ask for the chat surface only.
return f"{base}/models?type=openai%2Fchat-completions", headers
return f"{base}/models", headers


Expand All @@ -421,15 +427,21 @@ def _parse_model(value: dict[str, Any]) -> CatalogModel | None:
top_provider = (
value.get("top_provider") if isinstance(value.get("top_provider"), dict) else {}
)
# Some gateways (AI/ML API) nest the descriptive fields one level down
# instead of publishing OpenRouter's flat keys. Read it after the flat
# names so a provider that has both keeps winning at the top level.
info = value.get("info") if isinstance(value.get("info"), dict) else {}
context = (
value.get("context_length")
or value.get("context_window")
or value.get("max_input_tokens")
or info.get("contextLength")
or fallback.context_window
)
output = (
value.get("max_output_tokens")
or top_provider.get("max_completion_tokens")
or info.get("outputMax")
or fallback.max_output_tokens
)
supported = value.get("supported_parameters", [])
Expand All @@ -446,7 +458,12 @@ def _parse_model(value: dict[str, Any]) -> CatalogModel | None:
)
return CatalogModel(
id=model_id,
name=str(value.get("name") or value.get("display_name") or model_id),
name=str(
value.get("name")
or value.get("display_name")
or info.get("name")
or model_id
),
context_window=max(1, int(context)),
max_output_tokens=max(1, int(output)),
supported_parameters=supported_parameters,
Expand Down
30 changes: 30 additions & 0 deletions core/providers/openai_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import uuid
from collections.abc import Awaitable, Callable
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse

import json_repair
from loguru import logger
Expand Down Expand Up @@ -79,6 +80,17 @@
"HTTP-Referer": "https://github.com/HKUDS/DeepCode",
"X-Title": "DeepCode",
}
_DEFAULT_AIMLAPI_HEADERS = {
"HTTP-Referer": "https://github.com/HKUDS/DeepCode",
"X-Title": "DeepCode",
"X-AIMLAPI-Source": "agent/deepcode",
"X-AIMLAPI-Partner-ID": "part_CxcOejScJ3hI0O2cExWchiBy",
}
# Hosts that are actually AI/ML API. Attribution is keyed on the resolved
# request origin rather than on the selected template, so a user who repoints
# the ``aimlapi`` template at a proxy of their own does not hand a third party
# DeepCode's partner identity.
_AIMLAPI_HOST_SUFFIX = ".aimlapi.com"
# Per-model thinking / reasoning quirks now live declaratively in
# ``core.providers.model_compat`` (resolved via ``resolve_model_compat``);
# this module only assembles requests from the resolved value.
Expand Down Expand Up @@ -171,6 +183,22 @@ def _uses_requesty_attribution(
return bool(api_base and "requesty" in api_base.lower())


def _uses_aimlapi_attribution(
spec: "ProviderSpec | None", api_base: str | None
) -> bool:
"""Apply DeepCode attribution headers to AI/ML API requests by default.

Stricter than the two helpers above: a substring test would also fire for
a gateway that merely *fronts* AI/ML API, so the host of the resolved base
URL has to be ours. The template name only decides the case where no base
URL was resolved at all.
"""
if not api_base:
return bool(spec and spec.name == "aimlapi")
host = (urlparse(api_base.strip()).hostname or "").lower()
return host == "aimlapi.com" or host.endswith(_AIMLAPI_HOST_SUFFIX)


_RESPONSES_FAILURE_THRESHOLD = 3
_RESPONSES_PROBE_INTERVAL_S = 300 # 5 minutes

Expand Down Expand Up @@ -250,6 +278,8 @@ def __init__(
default_headers.update(_DEFAULT_OPENROUTER_HEADERS)
if _uses_requesty_attribution(spec, effective_base):
default_headers.update(_DEFAULT_REQUESTY_HEADERS)
if _uses_aimlapi_attribution(spec, effective_base):
default_headers.update(_DEFAULT_AIMLAPI_HEADERS)
if extra_headers:
default_headers.update(extra_headers)

Expand Down
15 changes: 15 additions & 0 deletions core/providers/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ def label(self) -> str:


PROVIDERS: tuple[ProviderSpec, ...] = (
ProviderSpec(
name="aimlapi",
keywords=("aimlapi",),
env_key="AIMLAPI_API_KEY",
# The vendor writes its own name lowercase, with the TLD.
display_name="aimlapi.com",
backend="openai_compat",
is_gateway=True,
detect_by_base_keyword="aimlapi",
default_api_base="https://api.aimlapi.com/v1",
# ``vendor/model`` ids like the OpenRouter-style gateways below, not
# Forge's bare ids. ``cache_control`` markers are honoured and the
# gateway reports ``cached_tokens`` back in usage.
supports_prompt_caching=True,
),
ProviderSpec(
name="custom",
keywords=(),
Expand Down
Loading