From 9a279014a7187d1389c7359e16448af29dc2e190 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 04:12:00 +0500 Subject: [PATCH 1/3] feat(providers): add aimlapi.com as an OpenAI-compatible gateway provider Users who hold an aimlapi.com key currently have to fall back to a named custom provider: they must know the base URL, and they lose preset detection, the `nanobot status` label and the Settings model catalog. A registry entry buys all of that for the same twelve lines the other gateways cost. Model IDs are sent unchanged so the catalog's `vendor/model` form keeps working, and reasoning is the plain top-level `reasoning_effort` rather than OpenRouter's nested request shape, because the endpoint is OpenAI-compatible. The default headers identify nanobot to the gateway the same way the existing OpenRouter attribution block does. They live on this provider's spec, so they cannot ride a request to any other provider, and the factory builds a fresh dict per provider with user-supplied `extraHeaders` taking precedence. A malformed partner id is dropped silently upstream rather than rejected, so its shape is asserted in a test instead of being discovered in production. --- docs/configuration.md | 1 + docs/providers.md | 39 ++++++ nanobot/config/schema.py | 1 + nanobot/providers/registry.py | 20 +++ tests/providers/test_aimlapi_provider.py | 119 ++++++++++++++++++ tests/webui/test_settings_api.py | 20 +++ .../settings/shared/ModelControls.tsx | 2 + webui/src/lib/provider-brand.ts | 1 + 8 files changed, 203 insertions(+) create mode 100644 tests/providers/test_aimlapi_provider.py diff --git a/docs/configuration.md b/docs/configuration.md index 53f570f7ea7..04595b5e1df 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -269,6 +269,7 @@ Tracing covers the providers that go through nanobot's OpenAI-compatible client | `custom` | Any OpenAI-compatible endpoint | — | | `openrouter` | LLM gateway for hosted model families + Voice transcription (STT models) | [openrouter.ai](https://openrouter.ai) | | `edenai` | LLM gateway for Eden AI's OpenAI-compatible model catalog | [app.edenai.run](https://app.edenai.run/) | +| `aimlapi` | LLM gateway for aimlapi.com's OpenAI-compatible model catalog | [aimlapi.com](https://aimlapi.com) | | `opencode` | LLM gateway (OpenCode Zen coding-agent models) | [opencode.ai/docs/zen](https://opencode.ai/docs/zen/) | | `opencode_zen` | LLM gateway (legacy alias for OpenCode Zen) | [opencode.ai/docs/zen](https://opencode.ai/docs/zen/) | | `opencode_go` | LLM gateway (OpenCode Go low-cost coding models) | [opencode.ai/docs/go](https://opencode.ai/docs/go/) | diff --git a/docs/providers.md b/docs/providers.md index 56db7211f6c..179c085f6b7 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -190,6 +190,45 @@ Eden AI's [model listing](https://www.edenai.co/docs/v3/llms/listing-models) to choose a currently available model. The WebUI can also load that catalog after the Eden AI API key is saved under **Settings → Models**. +### aimlapi.com Gateway + +[aimlapi.com](https://aimlapi.com) exposes an OpenAI-compatible chat-completions +endpoint at `https://api.aimlapi.com/v1`. Configure the built-in `aimlapi` +provider and use the full `vendor/model` identifier from its catalog: + +```json +{ + "providers": { + "aimlapi": { + "apiKey": "${AIMLAPI_API_KEY}" + } + }, + "modelPresets": { + "primary": { + "provider": "aimlapi", + "model": "openai/gpt-5", + "maxTokens": 8192 + } + }, + "agents": { + "defaults": { + "modelPreset": "primary" + } + } +} +``` + +Nanobot sends the model ID unchanged, including its vendor prefix, and passes +`reasoning_effort` as the normal top-level Chat Completions parameter. Other +valid IDs include `anthropic/claude-sonnet-4-6` and `google/gemini-3.1-pro-preview`. +The public catalog at `https://api.aimlapi.com/v1/models` lists the current IDs; +entries with `"type": "openai/chat-completions"` are the chat models. The WebUI +can also load that catalog after the API key is saved under **Settings → Models**. + +Note that `https://api.aimlapi.com/v1/models` answers `200` for any credential, +including an invalid one, so a successful model list does not prove the key +works. The first chat completion is what verifies it. + ### OpenCode Zen and Go OpenCode Zen and OpenCode Go are OpenCode-managed gateways for coding-agent models. diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 8438352f43d..2c1dc3c5bb9 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -280,6 +280,7 @@ class ProvidersConfig(Base): aihubmix: ProviderConfig = Field(default_factory=ProviderConfig) # AiHubMix API gateway siliconflow: ProviderConfig = Field(default_factory=ProviderConfig) # SiliconFlow (硅基流动) edenai: ProviderConfig = Field(default_factory=ProviderConfig) # Eden AI API gateway + aimlapi: ProviderConfig = Field(default_factory=ProviderConfig) # AI/ML API gateway novita: ProviderConfig = Field(default_factory=ProviderConfig) # Novita AI volcengine: ProviderConfig = Field(default_factory=ProviderConfig) # VolcEngine (火山引擎) volcengine_coding_plan: ProviderConfig = Field(default_factory=ProviderConfig) # VolcEngine Coding Plan diff --git a/nanobot/providers/registry.py b/nanobot/providers/registry.py index bbfa65789b0..502b8f09801 100644 --- a/nanobot/providers/registry.py +++ b/nanobot/providers/registry.py @@ -226,6 +226,26 @@ def label(self) -> str: detect_by_base_keyword="edenai", default_api_base="https://api.edenai.run/v3", ), + # AI/ML API: OpenAI-compatible gateway. Models use the "vendor/model" naming + # scheme (e.g. "openai/gpt-5"); the full id is sent upstream. The default + # headers identify nanobot to the gateway, the same way the OpenRouter + # attribution headers do; they ride only on this provider's requests. + ProviderSpec( + name="aimlapi", + keywords=("aimlapi",), + env_key="AIMLAPI_API_KEY", + display_name="aimlapi.com", + backend="openai_compat", + default_extra_headers=( + ("HTTP-Referer", "https://github.com/HKUDS/nanobot"), + ("X-Title", "nanobot"), + ("X-AIMLAPI-Source", "agent/hkuds-nanobot"), + ("X-AIMLAPI-Partner-ID", "part_hkudsnanobot"), + ), + is_gateway=True, + detect_by_base_keyword="aimlapi", + default_api_base="https://api.aimlapi.com/v1", + ), # OpenCode Zen: OpenAI-compatible chat-completions gateway for coding models. # models.dev/OpenCode use provider id "opencode" and model ids like # "opencode/"; send the bare model upstream. diff --git a/tests/providers/test_aimlapi_provider.py b/tests/providers/test_aimlapi_provider.py new file mode 100644 index 00000000000..4fe02b4640c --- /dev/null +++ b/tests/providers/test_aimlapi_provider.py @@ -0,0 +1,119 @@ +"""Tests for the aimlapi.com provider registration.""" + +import re +from unittest.mock import patch + +from nanobot.config.schema import Config, ProviderConfig, ProvidersConfig +from nanobot.providers.factory import _provider_extra_headers +from nanobot.providers.openai_compat_provider import OpenAICompatProvider +from nanobot.providers.registry import PROVIDERS, find_by_name + +# The gateway silently drops a malformed partner id, so only a test catches a typo. +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 test_aimlapi_config_field_exists() -> None: + assert hasattr(ProvidersConfig(), "aimlapi") + + +def test_aimlapi_registry_contract() -> None: + specs = {spec.name: spec for spec in PROVIDERS} + + assert "aimlapi" in specs + aimlapi = specs["aimlapi"] + assert aimlapi.backend == "openai_compat" + assert aimlapi.env_key == "AIMLAPI_API_KEY" + assert aimlapi.display_name == "aimlapi.com" + assert aimlapi.is_gateway is True + assert aimlapi.detect_by_base_keyword == "aimlapi" + assert aimlapi.default_api_base == "https://api.aimlapi.com/v1" + assert aimlapi.strip_model_prefix is False + # aimlapi.com accepts OpenAI's top-level reasoning_effort parameter. Do not add + # OpenRouter's separate {"reasoning": {"effort": ...}} request shape. + assert aimlapi.gateway_reasoning_style == "" + + +def test_aimlapi_forced_provider_uses_default_api_base() -> None: + config = Config.model_validate( + { + "providers": {"aimlapi": {"apiKey": "aimlapi-key"}}, + "agents": { + "defaults": { + "provider": "aimlapi", + "model": "openai/gpt-5", + } + }, + } + ) + + model = "openai/gpt-5" + assert config.get_provider_name(model) == "aimlapi" + assert config.get_api_key(model) == "aimlapi-key" + assert config.get_api_base(model) == "https://api.aimlapi.com/v1" + + +def test_aimlapi_preserves_model_id_and_reasoning_effort() -> None: + spec = find_by_name("aimlapi") + with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"): + provider = OpenAICompatProvider( + api_key="aimlapi-key", + default_model="openai/gpt-5", + spec=spec, + ) + + kwargs = provider._build_kwargs( + messages=[{"role": "user", "content": "hi"}], + tools=None, + model="openai/gpt-5", + max_tokens=1024, + temperature=0.7, + reasoning_effort="medium", + tool_choice=None, + ) + + assert kwargs["model"] == "openai/gpt-5" + assert kwargs["reasoning_effort"] == "medium" + assert "reasoning" not in kwargs.get("extra_body", {}) + + +def test_aimlapi_default_headers_identify_nanobot() -> None: + spec = find_by_name("aimlapi") + + assert spec is not None + headers = _provider_extra_headers(spec, ProviderConfig()) + assert headers == { + "HTTP-Referer": "https://github.com/HKUDS/nanobot", + "X-Title": "nanobot", + "X-AIMLAPI-Source": "agent/hkuds-nanobot", + "X-AIMLAPI-Partner-ID": "part_hkudsnanobot", + } + assert PARTNER_ID_PATTERN.match(headers["X-AIMLAPI-Partner-ID"]) + assert SOURCE_PATTERN.match(headers["X-AIMLAPI-Source"]) + + +def test_aimlapi_default_headers_are_scoped_to_this_provider() -> None: + for spec in PROVIDERS: + if spec.name == "aimlapi": + continue + assert not any( + name.lower().startswith("x-aimlapi-") for name, _ in spec.default_extra_headers + ) + + +def test_aimlapi_user_headers_win_without_mutating_the_spec() -> None: + spec = find_by_name("aimlapi") + assert spec is not None + before = dict(spec.default_extra_headers) + + provider = ProviderConfig.model_validate({ + "extraHeaders": {"X-Title": "my-fork", "X-Custom": "1"}, + }) + headers = _provider_extra_headers(spec, provider) + + assert headers["X-Title"] == "my-fork" + assert headers["X-Custom"] == "1" + assert headers["X-AIMLAPI-Partner-ID"] == "part_hkudsnanobot" + # The registry entry is a shared constant; building the per-request dict + # must not write back into it. + assert dict(spec.default_extra_headers) == before diff --git a/tests/webui/test_settings_api.py b/tests/webui/test_settings_api.py index a6b8c29e612..13822ddabf5 100644 --- a/tests/webui/test_settings_api.py +++ b/tests/webui/test_settings_api.py @@ -98,6 +98,26 @@ def test_settings_payload_includes_versioned_docs( } +def test_settings_payload_exposes_aimlapi_provider( + tmp_path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + config_path = tmp_path / "config.json" + config = Config() + config.providers.aimlapi.api_key = "aimlapi-test-key" + save_config(config, config_path) + monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path) + + payload = settings_payload() + aimlapi = next(row for row in payload["providers"] if row["name"] == "aimlapi") + + assert aimlapi["label"] == "aimlapi.com" + assert aimlapi["configured"] is True + assert aimlapi["default_api_base"] == "https://api.aimlapi.com/v1" + assert aimlapi["model_catalog"] == "catalog" + assert aimlapi["model_selectable"] is True + + def test_settings_payload_exposes_edenai_provider( tmp_path, monkeypatch: pytest.MonkeyPatch, diff --git a/webui/src/components/settings/shared/ModelControls.tsx b/webui/src/components/settings/shared/ModelControls.tsx index f42f7018c35..3aaab33c148 100644 --- a/webui/src/components/settings/shared/ModelControls.tsx +++ b/webui/src/components/settings/shared/ModelControls.tsx @@ -43,6 +43,7 @@ import { cn } from "@/lib/utils"; const DEFERRED_MODEL_LIST_PROVIDERS = new Set([ "aihubmix", + "aimlapi", "atomic_chat", "byteplus", "byteplus_coding_plan", @@ -583,6 +584,7 @@ export const PROVIDER_ICONS: Record = { orcarouter: Sparkles, skywork: Sparkles, aihubmix: Triangle, + aimlapi: Sparkles, anthropic: Brain, openai: Bot, deepseek: Waves, diff --git a/webui/src/lib/provider-brand.ts b/webui/src/lib/provider-brand.ts index 743cb4a63b1..b6065c5e391 100644 --- a/webui/src/lib/provider-brand.ts +++ b/webui/src/lib/provider-brand.ts @@ -150,6 +150,7 @@ const PROVIDER_LABEL_ALIASES: Record = { const PROVIDER_BRANDS: Record = { aihubmix: brand("aihubmix.com", "#111827", "AH"), + aimlapi: brand("aimlapi.com", "#111827", "AM"), ant_ling: brand("ant-ling.com", "#7C3AED", "AL"), anthropic: brand("anthropic.com", "#D97757", "A"), assemblyai: brand("assemblyai.com", "#111827", "AA"), From 2f6b67fd53fa1da2d9f6049cfb2927183c995d7d Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 04:13:33 +0500 Subject: [PATCH 2/3] =?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 Moves the aimlapi.com entry to the head of the gateway block in the registry and to the head of the gateway rows in both docs lists. The registry tuple's order is match priority, so this also makes aimlapi.com win gateway fallback ahead of the other gateways. This is placement, not function: nothing here is needed for the provider to work, and docs/providers.md states that the docs do not rank providers. It is kept as its own commit so it can be dropped before the change is offered upstream. The repository has no "recommended" or "featured" provider badge, and none was invented. --- docs/configuration.md | 2 +- docs/providers.md | 78 +++++++++++++++++------------------ nanobot/providers/registry.py | 40 +++++++++--------- 3 files changed, 60 insertions(+), 60 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 04595b5e1df..608b6e95aea 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -267,9 +267,9 @@ Tracing covers the providers that go through nanobot's OpenAI-compatible client | Provider | Purpose | Get API Key | |----------|---------|-------------| | `custom` | Any OpenAI-compatible endpoint | — | +| `aimlapi` | LLM gateway for aimlapi.com's OpenAI-compatible model catalog | [aimlapi.com](https://aimlapi.com) | | `openrouter` | LLM gateway for hosted model families + Voice transcription (STT models) | [openrouter.ai](https://openrouter.ai) | | `edenai` | LLM gateway for Eden AI's OpenAI-compatible model catalog | [app.edenai.run](https://app.edenai.run/) | -| `aimlapi` | LLM gateway for aimlapi.com's OpenAI-compatible model catalog | [aimlapi.com](https://aimlapi.com) | | `opencode` | LLM gateway (OpenCode Zen coding-agent models) | [opencode.ai/docs/zen](https://opencode.ai/docs/zen/) | | `opencode_zen` | LLM gateway (legacy alias for OpenCode Zen) | [opencode.ai/docs/zen](https://opencode.ai/docs/zen/) | | `opencode_go` | LLM gateway (OpenCode Go low-cost coding models) | [opencode.ai/docs/go](https://opencode.ai/docs/go/) | diff --git a/docs/providers.md b/docs/providers.md index 179c085f6b7..3a70b479fd0 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -71,6 +71,45 @@ Use `proxy` when one provider must send HTTP traffic through a proxy without cha ## Common Provider Patterns +### aimlapi.com Gateway + +[aimlapi.com](https://aimlapi.com) exposes an OpenAI-compatible chat-completions +endpoint at `https://api.aimlapi.com/v1`. Configure the built-in `aimlapi` +provider and use the full `vendor/model` identifier from its catalog: + +```json +{ + "providers": { + "aimlapi": { + "apiKey": "${AIMLAPI_API_KEY}" + } + }, + "modelPresets": { + "primary": { + "provider": "aimlapi", + "model": "openai/gpt-5", + "maxTokens": 8192 + } + }, + "agents": { + "defaults": { + "modelPreset": "primary" + } + } +} +``` + +Nanobot sends the model ID unchanged, including its vendor prefix, and passes +`reasoning_effort` as the normal top-level Chat Completions parameter. Other +valid IDs include `anthropic/claude-sonnet-4-6` and `google/gemini-3.1-pro-preview`. +The public catalog at `https://api.aimlapi.com/v1/models` lists the current IDs; +entries with `"type": "openai/chat-completions"` are the chat models. The WebUI +can also load that catalog after the API key is saved under **Settings → Models**. + +Note that `https://api.aimlapi.com/v1/models` answers `200` for any credential, +including an invalid one, so a successful model list does not prove the key +works. The first chat completion is what verifies it. + ### OpenRouter Gateway Gateway-style setup for model IDs served through OpenRouter. @@ -190,45 +229,6 @@ Eden AI's [model listing](https://www.edenai.co/docs/v3/llms/listing-models) to choose a currently available model. The WebUI can also load that catalog after the Eden AI API key is saved under **Settings → Models**. -### aimlapi.com Gateway - -[aimlapi.com](https://aimlapi.com) exposes an OpenAI-compatible chat-completions -endpoint at `https://api.aimlapi.com/v1`. Configure the built-in `aimlapi` -provider and use the full `vendor/model` identifier from its catalog: - -```json -{ - "providers": { - "aimlapi": { - "apiKey": "${AIMLAPI_API_KEY}" - } - }, - "modelPresets": { - "primary": { - "provider": "aimlapi", - "model": "openai/gpt-5", - "maxTokens": 8192 - } - }, - "agents": { - "defaults": { - "modelPreset": "primary" - } - } -} -``` - -Nanobot sends the model ID unchanged, including its vendor prefix, and passes -`reasoning_effort` as the normal top-level Chat Completions parameter. Other -valid IDs include `anthropic/claude-sonnet-4-6` and `google/gemini-3.1-pro-preview`. -The public catalog at `https://api.aimlapi.com/v1/models` lists the current IDs; -entries with `"type": "openai/chat-completions"` are the chat models. The WebUI -can also load that catalog after the API key is saved under **Settings → Models**. - -Note that `https://api.aimlapi.com/v1/models` answers `200` for any credential, -including an invalid one, so a successful model list does not prove the key -works. The first chat completion is what verifies it. - ### OpenCode Zen and Go OpenCode Zen and OpenCode Go are OpenCode-managed gateways for coding-agent models. diff --git a/nanobot/providers/registry.py b/nanobot/providers/registry.py index 502b8f09801..c52bd522534 100644 --- a/nanobot/providers/registry.py +++ b/nanobot/providers/registry.py @@ -188,6 +188,26 @@ def label(self) -> str: ), # === Gateways (detected by api_key / api_base, not model name) ========= # Gateways can route any model, so they win in fallback. + # AI/ML API: OpenAI-compatible gateway. Models use the "vendor/model" naming + # scheme (e.g. "openai/gpt-5"); the full id is sent upstream. The default + # headers identify nanobot to the gateway, the same way the OpenRouter + # attribution headers do; they ride only on this provider's requests. + ProviderSpec( + name="aimlapi", + keywords=("aimlapi",), + env_key="AIMLAPI_API_KEY", + display_name="aimlapi.com", + backend="openai_compat", + default_extra_headers=( + ("HTTP-Referer", "https://github.com/HKUDS/nanobot"), + ("X-Title", "nanobot"), + ("X-AIMLAPI-Source", "agent/hkuds-nanobot"), + ("X-AIMLAPI-Partner-ID", "part_hkudsnanobot"), + ), + is_gateway=True, + detect_by_base_keyword="aimlapi", + default_api_base="https://api.aimlapi.com/v1", + ), # OpenRouter: global gateway, keys start with "sk-or-" ProviderSpec( name="openrouter", @@ -226,26 +246,6 @@ def label(self) -> str: detect_by_base_keyword="edenai", default_api_base="https://api.edenai.run/v3", ), - # AI/ML API: OpenAI-compatible gateway. Models use the "vendor/model" naming - # scheme (e.g. "openai/gpt-5"); the full id is sent upstream. The default - # headers identify nanobot to the gateway, the same way the OpenRouter - # attribution headers do; they ride only on this provider's requests. - ProviderSpec( - name="aimlapi", - keywords=("aimlapi",), - env_key="AIMLAPI_API_KEY", - display_name="aimlapi.com", - backend="openai_compat", - default_extra_headers=( - ("HTTP-Referer", "https://github.com/HKUDS/nanobot"), - ("X-Title", "nanobot"), - ("X-AIMLAPI-Source", "agent/hkuds-nanobot"), - ("X-AIMLAPI-Partner-ID", "part_hkudsnanobot"), - ), - is_gateway=True, - detect_by_base_keyword="aimlapi", - default_api_base="https://api.aimlapi.com/v1", - ), # OpenCode Zen: OpenAI-compatible chat-completions gateway for coding models. # models.dev/OpenCode use provider id "opencode" and model ids like # "opencode/"; send the bare model upstream. From e31c78bde0541b049fc1df25ca2164a48a655393 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 18:12:29 +0500 Subject: [PATCH 3/3] fix(aimlapi): use the registered partner id The placeholder part_hkudsnanobot was a readable stand-in chosen before the partner was registered. Registration mints the id server-side, so the real value is part_TcTxHfamJ2kkNiFsYzEVELTy. A wrong or unknown partner id is accepted with a 200 and silently not attributed, so this would not have surfaced at runtime. --- nanobot/providers/registry.py | 2 +- tests/providers/test_aimlapi_provider.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nanobot/providers/registry.py b/nanobot/providers/registry.py index c52bd522534..90a68294181 100644 --- a/nanobot/providers/registry.py +++ b/nanobot/providers/registry.py @@ -202,7 +202,7 @@ def label(self) -> str: ("HTTP-Referer", "https://github.com/HKUDS/nanobot"), ("X-Title", "nanobot"), ("X-AIMLAPI-Source", "agent/hkuds-nanobot"), - ("X-AIMLAPI-Partner-ID", "part_hkudsnanobot"), + ("X-AIMLAPI-Partner-ID", "part_TcTxHfamJ2kkNiFsYzEVELTy"), ), is_gateway=True, detect_by_base_keyword="aimlapi", diff --git a/tests/providers/test_aimlapi_provider.py b/tests/providers/test_aimlapi_provider.py index 4fe02b4640c..7acd8c72a29 100644 --- a/tests/providers/test_aimlapi_provider.py +++ b/tests/providers/test_aimlapi_provider.py @@ -86,7 +86,7 @@ def test_aimlapi_default_headers_identify_nanobot() -> None: "HTTP-Referer": "https://github.com/HKUDS/nanobot", "X-Title": "nanobot", "X-AIMLAPI-Source": "agent/hkuds-nanobot", - "X-AIMLAPI-Partner-ID": "part_hkudsnanobot", + "X-AIMLAPI-Partner-ID": "part_TcTxHfamJ2kkNiFsYzEVELTy", } assert PARTNER_ID_PATTERN.match(headers["X-AIMLAPI-Partner-ID"]) assert SOURCE_PATTERN.match(headers["X-AIMLAPI-Source"]) @@ -113,7 +113,7 @@ def test_aimlapi_user_headers_win_without_mutating_the_spec() -> None: assert headers["X-Title"] == "my-fork" assert headers["X-Custom"] == "1" - assert headers["X-AIMLAPI-Partner-ID"] == "part_hkudsnanobot" + assert headers["X-AIMLAPI-Partner-ID"] == "part_TcTxHfamJ2kkNiFsYzEVELTy" # The registry entry is a shared constant; building the per-request dict # must not write back into it. assert dict(spec.default_extra_headers) == before