From 09bc238378d67808b5fa39f3d8fb81f47e8b90ab Mon Sep 17 00:00:00 2001 From: aimlapi Date: Thu, 3 Sep 2026 05:02:07 +0500 Subject: [PATCH 1/5] feat: add aimlapi.com as an OpenAI-compatible gateway provider DeepTutor could already reach AI/ML API only by typing a "custom" profile by hand: the endpoint, the env var and the capability flags all had to be rediscovered by each user, and nothing in the registry told the config loader that an https://api.aimlapi.com/v1 base belongs to a gateway that routes any model. Registering it once is what makes env resolution, api_base detection and the status display work the way they already do for the other aggregators in the same block. No detect_by_key_prefix: AI/ML API keys carry no distinguishing prefix, so the endpoint is the only signal that will not misfire on someone else's key. No strip_model_prefix: model ids keep their vendor prefix ("openai/gpt-4o-mini"), unlike aihubmix. The wizard fallback ids were each checked against GET https://api.aimlapi.com/v1/models (2026-09-03), filtered to type == "openai/chat-completions" and matched against both ids and aliases, rather than copied from another aggregator's list. --- deeptutor/services/llm/capabilities.py | 8 +++++++ deeptutor/services/provider_registry.py | 17 ++++++++++++++ deeptutor_cli/init_wizard.py | 8 +++++++ tests/services/test_provider_registry.py | 29 +++++++++++++++++++++++- 4 files changed, 61 insertions(+), 1 deletion(-) diff --git a/deeptutor/services/llm/capabilities.py b/deeptutor/services/llm/capabilities.py index 06f8ee1fc0..f5e6d2a8b1 100644 --- a/deeptutor/services/llm/capabilities.py +++ b/deeptutor/services/llm/capabilities.py @@ -154,6 +154,14 @@ "supports_vision": True, # Depends on underlying model "system_in_messages": True, }, + # AI/ML API (aggregator, OpenAI-compatible chat completions) + "aimlapi": { + "supports_response_format": True, # Depends on underlying model + "supports_streaming": True, + "supports_tools": True, + "supports_vision": True, # Depends on underlying model + "system_in_messages": True, + }, # OrcaRouter (aggregator, generally OpenAI-compatible) "orcarouter": { "supports_response_format": True, # Depends on underlying model diff --git a/deeptutor/services/provider_registry.py b/deeptutor/services/provider_registry.py index 8b035b9b21..d480440adf 100644 --- a/deeptutor/services/provider_registry.py +++ b/deeptutor/services/provider_registry.py @@ -151,6 +151,8 @@ def label(self) -> str: "atlas_cloud": "atlascloud", "atlas-cloud": "atlascloud", "eden_ai": "edenai", + "aiml": "aimlapi", + "aiml_api": "aimlapi", "novita_ai": "novita", "orca_router": "orcarouter", "orca-router": "orcarouter", @@ -274,6 +276,21 @@ def canonical_provider_name(name: str | None) -> str | None: detect_by_base_keyword="atlascloud", default_api_base="https://api.atlascloud.ai/v1", ), + # AI/ML API issues opaque keys with no distinguishing prefix, so the + # endpoint is the only reliable signal — hence no detect_by_key_prefix. + # Model ids keep their vendor prefix ("openai/gpt-4o-mini"), so no + # strip_model_prefix either. Only POST /v1/chat/completions and + # /v1/responses exist; there is no /v1/completions endpoint. + ProviderSpec( + name="aimlapi", + keywords=("aimlapi",), + env_key="AIMLAPI_API_KEY", + display_name="aimlapi.com", + backend="openai_compat", + is_gateway=True, + detect_by_base_keyword="aimlapi", + default_api_base="https://api.aimlapi.com/v1", + ), ProviderSpec( name="volcengine", keywords=("volcengine", "volces", "ark"), diff --git a/deeptutor_cli/init_wizard.py b/deeptutor_cli/init_wizard.py index 740062f69c..b401e283a5 100644 --- a/deeptutor_cli/init_wizard.py +++ b/deeptutor_cli/init_wizard.py @@ -74,6 +74,14 @@ "anthropic/claude-sonnet-4-6", "deepseek/deepseek-chat", ), + # Verified against GET https://api.aimlapi.com/v1/models on 2026-09-03, + # filtered to type == "openai/chat-completions". + "aimlapi": ( + "openai/gpt-4o-mini", + "openai/gpt-5-5", + "anthropic/claude-sonnet-4.5", + "deepseek/deepseek-chat", + ), "orcarouter": ( "orcarouter/auto", "anthropic/claude-sonnet-4-6", diff --git a/tests/services/test_provider_registry.py b/tests/services/test_provider_registry.py index a32fe90066..bf4966402d 100644 --- a/tests/services/test_provider_registry.py +++ b/tests/services/test_provider_registry.py @@ -1,4 +1,4 @@ -from deeptutor.services.provider_registry import find_by_name, find_gateway +from deeptutor.services.provider_registry import find_by_model, find_by_name, find_gateway def test_nvidia_nim_gateway_detection_by_key_and_base() -> None: @@ -53,6 +53,33 @@ def test_novita_provider_aliases_and_base_detection() -> None: assert find_gateway(api_base="https://api.novita.ai/openai") == spec +def test_aimlapi_provider_aliases_and_base_detection() -> None: + spec = find_by_name("aimlapi") + + assert spec is not None + # The user-facing label is the product's own name, lowercase domain form. + assert spec.display_name == "aimlapi.com" + assert spec.label == "aimlapi.com" + assert spec.env_key == "AIMLAPI_API_KEY" + assert spec.backend == "openai_compat" + assert spec.mode == "gateway" + assert spec.default_api_base == "https://api.aimlapi.com/v1" + # Keys carry no distinguishing prefix, so the endpoint is the only signal. + assert spec.detect_by_key_prefix == "" + # Model ids keep their vendor prefix ("openai/gpt-4o-mini"). + assert spec.strip_model_prefix is False + assert find_by_name("aiml") == spec + assert find_by_name("aiml-api") == spec + assert find_by_name("AIMLAPI") == spec + assert find_gateway(api_base="https://api.aimlapi.com/v1") == spec + + +def test_aimlapi_does_not_capture_unrelated_model_names() -> None: + """Gateways route any model; they must not claim one by keyword.""" + assert find_by_model("openai/gpt-4o-mini") != find_by_name("aimlapi") + assert find_by_model("aimlapi") is None + + def test_openai_codex_is_not_detected_from_api_base() -> None: assert find_gateway(api_base="https://codex.example.com/v1") is None From 281323c5b0f9f78885d5ed1a09f85c9987cd42e7 Mon Sep 17 00:00:00 2001 From: aimlapi Date: Thu, 3 Sep 2026 05:02:18 +0500 Subject: [PATCH 2/5] feat: attribute DeepTutor traffic to aimlapi.com by endpoint host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI/ML API reads the same HTTP-Referer / X-Title pair OpenRouter does, plus X-AIMLAPI-Partner-ID and X-AIMLAPI-Source, to tell which application sent a request. Without them DeepTutor's traffic is indistinguishable from anyone else's, which is what the existing OPENROUTER_ATTRIBUTION_HEADERS block already exists to avoid for the other gateway. The scoping is deliberately narrower than the OpenRouter check next to it. That one matches a substring of the URL, which also fires for a proxy whose path merely contains the vendor name; this one compares the *host* of the resolved endpoint against an exact allow-list, so the headers cannot ride a request to api.aimlapi.com.evil.io, to notaimlapi.com, or to a self-hosted proxy configured under an "aimlapi" binding. Headers are merged into a per-call dict rather than assigned, so a profile's own extra_headers still win and the module constant is never mutated. The partner id fails silently when malformed — an unparseable value is accepted and simply earns nothing — so its shape is asserted in a test rather than left to review, alongside a walk over every other ProviderSpec proving none of them carries an X-AIMLAPI-* header. --- deeptutor/services/llm/openai_http_client.py | 53 +++++++- tests/services/llm/test_openai_http_client.py | 113 ++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) diff --git a/deeptutor/services/llm/openai_http_client.py b/deeptutor/services/llm/openai_http_client.py index d0666232c9..97a4f5edb6 100644 --- a/deeptutor/services/llm/openai_http_client.py +++ b/deeptutor/services/llm/openai_http_client.py @@ -6,6 +6,7 @@ import os import threading from typing import TYPE_CHECKING, Any +from urllib.parse import urlsplit import uuid import httpx @@ -25,6 +26,23 @@ "X-OpenRouter-Title": "DeepTutor", } +# AI/ML API attributes traffic the same way OpenRouter does, plus two headers +# of its own. HTTP-Referer / X-Title identify DeepTutor as the calling app — +# they are not AI/ML API's own URL and title. +AIMLAPI_ATTRIBUTION_HEADERS: dict[str, str] = { + "HTTP-Referer": "https://github.com/HKUDS/DeepTutor", + "X-Title": "DeepTutor", + "X-AIMLAPI-Partner-ID": "part_deeptutor", + "X-AIMLAPI-Source": "agent/deeptutor", +} + +# Exact hosts these headers may be sent to. Matching the *host* of the resolved +# endpoint — not a substring of the URL and not the configured provider name — +# is what keeps attribution off a look-alike domain ("api.aimlapi.com.evil.io", +# "notaimlapi.com") and off a self-hosted proxy that merely fronts the same API +# under a binding still typed as "aimlapi". +_AIMLAPI_ATTRIBUTION_HOSTS: frozenset[str] = frozenset({"api.aimlapi.com"}) + _warning_lock = threading.Lock() _warning_logged = False @@ -114,6 +132,36 @@ def _uses_openrouter(spec: "ProviderSpec | None", api_base: str | None) -> bool: return bool(api_base and "openrouter" in api_base.lower()) +def _endpoint_host(spec: "ProviderSpec | None", api_base: str | None) -> str: + """Host of the endpoint a client will actually call, lowercased. + + Args: + spec: The resolved provider spec, whose ``default_api_base`` applies + when the profile carries no explicit endpoint. + api_base: The profile's configured endpoint, if any. + + Returns: + The hostname, or an empty string when no endpoint resolves or the + value does not parse as a URL. + """ + resolved = (api_base or (spec.default_api_base if spec is not None else "") or "").strip() + if not resolved: + return "" + # A bare "api.example.com/v1" has no scheme, so urlsplit would read it all + # as a path; "//" makes it a netloc without guessing http vs https. + if "//" not in resolved: + resolved = f"//{resolved}" + try: + return (urlsplit(resolved).hostname or "").lower() + except ValueError: + return "" + + +def _uses_aimlapi(spec: "ProviderSpec | None", api_base: str | None) -> bool: + """Whether the resolved endpoint is an AI/ML API host we may attribute to.""" + return _endpoint_host(spec, api_base) in _AIMLAPI_ATTRIBUTION_HOSTS + + def openai_sdk_client_kwargs( *, api_key: str | None, @@ -127,7 +175,7 @@ def openai_sdk_client_kwargs( """Constructor kwargs for ``AsyncOpenAI`` / ``AsyncAzureOpenAI``. The one place that decides what every OpenAI-SDK client DeepTutor builds - looks like on the wire: default headers (session affinity, OpenRouter + looks like on the wire: default headers (session affinity, gateway attribution, the profile's extra headers), the SDK retry budget, and the TLS-verification bypass. ``disable_ssl_verify=None`` reads the system setting; callers that already hold the flag pass it through. @@ -138,6 +186,8 @@ def openai_sdk_client_kwargs( headers["x-session-affinity"] = uuid.uuid4().hex if _uses_openrouter(spec, base_url): headers.update(OPENROUTER_ATTRIBUTION_HEADERS) + if _uses_aimlapi(spec, base_url): + headers.update(AIMLAPI_ATTRIBUTION_HEADERS) if extra_headers: headers.update(extra_headers) kwargs: dict[str, Any] = { @@ -159,6 +209,7 @@ def openai_sdk_client_kwargs( __all__ = [ + "AIMLAPI_ATTRIBUTION_HEADERS", "OPENROUTER_ATTRIBUTION_HEADERS", "build_openai_http_client", "disable_ssl_verify_enabled", diff --git a/tests/services/llm/test_openai_http_client.py b/tests/services/llm/test_openai_http_client.py index bbf25a2973..d5e5d6a409 100644 --- a/tests/services/llm/test_openai_http_client.py +++ b/tests/services/llm/test_openai_http_client.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from types import SimpleNamespace from typing import Any @@ -9,6 +10,7 @@ from deeptutor.services.llm import openai_http_client from deeptutor.services.llm.exceptions import LLMConfigError +from deeptutor.services.provider_registry import PROVIDERS, find_by_name @pytest.fixture(autouse=True) @@ -131,3 +133,114 @@ def test_embedding_sdk_passes_disable_ssl_http_client( assert captured[0]["http_client"] is clients[0] assert clients[0].kwargs == {"verify": False, "timeout": 60} + + +# --- AI/ML API attribution --------------------------------------------------- +# Attribution is scoped to the *host* of the resolved endpoint. A substring +# match on the URL, or a match on the configured provider name, would also fire +# for a look-alike domain and for a self-hosted proxy fronting the same API. + +_AIMLAPI_SPEC = find_by_name("aimlapi") + +_PARTNER_ID_PATTERN = re.compile(r"^part_[A-Za-z0-9]{1,64}$") +_SOURCE_PATTERN = re.compile(r"^(web|agent|mcp)/[a-z0-9-]{1,32}$") + + +def _default_headers(**kwargs: Any) -> dict[str, str]: + return openai_http_client.openai_sdk_client_kwargs(api_key="sk-test", **kwargs)[ + "default_headers" + ] + + +def test_aimlapi_partner_id_and_source_match_the_gateway_contract() -> None: + """A malformed partner id is accepted silently and earns nothing, so assert its shape.""" + headers = openai_http_client.AIMLAPI_ATTRIBUTION_HEADERS + + assert _PARTNER_ID_PATTERN.match(headers["X-AIMLAPI-Partner-ID"]) + assert _SOURCE_PATTERN.match(headers["X-AIMLAPI-Source"]) + # HTTP-Referer / X-Title identify the calling app, not the gateway. + assert headers["HTTP-Referer"] == "https://github.com/HKUDS/DeepTutor" + assert headers["X-Title"] == "DeepTutor" + + +def test_aimlapi_attribution_sent_for_the_registry_endpoint() -> None: + headers = _default_headers(base_url=None, spec=_AIMLAPI_SPEC) + + assert headers["X-AIMLAPI-Partner-ID"] == "part_deeptutor" + assert headers["X-AIMLAPI-Source"] == "agent/deeptutor" + assert headers["X-Title"] == "DeepTutor" + + +@pytest.mark.parametrize( + "base_url", + [ + "https://api.aimlapi.com/v1", + "https://API.AIMLAPI.COM/v1", + "api.aimlapi.com/v1", + ], +) +def test_aimlapi_attribution_sent_for_equivalent_spellings(base_url: str) -> None: + headers = _default_headers(base_url=base_url, spec=None) + + assert headers["X-AIMLAPI-Partner-ID"] == "part_deeptutor" + + +@pytest.mark.parametrize( + "base_url", + [ + # Suffix look-alike: a substring check on the URL would send our + # partner id to whoever controls evil.io. + "https://api.aimlapi.com.evil.io/v1", + "https://notaimlapi.com/v1", + "https://aimlapi.com.attacker.example/v1", + # A proxy that merely fronts the same API is still someone else's host. + "https://gateway.internal.example/aimlapi/v1", + "https://openrouter.ai/api/v1", + "https://api.openai.com/v1", + ], +) +def test_aimlapi_attribution_withheld_from_other_hosts(base_url: str) -> None: + headers = _default_headers(base_url=base_url, spec=None) + + assert not [key for key in headers if key.lower().startswith("x-aimlapi-")] + + +def test_aimlapi_attribution_withheld_when_binding_points_at_a_proxy() -> None: + """An aimlapi-typed profile pointed elsewhere must not carry our headers.""" + headers = _default_headers(base_url="https://proxy.example.com/v1", spec=_AIMLAPI_SPEC) + + assert not [key for key in headers if key.lower().startswith("x-aimlapi-")] + + +def test_aimlapi_attribution_does_not_override_caller_headers() -> None: + headers = _default_headers( + base_url="https://api.aimlapi.com/v1", + spec=_AIMLAPI_SPEC, + extra_headers={"X-Title": "Caller Wins", "X-Custom": "1"}, + ) + + assert headers["X-Title"] == "Caller Wins" + assert headers["X-Custom"] == "1" + assert headers["X-AIMLAPI-Partner-ID"] == "part_deeptutor" + + +def test_aimlapi_attribution_constant_is_never_mutated() -> None: + before = dict(openai_http_client.AIMLAPI_ATTRIBUTION_HEADERS) + + _default_headers( + base_url="https://api.aimlapi.com/v1", + spec=_AIMLAPI_SPEC, + extra_headers={"X-Title": "Caller Wins"}, + ) + + assert openai_http_client.AIMLAPI_ATTRIBUTION_HEADERS == before + + +def test_no_other_provider_spec_carries_aimlapi_headers() -> None: + """Attribution must never ride a request to a different vendor.""" + for spec in PROVIDERS: + if spec.name == "aimlapi": + continue + headers = _default_headers(base_url=None, spec=spec) + leaked = [key for key in headers if key.lower().startswith("x-aimlapi-")] + assert not leaked, f"{spec.name} leaks {leaked}" From 33c2aae82b87b8b2a3d0e12ee0bd9652e9f5d632 Mon Sep 17 00:00:00 2001 From: aimlapi Date: Thu, 3 Sep 2026 05:02:25 +0500 Subject: [PATCH 3/5] fix: drop repeated ids from a provider's /models list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A /models payload may list one model id once per endpoint family it serves, so the same name arrives more than once. AI/ML API returns 936 rows for 785 distinct ids; every repeat became a separate, identical row in the model picker with no way for the user to tell them apart. First occurrence wins, so payload order — which providers use to put their preferred models first — is preserved. --- deeptutor/services/llm/utils.py | 18 ++++++++++++++++-- tests/services/llm/test_utils.py | 10 ++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/deeptutor/services/llm/utils.py b/deeptutor/services/llm/utils.py index 35b7c90d51..288a95abc3 100644 --- a/deeptutor/services/llm/utils.py +++ b/deeptutor/services/llm/utils.py @@ -285,11 +285,25 @@ def _normalize_model_name(entry: object) -> str | None: def collect_model_names(entries: Sequence[object]) -> list[str]: - """Collect model names from provider payloads.""" + """Collect model names from provider payloads, first occurrence wins. + + A ``/models`` payload may list one model id once per endpoint family it + serves, so the same name arrives several times: AI/ML API returns 936 rows + for 785 distinct ids. Without the de-duplication the picker shows the + repeats as separate, identical choices. + + Args: + entries: Raw provider payload entries, each a mapping or a bare string. + + Returns: + Model names in payload order, without repeats. + """ names: list[str] = [] + seen: set[str] = set() for entry in entries: name = _normalize_model_name(entry) - if name: + if name and name not in seen: + seen.add(name) names.append(name) return names diff --git a/tests/services/llm/test_utils.py b/tests/services/llm/test_utils.py index 57d5814813..ab30e67a64 100644 --- a/tests/services/llm/test_utils.py +++ b/tests/services/llm/test_utils.py @@ -109,6 +109,16 @@ def test_collect_model_names() -> None: assert collect_model_names(entries) == ["m1", "m2", "m3", "m4"] +def test_collect_model_names_drops_repeats_in_payload_order() -> None: + """One id listed once per endpoint family must not become several choices.""" + entries = [ + {"id": "m1", "type": "openai/chat-completions"}, + {"id": "m2", "type": "openai/chat-completions"}, + {"id": "m1", "type": "openai/embeddings"}, + ] + assert collect_model_names(entries) == ["m1", "m2"] + + def test_build_auth_headers() -> None: """Auth headers should vary by provider binding.""" assert build_auth_headers("key", binding="anthropic")["x-api-key"] == "key" From 1eb90e93479eafa0631fa7baee4f1a7bbf8a73c2 Mon Sep 17 00:00:00 2001 From: aimlapi Date: Thu, 3 Sep 2026 05:04:00 +0500 Subject: [PATCH 4/5] =?UTF-8?q?chore(aimlapi):=20fork-only=20placement=20?= =?UTF-8?q?=E2=80=94=20do=20not=20send=20upstream?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Partnership placement, kept as one commit so it can be dropped wholesale before anything is offered upstream. Nothing here changes behaviour that a user can observe beyond ordering: - aimlapi.com moves to the head of the gateway block in PROVIDERS. The block is hand-ordered and the file's own docstring says order controls match priority; every gateway in it is distinguished by an unambiguous api_base keyword, so the move changes which entry is offered first, not which one is detected. - aimlapi.com is added at the head of FEATURED_LLM_PROVIDERS, the wizard's existing hand-picked shortlist. That list is the repo's own "featured" mechanism; no new badge concept was invented for it. The wizard's default selection is untouched — it still falls back to openai unless the user's current binding is something else. --- deeptutor/services/provider_registry.py | 30 ++++++++++++------------- deeptutor_cli/init_wizard.py | 1 + 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/deeptutor/services/provider_registry.py b/deeptutor/services/provider_registry.py index d480440adf..06e47c4eb3 100644 --- a/deeptutor/services/provider_registry.py +++ b/deeptutor/services/provider_registry.py @@ -202,6 +202,21 @@ def canonical_provider_name(name: str | None) -> str | None: is_direct=True, ), # === Gateways (detected by api_key / api_base, route any model) ======== + # AI/ML API issues opaque keys with no distinguishing prefix, so the + # endpoint is the only reliable signal — hence no detect_by_key_prefix. + # Model ids keep their vendor prefix ("openai/gpt-4o-mini"), so no + # strip_model_prefix either. Only POST /v1/chat/completions and + # /v1/responses exist; there is no /v1/completions endpoint. + ProviderSpec( + name="aimlapi", + keywords=("aimlapi",), + env_key="AIMLAPI_API_KEY", + display_name="aimlapi.com", + backend="openai_compat", + is_gateway=True, + detect_by_base_keyword="aimlapi", + default_api_base="https://api.aimlapi.com/v1", + ), ProviderSpec( name="openrouter", keywords=("openrouter",), @@ -276,21 +291,6 @@ def canonical_provider_name(name: str | None) -> str | None: detect_by_base_keyword="atlascloud", default_api_base="https://api.atlascloud.ai/v1", ), - # AI/ML API issues opaque keys with no distinguishing prefix, so the - # endpoint is the only reliable signal — hence no detect_by_key_prefix. - # Model ids keep their vendor prefix ("openai/gpt-4o-mini"), so no - # strip_model_prefix either. Only POST /v1/chat/completions and - # /v1/responses exist; there is no /v1/completions endpoint. - ProviderSpec( - name="aimlapi", - keywords=("aimlapi",), - env_key="AIMLAPI_API_KEY", - display_name="aimlapi.com", - backend="openai_compat", - is_gateway=True, - detect_by_base_keyword="aimlapi", - default_api_base="https://api.aimlapi.com/v1", - ), ProviderSpec( name="volcengine", keywords=("volcengine", "volces", "ark"), diff --git a/deeptutor_cli/init_wizard.py b/deeptutor_cli/init_wizard.py index b401e283a5..f75f2c6f27 100644 --- a/deeptutor_cli/init_wizard.py +++ b/deeptutor_cli/init_wizard.py @@ -38,6 +38,7 @@ # via the "Show all" option. Names match ProviderSpec.name in provider_registry. FEATURED_LLM_PROVIDERS: tuple[str, ...] = ( + "aimlapi", "openai", "anthropic", "deepseek", From 2ead62b51d335ea6b2097ff8f679b49344603ab1 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 18:10:57 +0500 Subject: [PATCH 5/5] fix(aimlapi): use the registered partner id The placeholder part_deeptutor was a readable stand-in chosen before the partner was registered. Registration mints the id server-side, so the real value is part_ItAs0L5uSTvV2dFDOZZaS1BL. A wrong or unknown partner id is accepted with a 200 and silently not attributed, so this would not have surfaced at runtime. --- deeptutor/services/llm/openai_http_client.py | 2 +- tests/services/llm/test_openai_http_client.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deeptutor/services/llm/openai_http_client.py b/deeptutor/services/llm/openai_http_client.py index 97a4f5edb6..92757c88d1 100644 --- a/deeptutor/services/llm/openai_http_client.py +++ b/deeptutor/services/llm/openai_http_client.py @@ -32,7 +32,7 @@ AIMLAPI_ATTRIBUTION_HEADERS: dict[str, str] = { "HTTP-Referer": "https://github.com/HKUDS/DeepTutor", "X-Title": "DeepTutor", - "X-AIMLAPI-Partner-ID": "part_deeptutor", + "X-AIMLAPI-Partner-ID": "part_ItAs0L5uSTvV2dFDOZZaS1BL", "X-AIMLAPI-Source": "agent/deeptutor", } diff --git a/tests/services/llm/test_openai_http_client.py b/tests/services/llm/test_openai_http_client.py index d5e5d6a409..220993bca9 100644 --- a/tests/services/llm/test_openai_http_client.py +++ b/tests/services/llm/test_openai_http_client.py @@ -166,7 +166,7 @@ def test_aimlapi_partner_id_and_source_match_the_gateway_contract() -> None: def test_aimlapi_attribution_sent_for_the_registry_endpoint() -> None: headers = _default_headers(base_url=None, spec=_AIMLAPI_SPEC) - assert headers["X-AIMLAPI-Partner-ID"] == "part_deeptutor" + assert headers["X-AIMLAPI-Partner-ID"] == "part_ItAs0L5uSTvV2dFDOZZaS1BL" assert headers["X-AIMLAPI-Source"] == "agent/deeptutor" assert headers["X-Title"] == "DeepTutor" @@ -182,7 +182,7 @@ def test_aimlapi_attribution_sent_for_the_registry_endpoint() -> None: def test_aimlapi_attribution_sent_for_equivalent_spellings(base_url: str) -> None: headers = _default_headers(base_url=base_url, spec=None) - assert headers["X-AIMLAPI-Partner-ID"] == "part_deeptutor" + assert headers["X-AIMLAPI-Partner-ID"] == "part_ItAs0L5uSTvV2dFDOZZaS1BL" @pytest.mark.parametrize( @@ -221,7 +221,7 @@ def test_aimlapi_attribution_does_not_override_caller_headers() -> None: assert headers["X-Title"] == "Caller Wins" assert headers["X-Custom"] == "1" - assert headers["X-AIMLAPI-Partner-ID"] == "part_deeptutor" + assert headers["X-AIMLAPI-Partner-ID"] == "part_ItAs0L5uSTvV2dFDOZZaS1BL" def test_aimlapi_attribution_constant_is_never_mutated() -> None: