Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
3 changes: 3 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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-..."
Expand Down
21 changes: 21 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,27 @@ Zilliz Cloud endpoint; a Milvus Lite filesystem path is rejected.
| `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 the shipped `[llm]` default works unchanged. |
| OpenRouter | `https://openrouter.ai/api/v1` | Historical default. |

The shipped `[llm]` default `openai/gpt-4.1-mini` was 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 |
Expand Down
4 changes: 4 additions & 0 deletions src/everos/component/embedding/openai_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import openai

from everos.component.utils.attribution import aimlapi_headers
from everos.core.errors import EmbeddingInputError
from everos.core.observability.tracing import memory_span, set_generation_usage

Expand Down Expand Up @@ -87,11 +88,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]:
Expand Down
9 changes: 8 additions & 1 deletion src/everos/component/llm/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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 Settings, load_settings
from everos.core.observability.logging import get_logger
Expand Down Expand Up @@ -105,7 +106,12 @@ def get_llm_client() -> LLMClient:
api_key=api_key,
base_url=llm_cfg.base_url,
timeout=llm_cfg.timeout_seconds,
extra=dict(llm_cfg.extra),
# Attribution first, ``[llm].extra`` second, so a configured
# key keeps winning on collision. For every non-aimlapi
# endpoint the attribution mapping is empty, so no key is
# added to the request and no header can reach a foreign
# provider.
extra={**aimlapi_request_extra(llm_cfg.base_url), **llm_cfg.extra},
)
)
# Wrap for OTel token capture only when tracing is on — keeps the
Expand Down Expand Up @@ -251,6 +257,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)
Expand Down
6 changes: 6 additions & 0 deletions src/everos/component/llm/openai_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import openai

from everos.component.utils.attribution import aimlapi_headers

from .protocol import ChatMessage, ChatResponse, LLMError, Usage


Expand Down Expand Up @@ -75,6 +77,10 @@ def __init__(
}
if max_retries is not None:
client_kwargs["max_retries"] = max_retries
# Partner attribution, merged into (not over) the SDK's own
# defaults and absent unless ``base_url`` is an aimlapi.com host.
if attribution := aimlapi_headers(base_url):
client_kwargs["default_headers"] = attribution
self._client = openai.AsyncOpenAI(**client_kwargs) # type: ignore[arg-type]

async def chat(
Expand Down
7 changes: 7 additions & 0 deletions src/everos/component/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
"""
94 changes: 94 additions & 0 deletions src/everos/component/utils/attribution.py
Original file line number Diff line number Diff line change
@@ -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_VxTyAUvoIVbl30dPrB7kbRZk"
_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 {}
4 changes: 4 additions & 0 deletions src/everos/config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ collection_prefix = "everos"
# 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 (<root>/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"
Expand Down
5 changes: 3 additions & 2 deletions src/everos/templates/env.template
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/test_component/test_embedding/test_openai_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,13 @@ 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")
sent = ours._client.default_headers["X-AIMLAPI-Partner-ID"]
assert sent == "part_VxTyAUvoIVbl30dPrB7kbRZk"

theirs = _make_provider(base_url="https://api.deepinfra.com/v1/openai")
assert "X-AIMLAPI-Partner-ID" not in theirs._client.default_headers
Loading