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
25 changes: 14 additions & 11 deletions QUICKSTART.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand Down Expand Up @@ -82,19 +83,21 @@ This creates two files under the default memory root:
To use another root, run `everos init --root <path>` and pass the same
`--root <path>` 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 = "<OPENROUTER_API_KEY>"
base_url = "https://openrouter.ai/api/v1"
api_key = "<AIMLAPI_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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
25 changes: 16 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -139,23 +141,26 @@ the memory move through ingest -> extract -> index -> recall.

<https://github.com/user-attachments/assets/98cb8e1e-2ca8-4504-b0a6-0b9a040a0a5c>

### 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 = "<OPENROUTER_API_KEY>"
base_url = "https://openrouter.ai/api/v1"
api_key = "<AIMLAPI_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.

Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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
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 @@ -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 |
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.observability.tracing import memory_span, set_generation_usage

from .protocol import EmbeddingServiceError
Expand Down Expand Up @@ -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]:
Expand Down
5 changes: 5 additions & 0 deletions src/everos/component/llm/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 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 @@ -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(
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 @@ -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 (<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
Loading