From 2d0ace613fa8cba4d743ecd3894fb0139022a341 Mon Sep 17 00:00:00 2001 From: aimlapi Date: Thu, 3 Sep 2026 12:54:48 +0500 Subject: [PATCH 1/4] feat(llm): attribute traffic sent to aimlapi.com MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EverOS reaches every model through the same OpenAI-protocol clients, so an aimlapi.com key already works today by setting base_url alone. What does not work is attribution: aimlapi.com credits the project that sent the traffic only when the request carries partner headers, and a missing header fails silently — the call succeeds, the credit is simply lost. The headers cannot be pinned to the SDK layer, because the same client class is what talks to OpenRouter, OpenAI, DeepInfra and vLLM. So the helper is origin-scoped: it parses the configured base_url and returns an empty mapping for any host that is not aimlapi.com, which is why the LLM, multimodal and embedding clients can all call it unconditionally. Matching on the parsed hostname at a dot boundary keeps a lookalike host (api.aimlapi.com.example.net) from collecting headers meant for us, and returning a fresh dict each call keeps callers from mutating a shared constant. Where a client accepts only per-request options — the everalgo LLMConfig passthrough — the headers ride extra_headers, which the openai SDK forwards as headers rather than as body fields; for other providers no key is added to the request at all, since some upstreams reject a null-valued option outright. Tests pin the partner id against ^part_[A-Za-z0-9]{1,64}$, because a malformed id is accepted by the API and then earns nothing. --- .../component/embedding/openai_provider.py | 4 + src/everos/component/llm/client.py | 5 + src/everos/component/llm/openai_provider.py | 5 + src/everos/component/utils/__init__.py | 7 + src/everos/component/utils/attribution.py | 94 ++++++++++++++ .../test_embedding/test_openai_provider.py | 9 ++ .../test_llm/test_attribution_wiring.py | 120 ++++++++++++++++++ .../test_utils/test_attribution.py | 99 +++++++++++++++ 8 files changed, 343 insertions(+) create mode 100644 src/everos/component/utils/attribution.py create mode 100644 tests/unit/test_component/test_llm/test_attribution_wiring.py create mode 100644 tests/unit/test_component/test_utils/test_attribution.py diff --git a/src/everos/component/embedding/openai_provider.py b/src/everos/component/embedding/openai_provider.py index b556b94b3..a83302ca9 100644 --- a/src/everos/component/embedding/openai_provider.py +++ b/src/everos/component/embedding/openai_provider.py @@ -24,6 +24,7 @@ import openai +from everos.component.utils.attribution import aimlapi_headers from everos.core.observability.tracing import memory_span, set_generation_usage from .protocol import EmbeddingServiceError @@ -67,11 +68,14 @@ def __init__( self._model = model self._batch_size = batch_size self._semaphore = asyncio.Semaphore(max_concurrent) + # Partner attribution, merged into (not over) the SDK's own + # defaults and empty unless ``base_url`` is an aimlapi.com host. self._client = openai.AsyncOpenAI( api_key=api_key, base_url=base_url, timeout=timeout, max_retries=max_retries, + default_headers=aimlapi_headers(base_url) or None, ) async def embed(self, text: str) -> list[float]: diff --git a/src/everos/component/llm/client.py b/src/everos/component/llm/client.py index 6d48804fb..48a052cb9 100644 --- a/src/everos/component/llm/client.py +++ b/src/everos/component/llm/client.py @@ -17,6 +17,7 @@ from everalgo.llm.types import ChatMessage, ChatResponse from pydantic import BaseModel +from everos.component.utils.attribution import aimlapi_request_extra from everos.component.utils.config_hints import missing_config_error from everos.config import load_settings from everos.core.observability.logging import get_logger @@ -101,6 +102,9 @@ def get_llm_client() -> LLMClient: model=llm_cfg.model, api_key=api_key, base_url=llm_cfg.base_url, + # Empty for every other endpoint, so no key is added to the + # request body and no header can reach a foreign provider. + extra=aimlapi_request_extra(llm_cfg.base_url), ) ) # Wrap for OTel token capture only when tracing is on — keeps the @@ -141,6 +145,7 @@ def get_multimodal_llm_client() -> LLMClient: model=cfg.model, api_key=api_key, base_url=cfg.base_url, + extra=aimlapi_request_extra(cfg.base_url), ) ) logger.info("multimodal_llm_client_built", model=cfg.model) diff --git a/src/everos/component/llm/openai_provider.py b/src/everos/component/llm/openai_provider.py index c73d2f5af..917717929 100644 --- a/src/everos/component/llm/openai_provider.py +++ b/src/everos/component/llm/openai_provider.py @@ -18,6 +18,8 @@ import openai +from everos.component.utils.attribution import aimlapi_headers + from .protocol import ChatMessage, ChatResponse, LLMError, Usage @@ -54,10 +56,13 @@ def __init__( self._model = model self._temperature = temperature self._max_tokens = max_tokens + # Partner attribution, merged into (not over) the SDK's own + # defaults and empty unless ``base_url`` is an aimlapi.com host. self._client = openai.AsyncOpenAI( api_key=api_key, base_url=base_url, timeout=timeout, + default_headers=aimlapi_headers(base_url) or None, ) async def chat( diff --git a/src/everos/component/utils/__init__.py b/src/everos/component/utils/__init__.py index e9cada50e..9b5228172 100644 --- a/src/everos/component/utils/__init__.py +++ b/src/everos/component/utils/__init__.py @@ -19,4 +19,11 @@ tokens_for_query, join_tokens, ) + from everos.component.utils.attribution import ( + AIMLAPI_BASE_URL, + AIMLAPI_DISPLAY_NAME, + aimlapi_headers, + aimlapi_request_extra, + is_aimlapi_base_url, + ) """ diff --git a/src/everos/component/utils/attribution.py b/src/everos/component/utils/attribution.py new file mode 100644 index 000000000..da19adf61 --- /dev/null +++ b/src/everos/component/utils/attribution.py @@ -0,0 +1,94 @@ +"""Partner attribution headers for aimlapi.com endpoints. + +aimlapi.com credits the projects that send it traffic, but only when the +request carries the partner headers below. EverOS reaches every model +provider through the same OpenAI-protocol clients, so the headers cannot +be attached at the SDK layer without leaking to whichever endpoint the +user happens to configure. Instead every helper here is *origin-scoped*: +it inspects the configured ``base_url`` and returns an empty mapping for +anything that is not an aimlapi.com host, so the headers can never ride a +request to OpenRouter, DeepInfra, OpenAI or a proxy in front of them. + +The returned mapping is always a fresh ``dict`` — callers merge it into +their own header set, and no shared constant is ever handed out for +mutation. +""" + +from __future__ import annotations + +from typing import Any +from urllib.parse import urlsplit + +AIMLAPI_BASE_URL = "https://api.aimlapi.com/v1" +"""Chat-completions / embeddings base URL for aimlapi.com.""" + +AIMLAPI_DISPLAY_NAME = "aimlapi.com" +"""Human-facing provider label, as the provider spells it.""" + +_AIMLAPI_DOMAIN = "aimlapi.com" + +# Identifies EverOS to aimlapi.com. Must match ``^part_[A-Za-z0-9]{1,64}$`` +# — a malformed id is accepted by the API and then silently unattributed. +_PARTNER_ID = "part_everos" +_SOURCE = "agent/everos" + +# ``HTTP-Referer`` / ``X-Title`` name the *host* project (EverOS), the +# same convention OpenRouter uses for app attribution. +_REFERER = "https://github.com/EverMind-AI/EverOS" +_TITLE = "EverOS" + + +def is_aimlapi_base_url(base_url: str | None) -> bool: + """Return whether ``base_url`` points at an aimlapi.com host. + + Matches on the parsed hostname only, on a dot boundary, so lookalike + hosts such as ``api.aimlapi.com.example.net`` do not match. + + Args: + base_url: Configured OpenAI-protocol endpoint, or ``None``. + + Returns: + ``True`` when the host is ``aimlapi.com`` or a subdomain of it. + """ + if not base_url: + return False + host = (urlsplit(base_url).hostname or "").lower() + return host == _AIMLAPI_DOMAIN or host.endswith(f".{_AIMLAPI_DOMAIN}") + + +def aimlapi_headers(base_url: str | None) -> dict[str, str]: + """Return the partner attribution headers for an aimlapi.com endpoint. + + Args: + base_url: Configured OpenAI-protocol endpoint, or ``None``. + + Returns: + A new ``dict`` of headers when ``base_url`` is an aimlapi.com + host, otherwise an empty ``dict``. Never returns a shared object. + """ + if not is_aimlapi_base_url(base_url): + return {} + return { + "X-AIMLAPI-Partner-ID": _PARTNER_ID, + "X-AIMLAPI-Source": _SOURCE, + "HTTP-Referer": _REFERER, + "X-Title": _TITLE, + } + + +def aimlapi_request_extra(base_url: str | None) -> dict[str, Any]: + """Return per-request kwargs carrying the attribution headers. + + Shaped for clients that only accept extra *request* options (the + everalgo ``LLMConfig.extra`` passthrough), where ``extra_headers`` is + forwarded by the openai SDK as headers rather than as body fields. + + Args: + base_url: Configured OpenAI-protocol endpoint, or ``None``. + + Returns: + ``{"extra_headers": {...}}`` for an aimlapi.com host, otherwise an + empty ``dict`` so no key is added to the request at all. + """ + headers = aimlapi_headers(base_url) + return {"extra_headers": headers} if headers else {} diff --git a/tests/unit/test_component/test_embedding/test_openai_provider.py b/tests/unit/test_component/test_embedding/test_openai_provider.py index abc6c4232..9ee6726d3 100644 --- a/tests/unit/test_component/test_embedding/test_openai_provider.py +++ b/tests/unit/test_component/test_embedding/test_openai_provider.py @@ -35,3 +35,12 @@ async def test_empty_response_data_raises_embedding_error() -> None: with pytest.raises(EmbeddingServiceError, match="empty data"): await provider.embed("hello") + + +def test_attribution_headers_sent_only_to_aimlapi() -> None: + """Partner headers ride aimlapi.com requests and no others.""" + ours = _make_provider(base_url="https://api.aimlapi.com/v1") + assert ours._client.default_headers["X-AIMLAPI-Partner-ID"] == "part_everos" + + theirs = _make_provider(base_url="https://api.deepinfra.com/v1/openai") + assert "X-AIMLAPI-Partner-ID" not in theirs._client.default_headers diff --git a/tests/unit/test_component/test_llm/test_attribution_wiring.py b/tests/unit/test_component/test_llm/test_attribution_wiring.py new file mode 100644 index 000000000..cab1a8012 --- /dev/null +++ b/tests/unit/test_component/test_llm/test_attribution_wiring.py @@ -0,0 +1,120 @@ +"""Attribution headers reach the wire, and only for aimlapi.com. + +The header helper is unit-tested separately; what breaks silently is the +*wiring* — a client built without the headers still works, just +unattributed, so nothing fails loudly. These tests pin that each client +construction site actually forwards them, and that a non-aimlapi +``base_url`` adds no request key at all (a stray ``extra_headers`` or a +``None`` valued key is a 400 on some upstreams). +""" + +from __future__ import annotations + +import importlib +from typing import Any + +import pytest +from pydantic import SecretStr + +from everos.component.llm.openai_provider import OpenAIProvider +from everos.config import Settings +from everos.config.settings import LLMSettings, MultimodalSettings + +_client_mod = importlib.import_module("everos.component.llm.client") + +_AIMLAPI = "https://api.aimlapi.com/v1" +_OPENROUTER = "https://openrouter.ai/api/v1" + + +def _recorder(captured: dict[str, Any]) -> Any: + """Return a ``build_client`` stub that records the config it is given.""" + + def _build(cfg: Any) -> object: + captured["cfg"] = cfg + return object() + + return _build + + +def _capture_llm_config( + monkeypatch: pytest.MonkeyPatch, *, base_url: str +) -> dict[str, Any]: + """Build the LLM singleton against ``base_url`` and return its config.""" + captured: dict[str, Any] = {} + monkeypatch.setattr(_client_mod, "_llm_client", None, raising=False) + monkeypatch.setattr( + _client_mod, + "load_settings", + lambda: Settings( + llm=LLMSettings( + model="openai/gpt-4.1-mini", + api_key=SecretStr("sk-test"), + base_url=base_url, + ) + ), + ) + monkeypatch.setattr(_client_mod, "build_client", _recorder(captured)) + _client_mod.get_llm_client() + return captured["cfg"].extra + + +def _capture_multimodal_config( + monkeypatch: pytest.MonkeyPatch, *, base_url: str +) -> dict[str, Any]: + """Build the multimodal singleton and return its config ``extra``.""" + captured: dict[str, Any] = {} + monkeypatch.setattr(_client_mod, "_multimodal_client", None, raising=False) + monkeypatch.setattr( + _client_mod, + "load_settings", + lambda: Settings( + multimodal=MultimodalSettings( + model="google/gemini-3-flash-preview", + api_key=SecretStr("sk-test"), + base_url=base_url, + ) + ), + ) + monkeypatch.setattr(_client_mod, "build_client", _recorder(captured)) + _client_mod.get_multimodal_llm_client() + return captured["cfg"].extra + + +def test_llm_client_sends_attribution_to_aimlapi( + monkeypatch: pytest.MonkeyPatch, +) -> None: + extra = _capture_llm_config(monkeypatch, base_url=_AIMLAPI) + assert extra["extra_headers"]["X-AIMLAPI-Partner-ID"] == "part_everos" + assert extra["extra_headers"]["X-AIMLAPI-Source"] == "agent/everos" + + +def test_llm_client_adds_no_request_key_for_other_providers( + monkeypatch: pytest.MonkeyPatch, +) -> None: + assert _capture_llm_config(monkeypatch, base_url=_OPENROUTER) == {} + + +def test_multimodal_client_sends_attribution_to_aimlapi( + monkeypatch: pytest.MonkeyPatch, +) -> None: + extra = _capture_multimodal_config(monkeypatch, base_url=_AIMLAPI) + assert extra["extra_headers"]["X-AIMLAPI-Partner-ID"] == "part_everos" + + +def test_multimodal_client_adds_no_request_key_for_other_providers( + monkeypatch: pytest.MonkeyPatch, +) -> None: + assert _capture_multimodal_config(monkeypatch, base_url=_OPENROUTER) == {} + + +def test_openai_provider_sets_default_headers_for_aimlapi() -> None: + provider = OpenAIProvider(model="m", api_key="sk-test", base_url=_AIMLAPI) + sent = provider._client.default_headers + assert sent["X-AIMLAPI-Partner-ID"] == "part_everos" + # The SDK's own defaults survive — the partner headers merge in. + assert "Content-Type" in sent + + +def test_openai_provider_sends_no_partner_headers_elsewhere() -> None: + provider = OpenAIProvider(model="m", api_key="sk-test", base_url=_OPENROUTER) + assert "X-AIMLAPI-Partner-ID" not in provider._client.default_headers diff --git a/tests/unit/test_component/test_utils/test_attribution.py b/tests/unit/test_component/test_utils/test_attribution.py new file mode 100644 index 000000000..a80a479f9 --- /dev/null +++ b/tests/unit/test_component/test_utils/test_attribution.py @@ -0,0 +1,99 @@ +"""Attribution headers are well-formed and scoped to aimlapi.com only. + +Pins three contracts that fail silently in production if broken: + +1. A malformed partner id is accepted by the API and then earns nothing, + so the id is asserted against the documented pattern. +2. The headers must never be attached to a request bound for another + provider, so every non-aimlapi host must yield an empty mapping. +3. Callers merge the result into their own header set, so a fresh dict + must be returned each call and never a shared constant. +""" + +from __future__ import annotations + +import re + +import pytest + +from everos.component.utils.attribution import ( + AIMLAPI_BASE_URL, + AIMLAPI_DISPLAY_NAME, + aimlapi_headers, + aimlapi_request_extra, + is_aimlapi_base_url, +) + +_PARTNER_ID_PATTERN = re.compile(r"^part_[A-Za-z0-9]{1,64}$") + +_EXPECTED_KEYS = { + "X-AIMLAPI-Partner-ID", + "X-AIMLAPI-Source", + "HTTP-Referer", + "X-Title", +} + + +def test_partner_id_matches_documented_pattern() -> None: + headers = aimlapi_headers(AIMLAPI_BASE_URL) + assert _PARTNER_ID_PATTERN.match(headers["X-AIMLAPI-Partner-ID"]) + + +def test_all_four_attribution_headers_are_present() -> None: + assert set(aimlapi_headers(AIMLAPI_BASE_URL)) == _EXPECTED_KEYS + + +def test_referer_and_title_name_the_host_project_not_the_provider() -> None: + headers = aimlapi_headers(AIMLAPI_BASE_URL) + assert headers["HTTP-Referer"] == "https://github.com/EverMind-AI/EverOS" + assert headers["X-Title"] == "EverOS" + + +def test_display_name_is_the_provider_spelling() -> None: + assert AIMLAPI_DISPLAY_NAME == "aimlapi.com" + + +@pytest.mark.parametrize( + "base_url", + [ + "https://api.aimlapi.com/v1", + "https://api.aimlapi.com/v1/", + "https://AIMLAPI.com/v1", + "http://api.aimlapi.com/v1", + ], +) +def test_aimlapi_hosts_are_recognised(base_url: str) -> None: + assert is_aimlapi_base_url(base_url) + assert set(aimlapi_headers(base_url)) == _EXPECTED_KEYS + + +@pytest.mark.parametrize( + "base_url", + [ + None, + "", + "https://openrouter.ai/api/v1", + "https://api.openai.com/v1", + "https://api.deepinfra.com/v1/openai", + # Lookalike hosts: a proxy fronting us, or an outright imposter. + "https://api.aimlapi.com.example.net/v1", + "https://not-aimlapi.com/v1", + "https://proxy.example.net/?upstream=api.aimlapi.com", + ], +) +def test_no_headers_leak_to_other_origins(base_url: str | None) -> None: + assert not is_aimlapi_base_url(base_url) + assert aimlapi_headers(base_url) == {} + assert aimlapi_request_extra(base_url) == {} + + +def test_request_extra_wraps_headers_for_the_sdk() -> None: + extra = aimlapi_request_extra(AIMLAPI_BASE_URL) + assert set(extra) == {"extra_headers"} + assert set(extra["extra_headers"]) == _EXPECTED_KEYS + + +def test_each_call_returns_a_fresh_mapping() -> None: + first = aimlapi_headers(AIMLAPI_BASE_URL) + first["X-Title"] = "mutated" + assert aimlapi_headers(AIMLAPI_BASE_URL)["X-Title"] == "EverOS" From 81eb32f8c8c8696f62bc46e2690b582b048b7b23 Mon Sep 17 00:00:00 2001 From: aimlapi Date: Thu, 3 Sep 2026 12:56:00 +0500 Subject: [PATCH 2/4] docs(config): document aimlapi.com as an LLM endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shipped `[llm]` and `[multimodal]` model slugs are already spelled the way aimlapi.com spells them, so pointing EverOS at it is a base_url and key change with no model rewriting — worth saying out loud, because the docs currently read as if OpenRouter were a dependency rather than one choice of OpenAI-protocol endpoint. Both shipped default models were called live through the component/llm client path before this was written, structured-output path included. The multimodal caveat is recorded rather than papered over: the image parts EverOS sends carry `image_url.detail = null`, which aimlapi.com rejects with a 400 while OpenAI and OpenRouter accept it. Anyone who switches `[multimodal]` over would otherwise hit it with no explanation. `.env.example` is regenerated from the template it must match, per the `make docs-check` gate. --- .env.example | 5 +++-- config.example.toml | 3 +++ docs/configuration.md | 21 +++++++++++++++++++++ src/everos/config/default.toml | 4 ++++ src/everos/templates/env.template | 5 +++-- 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index ad02bee04..d7749d775 100644 --- a/.env.example +++ b/.env.example @@ -23,8 +23,9 @@ # ─── LLM (OpenAI-protocol compatible) ──────────────── # Any OpenAI-API-compatible endpoint plugs in via base_url. Defaults # below target OpenRouter (one key, broad model catalogue); switch to -# OpenAI, vLLM, Ollama (OpenAI bridge), or any other compatible endpoint -# by changing model + base_url + api_key. +# aimlapi.com (https://api.aimlapi.com/v1 — same slug spelling, so the +# model below is unchanged), OpenAI, vLLM, Ollama (OpenAI bridge), or any +# other compatible endpoint by changing model + base_url + api_key. EVEROS_LLM__MODEL=openai/gpt-4.1-mini EVEROS_LLM__API_KEY= diff --git a/config.example.toml b/config.example.toml index f8043127f..25ed247f4 100644 --- a/config.example.toml +++ b/config.example.toml @@ -20,6 +20,9 @@ # ── LLM ─────────────────────────────────────────────── # OpenAI-protocol chat-completions endpoint used by the algo extractors. +# Alternatives, same protocol — swap all three fields together: +# aimlapi.com model = "openai/gpt-4.1-mini", base_url = "https://api.aimlapi.com/v1" +# OpenRouter model = "openai/gpt-4.1-mini", base_url = "https://openrouter.ai/api/v1" [llm] model = "gpt-4.1-mini" api_key = "sk-..." diff --git a/docs/configuration.md b/docs/configuration.md index 98fdf3479..18c5c34cc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -107,6 +107,27 @@ everos init --root /data/everos | `api_key` | string | — | **Yes** | API key for the LLM provider. | | `base_url` | string | — | No | Custom endpoint URL (OpenAI-compatible). | +#### Provider endpoints + +Any OpenAI-protocol chat-completions endpoint works. Two aggregators cover +the shipped model slugs without rewriting them: + +| Provider | `base_url` | Notes | +|---|---|---| +| aimlapi.com | `https://api.aimlapi.com/v1` | Same `vendor/model` slug convention, so `[llm]` and `[multimodal]` defaults work unchanged. | +| OpenRouter | `https://openrouter.ai/api/v1` | Historical default. | + +Both `openai/gpt-4.1-mini` and `google/gemini-3-flash-preview` were called +live against aimlapi.com and answered, including the structured-output +(`response_format`) path the extractors use. + +> **Known limitation — multimodal against aimlapi.com.** The image content +> parts EverOS sends carry `image_url.detail = null`, which aimlapi.com +> rejects with HTTP 400 (`messages.0.content` / `invalid_union`) where +> OpenAI and OpenRouter accept it. Text-only calls to `[multimodal]` are +> fine; keep `[multimodal]` on a provider that tolerates the null field +> until either side changes. + ### `[multimodal]` | Field | Type | Default | Required | Description | diff --git a/src/everos/config/default.toml b/src/everos/config/default.toml index e2137f84a..72be4f39e 100644 --- a/src/everos/config/default.toml +++ b/src/everos/config/default.toml @@ -54,6 +54,10 @@ cache_size_kb = 2048 # Provider-agnostic OpenAI-protocol client config. Override via env: # EVEROS_LLM__MODEL, EVEROS_LLM__API_KEY, EVEROS_LLM__BASE_URL # Or set the field directly in this file (/everos.toml). +# The model slug below is spelled the same way by both aggregators, so +# only base_url + api_key change: +# aimlapi.com -> https://api.aimlapi.com/v1 +# OpenRouter -> https://openrouter.ai/api/v1 model = "openai/gpt-4.1-mini" api_key = "" base_url = "https://openrouter.ai/api/v1" diff --git a/src/everos/templates/env.template b/src/everos/templates/env.template index ad02bee04..d7749d775 100755 --- a/src/everos/templates/env.template +++ b/src/everos/templates/env.template @@ -23,8 +23,9 @@ # ─── LLM (OpenAI-protocol compatible) ──────────────── # Any OpenAI-API-compatible endpoint plugs in via base_url. Defaults # below target OpenRouter (one key, broad model catalogue); switch to -# OpenAI, vLLM, Ollama (OpenAI bridge), or any other compatible endpoint -# by changing model + base_url + api_key. +# aimlapi.com (https://api.aimlapi.com/v1 — same slug spelling, so the +# model below is unchanged), OpenAI, vLLM, Ollama (OpenAI bridge), or any +# other compatible endpoint by changing model + base_url + api_key. EVEROS_LLM__MODEL=openai/gpt-4.1-mini EVEROS_LLM__API_KEY= From cb397ff9abef18202be1d62994a00fb48dd2fc8e Mon Sep 17 00:00:00 2001 From: aimlapi Date: Thu, 3 Sep 2026 12:56:57 +0500 Subject: [PATCH 3/4] =?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 Leads the Quick Start with aimlapi.com instead of OpenRouter. This is a placement preference, not a correctness change: both endpoints spell the shipped model slugs identically, so the walkthrough works either way and the alternative stays one line below. Separated from the preceding commits so it can be dropped wholesale before the docs change is offered upstream, where choosing a default provider is the maintainers' call and not a contributor's. EverOS has no provider registry, list, or featured-badge mechanism, so there is nothing to reorder and no badge convention to follow — the Quick Start is the only place a provider is named prominently. --- QUICKSTART.md | 25 ++++++++++++++----------- README.md | 25 ++++++++++++++++--------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/QUICKSTART.md b/QUICKSTART.md index 52e5216e9..ef469ab9b 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -1,10 +1,10 @@ # Quickstart -> Five minutes from one OpenRouter API key to durable Markdown memory and +> Five minutes from one aimlapi.com API key to durable Markdown memory and > keyword recall. EverOS runs as a local service. The minimum production path needs only an LLM: -configure one OpenRouter key, start the server, then call the HTTP API. +configure one aimlapi.com key, start the server, then call the HTTP API. ## What the one-key setup includes @@ -21,7 +21,8 @@ They are not required for this walkthrough. ## Prerequisites - Python 3.12+ -- One [OpenRouter API key](https://openrouter.ai/keys) +- One [aimlapi.com API key](https://aimlapi.com/app/keys) (recommended), or an + [OpenRouter API key](https://openrouter.ai/keys) ## 1. Install @@ -82,19 +83,21 @@ This creates two files under the default memory root: To use another root, run `everos init --root ` and pass the same `--root ` to subsequent commands. -## 4. Add your OpenRouter key +## 4. Add your API key -Open `~/.everos/everos.toml`. The generated -`[llm]` section already contains the recommended model and base URL; replace -only the empty `api_key`: +Open `~/.everos/everos.toml`. The generated `[llm]` section already contains +the recommended model slug; set `api_key` and `base_url`: ```toml [llm] model = "openai/gpt-4.1-mini" -api_key = "" -base_url = "https://openrouter.ai/api/v1" +api_key = "" +base_url = "https://api.aimlapi.com/v1" ``` +The same slug works on OpenRouter — swap `base_url` for +`https://openrouter.ai/api/v1` and use an OpenRouter key instead. + Leave `[embedding]`, `[rerank]`, and `[multimodal]` unchanged for this walkthrough. Their empty keys do not prevent the server from starting; this setup uses keyword search. @@ -208,7 +211,7 @@ Cap. If the first search is empty, wait a moment for cascade indexing and retry. Keyword retrieval returns matching episodes from the local BM25 index. Atomic facts are created by an embedding-dependent strategy, so they are not expected -in the OpenRouter Tier 1 response. +in the Tier 1 response. ## 9. Read the Markdown source of truth @@ -254,7 +257,7 @@ EverOS reports unavailable features through `/health`. Requests that require a missing provider fail fast with a descriptive HTTP 422 instead of silently degrading to a different search method. -You can replace OpenRouter with another OpenAI-compatible LLM endpoint by +You can replace aimlapi.com with another OpenAI-compatible LLM endpoint by changing the `[llm]` model, base URL, and key. ## Stop the server diff --git a/README.md b/README.md index e4f21d405..838020bd8 100644 --- a/README.md +++ b/README.md @@ -106,13 +106,15 @@ built into Raven. Choose an integration to open its setup guide. ## Quick Start -> One OpenRouter API key is enough to start EverOS, write durable memories, +> One aimlapi.com API key is enough to start EverOS, write durable memories, > and retrieve them with keyword search. ### Prerequisites - Python 3.12+ -- One [OpenRouter API key](https://openrouter.ai/keys) +- One [aimlapi.com API key](https://aimlapi.com/app/keys) (recommended), or an + [OpenRouter API key](https://openrouter.ai/keys) — the shipped model slugs + are spelled the same on both, so only `base_url` differs. ### 1. Install @@ -139,23 +141,26 @@ the memory move through ingest -> extract -> index -> recall. -### 3. Initialize and add your OpenRouter key +### 3. Initialize and add your API key ```bash everos init ``` This creates `~/.everos/everos.toml` and `~/.everos/ome.toml`. Open -`~/.everos/everos.toml`; the generated model and OpenRouter URL are already -correct, so replace only the empty `api_key`: +`~/.everos/everos.toml`; the generated model slug is already correct, so set +`api_key` and `base_url`: ```toml [llm] model = "openai/gpt-4.1-mini" -api_key = "" -base_url = "https://openrouter.ai/api/v1" +api_key = "" +base_url = "https://api.aimlapi.com/v1" ``` +To use OpenRouter instead, keep the same `model` and set +`base_url = "https://openrouter.ai/api/v1"`. + This is the smallest Tier 1 setup: memory add, flush, Markdown persistence, cascade indexing, and keyword search. @@ -242,7 +247,7 @@ For annotated responses and the Markdown files EverOS creates, see ### What works with one key? -The OpenRouter one-key setup is EverOS Tier 1. It supports server startup, +The one-key setup above is EverOS Tier 1. It supports server startup, memory add and flush, durable Markdown storage, cascade indexing, and keyword search. Add optional providers only when you need the features below: @@ -273,7 +278,9 @@ uv pip install 'everos[multimodal]' # or: pip install 'everos[multimodal]' This pulls in `everalgo-parser` (with the `[svg]` bundle for SVG support via cairosvg). Configure the `[multimodal]` section in `everos.toml`; its default -model is `google/gemini-3-flash-preview` via OpenRouter. +model is `google/gemini-3-flash-preview` via OpenRouter. Point `[multimodal]` +at OpenRouter rather than aimlapi.com — see the limitation noted in +[docs/configuration.md](docs/configuration.md). **Office document support requires LibreOffice as a system dependency.** The parser shells out to `soffice` (LibreOffice's headless renderer) to From eac45d86ada8dc8b063ea79c75e16214d5000e2d Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 18:11:54 +0500 Subject: [PATCH 4/4] fix(aimlapi): use the registered partner id The placeholder part_everos was a readable stand-in chosen before the partner was registered. Registration mints the id server-side, so the real value is part_VxTyAUvoIVbl30dPrB7kbRZk. A wrong or unknown partner id is accepted with a 200 and silently not attributed, so this would not have surfaced at runtime. --- src/everos/component/utils/attribution.py | 2 +- .../test_component/test_embedding/test_openai_provider.py | 2 +- .../unit/test_component/test_llm/test_attribution_wiring.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/everos/component/utils/attribution.py b/src/everos/component/utils/attribution.py index da19adf61..3021b27a6 100644 --- a/src/everos/component/utils/attribution.py +++ b/src/everos/component/utils/attribution.py @@ -29,7 +29,7 @@ # Identifies EverOS to aimlapi.com. Must match ``^part_[A-Za-z0-9]{1,64}$`` # — a malformed id is accepted by the API and then silently unattributed. -_PARTNER_ID = "part_everos" +_PARTNER_ID = "part_VxTyAUvoIVbl30dPrB7kbRZk" _SOURCE = "agent/everos" # ``HTTP-Referer`` / ``X-Title`` name the *host* project (EverOS), the diff --git a/tests/unit/test_component/test_embedding/test_openai_provider.py b/tests/unit/test_component/test_embedding/test_openai_provider.py index 9ee6726d3..4edd20a2a 100644 --- a/tests/unit/test_component/test_embedding/test_openai_provider.py +++ b/tests/unit/test_component/test_embedding/test_openai_provider.py @@ -40,7 +40,7 @@ async def test_empty_response_data_raises_embedding_error() -> None: def test_attribution_headers_sent_only_to_aimlapi() -> None: """Partner headers ride aimlapi.com requests and no others.""" ours = _make_provider(base_url="https://api.aimlapi.com/v1") - assert ours._client.default_headers["X-AIMLAPI-Partner-ID"] == "part_everos" + assert ours._client.default_headers["X-AIMLAPI-Partner-ID"] == "part_VxTyAUvoIVbl30dPrB7kbRZk" theirs = _make_provider(base_url="https://api.deepinfra.com/v1/openai") assert "X-AIMLAPI-Partner-ID" not in theirs._client.default_headers diff --git a/tests/unit/test_component/test_llm/test_attribution_wiring.py b/tests/unit/test_component/test_llm/test_attribution_wiring.py index cab1a8012..ef9ac2d86 100644 --- a/tests/unit/test_component/test_llm/test_attribution_wiring.py +++ b/tests/unit/test_component/test_llm/test_attribution_wiring.py @@ -84,7 +84,7 @@ def test_llm_client_sends_attribution_to_aimlapi( monkeypatch: pytest.MonkeyPatch, ) -> None: extra = _capture_llm_config(monkeypatch, base_url=_AIMLAPI) - assert extra["extra_headers"]["X-AIMLAPI-Partner-ID"] == "part_everos" + assert extra["extra_headers"]["X-AIMLAPI-Partner-ID"] == "part_VxTyAUvoIVbl30dPrB7kbRZk" assert extra["extra_headers"]["X-AIMLAPI-Source"] == "agent/everos" @@ -98,7 +98,7 @@ def test_multimodal_client_sends_attribution_to_aimlapi( monkeypatch: pytest.MonkeyPatch, ) -> None: extra = _capture_multimodal_config(monkeypatch, base_url=_AIMLAPI) - assert extra["extra_headers"]["X-AIMLAPI-Partner-ID"] == "part_everos" + assert extra["extra_headers"]["X-AIMLAPI-Partner-ID"] == "part_VxTyAUvoIVbl30dPrB7kbRZk" def test_multimodal_client_adds_no_request_key_for_other_providers( @@ -110,7 +110,7 @@ def test_multimodal_client_adds_no_request_key_for_other_providers( def test_openai_provider_sets_default_headers_for_aimlapi() -> None: provider = OpenAIProvider(model="m", api_key="sk-test", base_url=_AIMLAPI) sent = provider._client.default_headers - assert sent["X-AIMLAPI-Partner-ID"] == "part_everos" + assert sent["X-AIMLAPI-Partner-ID"] == "part_VxTyAUvoIVbl30dPrB7kbRZk" # The SDK's own defaults survive — the partner headers merge in. assert "Content-Type" in sent