From 5ea1e2b0ae3f2362c5046aba58a27b7f14e111f3 Mon Sep 17 00:00:00 2001 From: aimlapi Date: Thu, 3 Sep 2026 07:53:36 +0500 Subject: [PATCH 1/3] feat: add aimlapi.com as an OpenAI-compatible LLM provider AgenticSeek's prompts are tuned for Deepseek-class models, but reaching one today means holding a separate key per vendor. aimlapi.com is a single OpenAI-compatible endpoint in front of 350+ chat models, so a user can switch between Deepseek, Anthropic and others by editing provider_model alone. This follows the precedent already set by the litellm gateway provider. The provider is registered in unsafe_providers so the existing "your data will be sent to the cloud" warning fires, as docs/CONTRIBUTING.md requires of any cloud service, and get_api_key derives AIMLAPI_API_KEY with no extra code. Request parameters are built by omitting unset keys instead of passing None: aimlapi.com answers 400 for an explicit null on temperature/top_p/tools and others on some of its models, which a mock-based test cannot catch. Attribution headers identifying AgenticSeek are keyed on the request origin rather than on the provider name, so overriding AIMLAPI_BASE_URL to a proxy sends none of them, and each call builds a fresh dict. --- .env.example | 3 + README.md | 2 + sources/llm_provider.py | 61 +++++++++++- tests/test_aimlapi_provider.py | 171 +++++++++++++++++++++++++++++++++ 4 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 tests/test_aimlapi_provider.py diff --git a/.env.example b/.env.example index 225f736c..982f9225 100644 --- a/.env.example +++ b/.env.example @@ -40,3 +40,6 @@ MINIMAX_API_KEY='xxxxx' # Optional: MiniMax API base URL (default: https://api.minimax.io/v1) # For mainland China users: https://api.minimaxi.com/v1 # MINIMAX_BASE_URL='https://api.minimax.io/v1' +AIMLAPI_API_KEY='xxxxx' +# Optional: aimlapi.com API base URL (default: https://api.aimlapi.com/v1) +# AIMLAPI_BASE_URL='https://api.aimlapi.com/v1' diff --git a/README.md b/README.md index 794d6c4c..964c0bf6 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ OPENROUTER_API_KEY='optional' TOGETHER_API_KEY='optional' GOOGLE_API_KEY='optional' ANTHROPIC_API_KEY='optional' +AIMLAPI_API_KEY='optional' ``` @@ -249,6 +250,7 @@ provider_server_address = # Typically ignored or can be left blank when is_local | TogetherAI | `togetherAI` | No | Use various open-source models via TogetherAI API.| [api.together.ai/settings/api-keys](https://api.together.ai/settings/api-keys) | | OpenRouter | `openrouter` | No | Use OpenRouter Models| [https://openrouter.ai/](https://openrouter.ai/) | | MiniMax | `minimax` | No | Use MiniMax models (e.g., MiniMax-M3, MiniMax-M2.7).| [platform.minimax.io](https://platform.minimax.io/user-center/basic-information) | +| aimlapi.com | `aimlapi` | No | One API for 350+ chat models from many vendors (e.g. `deepseek/deepseek-v4-flash`, `anthropic/claude-sonnet-4.6`). Model ids are listed at [api.aimlapi.com/v1/models](https://api.aimlapi.com/v1/models). | [aimlapi.com/app/keys](https://aimlapi.com/app/keys) | *Note:* * We advise against using `gpt-4o` or other OpenAI models for complex web browsing and task planning as current prompt optimizations are geared towards models like Deepseek. diff --git a/sources/llm_provider.py b/sources/llm_provider.py index eb1c7560..1056f127 100644 --- a/sources/llm_provider.py +++ b/sources/llm_provider.py @@ -14,6 +14,27 @@ from sources.logger import Logger from sources.utility import pretty_print, animate_thinking +AIMLAPI_DEFAULT_BASE_URL = "https://api.aimlapi.com/v1" +AIMLAPI_ATTRIBUTION_HEADERS = { + "HTTP-Referer": "https://github.com/Fosowl/agenticSeek", + "X-Title": "AgenticSeek", + "X-AIMLAPI-Partner-ID": "part_agenticseek", + "X-AIMLAPI-Source": "agent/agenticseek", +} + +def aimlapi_attribution_headers(base_url: str) -> dict: + """ + Attribution headers identifying AgenticSeek to aimlapi.com. + + Keyed on the request origin, not on the provider name: if AIMLAPI_BASE_URL + points somewhere else (a proxy, a self-hosted gateway) we send nothing, so + these can never ride a request to a third party. Returns a fresh dict every + call so the module level constant is never mutated. + """ + if urlparse(base_url).hostname != "api.aimlapi.com": + return {} + return dict(AIMLAPI_ATTRIBUTION_HEADERS) + class Provider: def __init__(self, provider_name, model, server_address="127.0.0.1:5000", is_local=False): self.provider_name = provider_name.lower() @@ -38,12 +59,13 @@ def __init__(self, provider_name, model, server_address="127.0.0.1:5000", is_loc "anthropic": self.anthropic_fn, "minimax": self.minimax_fn, "litellm": self.litellm_fn, + "aimlapi": self.aimlapi_fn, "test": self.test_fn } self.logger = Logger("provider.log") self.api_key = None self.internal_url, self.in_docker = self.get_internal_url() - self.unsafe_providers = ["openai", "deepseek", "dsk_deepseek", "together", "google", "openrouter", "anthropic", "minimax"] + self.unsafe_providers = ["openai", "deepseek", "dsk_deepseek", "together", "google", "openrouter", "anthropic", "minimax", "aimlapi"] if self.provider_name not in self.available_providers: raise ValueError(f"Unknown provider: {provider_name}") if self.provider_name in self.unsafe_providers and self.is_local == False: @@ -541,6 +563,43 @@ def litellm_fn(self, history, verbose=False): except Exception as e: raise Exception(f"LiteLLM API error: {str(e)}") from e + def aimlapi_fn(self, history, verbose=False): + """ + Use aimlapi.com (AI/ML API) to generate text through its + OpenAI-compatible endpoint. + + Set AIMLAPI_API_KEY in your .env. provider_model is any chat model id + listed by https://api.aimlapi.com/v1/models (filter on + type == "openai/chat-completions"), e.g. deepseek/deepseek-v4-flash. + """ + if self.is_local: + raise Exception("aimlapi.com is not available for local use. Change config.ini") + load_dotenv() + base_url = os.getenv("AIMLAPI_BASE_URL", AIMLAPI_DEFAULT_BASE_URL) + + client = OpenAI( + api_key=self.api_key, + base_url=base_url, + default_headers=aimlapi_attribution_headers(base_url), + ) + # Optional parameters are omitted, never sent as None: aimlapi.com + # rejects an explicit null for temperature/top_p/tools/... with a 400 on + # some of its models, and the SDK serialises an unset optional as null. + params = { + "model": self.model, + "messages": history, + } + try: + response = client.chat.completions.create(**params) + if response is None: + raise Exception("aimlapi.com response is empty.") + thought = response.choices[0].message.content + if verbose: + print(thought) + return thought + except Exception as e: + raise Exception(f"aimlapi.com API error: {str(e)}") from e + def test_fn(self, history, verbose=True): """ This function is used to conduct tests. diff --git a/tests/test_aimlapi_provider.py b/tests/test_aimlapi_provider.py new file mode 100644 index 00000000..e04a4bfc --- /dev/null +++ b/tests/test_aimlapi_provider.py @@ -0,0 +1,171 @@ +import os +import re +import sys +import unittest +from unittest.mock import patch, MagicMock + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from sources.llm_provider import ( + AIMLAPI_ATTRIBUTION_HEADERS, + AIMLAPI_DEFAULT_BASE_URL, + Provider, + aimlapi_attribution_headers, +) + +FAKE_ENV = {"AIMLAPI_API_KEY": "sk-test-123"} + + +def make_provider(is_local=False): + return Provider("aimlapi", "deepseek/deepseek-v4-flash", is_local=is_local) + + +def mocked_openai(mock_openai, content="42"): + """Wire an OpenAI mock so chat.completions.create returns `content`.""" + client = MagicMock() + client.chat.completions.create.return_value = MagicMock( + choices=[MagicMock(message=MagicMock(content=content))] + ) + mock_openai.return_value = client + return client + + +@patch.dict(os.environ, FAKE_ENV) +class TestAimlapiProvider(unittest.TestCase): + """Test cases for the aimlapi.com provider integration.""" + + def test_aimlapi_provider_registered(self): + """aimlapi is registered in available_providers.""" + self.assertIn("aimlapi", make_provider().available_providers) + + def test_aimlapi_is_unsafe_provider(self): + """aimlapi is a cloud API, so it must trigger the cloud data warning.""" + self.assertIn("aimlapi", make_provider().unsafe_providers) + + def test_aimlapi_reads_aimlapi_api_key(self): + """The key is read from AIMLAPI_API_KEY.""" + self.assertEqual(make_provider().api_key, "sk-test-123") + + def test_aimlapi_local_not_supported(self): + """aimlapi_fn refuses is_local=True.""" + provider = make_provider(is_local=True) + with self.assertRaises(Exception) as ctx: + provider.aimlapi_fn([{"role": "user", "content": "hi"}]) + self.assertIn("not available for local use", str(ctx.exception)) + + @patch('sources.llm_provider.OpenAI') + def test_aimlapi_fn_returns_content(self, mock_openai): + """aimlapi_fn returns the assistant message content.""" + mocked_openai(mock_openai) + result = make_provider().aimlapi_fn([{"role": "user", "content": "What is 6*7?"}]) + self.assertEqual(result, "42") + + @patch('sources.llm_provider.OpenAI') + def test_aimlapi_fn_uses_default_base_url(self, mock_openai): + """The OpenAI-compatible endpoint is https://api.aimlapi.com/v1.""" + mocked_openai(mock_openai) + make_provider().aimlapi_fn([{"role": "user", "content": "hi"}]) + self.assertEqual(mock_openai.call_args[1]["base_url"], AIMLAPI_DEFAULT_BASE_URL) + + @patch.dict(os.environ, {"AIMLAPI_BASE_URL": "https://proxy.example.com/v1"}) + @patch('sources.llm_provider.OpenAI') + def test_aimlapi_fn_base_url_is_overridable(self, mock_openai): + """AIMLAPI_BASE_URL overrides the endpoint.""" + mocked_openai(mock_openai) + make_provider().aimlapi_fn([{"role": "user", "content": "hi"}]) + self.assertEqual(mock_openai.call_args[1]["base_url"], "https://proxy.example.com/v1") + + @patch('sources.llm_provider.OpenAI') + def test_aimlapi_fn_passes_model_and_messages(self, mock_openai): + """The configured model and the history reach the API unchanged.""" + client = mocked_openai(mock_openai) + history = [{"role": "user", "content": "hi"}] + make_provider().aimlapi_fn(history) + call_kwargs = client.chat.completions.create.call_args[1] + self.assertEqual(call_kwargs["model"], "deepseek/deepseek-v4-flash") + self.assertEqual(call_kwargs["messages"], history) + + @patch('sources.llm_provider.OpenAI') + def test_aimlapi_fn_omits_unset_parameters(self, mock_openai): + """ + Unset optional parameters are omitted, never sent as None. + + aimlapi.com answers 400 for an explicit null on temperature, top_p, + seed, tools, tool_choice, response_format, stream and max_tokens on some + of its models, so passing None would fail live while mocks stay green. + """ + client = mocked_openai(mock_openai) + make_provider().aimlapi_fn([{"role": "user", "content": "hi"}]) + call_kwargs = client.chat.completions.create.call_args[1] + self.assertEqual( + [k for k, v in call_kwargs.items() if v is None], + [], + "no request parameter may be sent as None", + ) + + @patch('sources.llm_provider.OpenAI') + def test_aimlapi_fn_sends_attribution_headers(self, mock_openai): + """All four attribution headers are attached to the client.""" + mocked_openai(mock_openai) + make_provider().aimlapi_fn([{"role": "user", "content": "hi"}]) + headers = mock_openai.call_args[1]["default_headers"] + self.assertEqual(headers["HTTP-Referer"], "https://github.com/Fosowl/agenticSeek") + self.assertEqual(headers["X-Title"], "AgenticSeek") + self.assertEqual(headers["X-AIMLAPI-Source"], "agent/agenticseek") + self.assertEqual(headers["X-AIMLAPI-Partner-ID"], "part_agenticseek") + + def test_partner_id_matches_gateway_pattern(self): + """ + A malformed partner id is dropped silently by the gateway, so its shape + is asserted here rather than discovered in production. + """ + self.assertRegex( + AIMLAPI_ATTRIBUTION_HEADERS["X-AIMLAPI-Partner-ID"], + re.compile(r"^part_[A-Za-z0-9]{1,64}$"), + ) + + def test_source_matches_gateway_pattern(self): + """X-AIMLAPI-Source is / with channel in web|agent|mcp.""" + self.assertRegex( + AIMLAPI_ATTRIBUTION_HEADERS["X-AIMLAPI-Source"], + re.compile(r"^(web|agent|mcp)/[a-z0-9-]{1,32}$"), + ) + + def test_attribution_headers_are_scoped_to_our_origin(self): + """Attribution never rides a request to another host.""" + self.assertEqual(aimlapi_attribution_headers(AIMLAPI_DEFAULT_BASE_URL), + AIMLAPI_ATTRIBUTION_HEADERS) + self.assertEqual(aimlapi_attribution_headers("https://proxy.example.com/v1"), {}) + + def test_attribution_headers_constant_is_not_mutated(self): + """Each call gets a fresh dict; the module level constant is immutable.""" + first = aimlapi_attribution_headers(AIMLAPI_DEFAULT_BASE_URL) + first["X-Title"] = "tampered" + self.assertIsNot(first, AIMLAPI_ATTRIBUTION_HEADERS) + self.assertEqual(AIMLAPI_ATTRIBUTION_HEADERS["X-Title"], "AgenticSeek") + self.assertEqual(aimlapi_attribution_headers(AIMLAPI_DEFAULT_BASE_URL)["X-Title"], + "AgenticSeek") + + @patch('sources.llm_provider.OpenAI') + def test_aimlapi_fn_raises_on_empty_response(self, mock_openai): + """An empty response is reported instead of raising an IndexError.""" + client = MagicMock() + client.chat.completions.create.return_value = None + mock_openai.return_value = client + with self.assertRaises(Exception) as ctx: + make_provider().aimlapi_fn([{"role": "user", "content": "hi"}]) + self.assertIn("empty", str(ctx.exception).lower()) + + @patch('sources.llm_provider.OpenAI') + def test_aimlapi_fn_raises_on_api_error(self, mock_openai): + """API errors are wrapped with the provider name.""" + client = MagicMock() + client.chat.completions.create.side_effect = Exception("rate limit exceeded") + mock_openai.return_value = client + with self.assertRaises(Exception) as ctx: + make_provider().aimlapi_fn([{"role": "user", "content": "hi"}]) + self.assertIn("aimlapi.com API error", str(ctx.exception)) + + +if __name__ == '__main__': + unittest.main() From 5e3d00ba55ab50e50ed4945bf4dd6b15cfe0b5e3 Mon Sep 17 00:00:00 2001 From: aimlapi Date: Thu, 3 Sep 2026 07:53:55 +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 row to the top of the API provider table. The table is hand-ordered (roughly by age, not alphabetically), so reordering touches no generation machinery. The repo has no "recommended"/featured badge concept and none is invented here. This commit exists only so it can be dropped before any upstream PR; the provider itself is complete without it. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 964c0bf6..fb2bfdfb 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,7 @@ provider_server_address = # Typically ignored or can be left blank when is_local | Provider | `provider_name` | Local? | Description | API Key Link (Examples) | |--------------|-----------------|--------|---------------------------------------------------|---------------------------------------------| +| aimlapi.com | `aimlapi` | No | One API for 350+ chat models from many vendors (e.g. `deepseek/deepseek-v4-flash`, `anthropic/claude-sonnet-4.6`). Model ids are listed at [api.aimlapi.com/v1/models](https://api.aimlapi.com/v1/models). | [aimlapi.com/app/keys](https://aimlapi.com/app/keys) | | OpenAI | `openai` | No | Use ChatGPT models via OpenAI's API. | [platform.openai.com/signup](https://platform.openai.com/signup) | | Google Gemini| `google` | No | Use Google Gemini models via Google AI Studio. | [aistudio.google.com/keys](https://aistudio.google.com/keys) | | Deepseek | `deepseek` | No | Use Deepseek models via their API. | [platform.deepseek.com](https://platform.deepseek.com) | @@ -250,7 +251,6 @@ provider_server_address = # Typically ignored or can be left blank when is_local | TogetherAI | `togetherAI` | No | Use various open-source models via TogetherAI API.| [api.together.ai/settings/api-keys](https://api.together.ai/settings/api-keys) | | OpenRouter | `openrouter` | No | Use OpenRouter Models| [https://openrouter.ai/](https://openrouter.ai/) | | MiniMax | `minimax` | No | Use MiniMax models (e.g., MiniMax-M3, MiniMax-M2.7).| [platform.minimax.io](https://platform.minimax.io/user-center/basic-information) | -| aimlapi.com | `aimlapi` | No | One API for 350+ chat models from many vendors (e.g. `deepseek/deepseek-v4-flash`, `anthropic/claude-sonnet-4.6`). Model ids are listed at [api.aimlapi.com/v1/models](https://api.aimlapi.com/v1/models). | [aimlapi.com/app/keys](https://aimlapi.com/app/keys) | *Note:* * We advise against using `gpt-4o` or other OpenAI models for complex web browsing and task planning as current prompt optimizations are geared towards models like Deepseek. From 3316cccbfecd42d612ba6b248c948f877789854e Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 18:08:53 +0500 Subject: [PATCH 3/3] fix(aimlapi): use the registered partner id The placeholder part_agenticseek was a readable stand-in chosen before the partner was registered. Registration mints the id server-side, so the real value is part_l9PWfCWCyHgneiq7GLfDHUsf. A wrong or unknown partner id is accepted with a 200 and silently not attributed, so this would not have surfaced at runtime. --- sources/llm_provider.py | 2 +- tests/test_aimlapi_provider.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/llm_provider.py b/sources/llm_provider.py index 1056f127..b66e04a3 100644 --- a/sources/llm_provider.py +++ b/sources/llm_provider.py @@ -18,7 +18,7 @@ AIMLAPI_ATTRIBUTION_HEADERS = { "HTTP-Referer": "https://github.com/Fosowl/agenticSeek", "X-Title": "AgenticSeek", - "X-AIMLAPI-Partner-ID": "part_agenticseek", + "X-AIMLAPI-Partner-ID": "part_l9PWfCWCyHgneiq7GLfDHUsf", "X-AIMLAPI-Source": "agent/agenticseek", } diff --git a/tests/test_aimlapi_provider.py b/tests/test_aimlapi_provider.py index e04a4bfc..724391c6 100644 --- a/tests/test_aimlapi_provider.py +++ b/tests/test_aimlapi_provider.py @@ -112,7 +112,7 @@ def test_aimlapi_fn_sends_attribution_headers(self, mock_openai): self.assertEqual(headers["HTTP-Referer"], "https://github.com/Fosowl/agenticSeek") self.assertEqual(headers["X-Title"], "AgenticSeek") self.assertEqual(headers["X-AIMLAPI-Source"], "agent/agenticseek") - self.assertEqual(headers["X-AIMLAPI-Partner-ID"], "part_agenticseek") + self.assertEqual(headers["X-AIMLAPI-Partner-ID"], "part_l9PWfCWCyHgneiq7GLfDHUsf") def test_partner_id_matches_gateway_pattern(self): """