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
9 changes: 7 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Copy this file to .env and fill in your values

# LLM Configuration (Required)
# Supported providers: openai, openai-responses, groq, ollama, gemini, anthropic, lmstudio, vertexai, minimax, deepseek, zai, atlas, volcano, openai-codex, claude-code, github-copilot
# Supported providers: aimlapi, openai, openai-responses, groq, ollama, gemini, anthropic, lmstudio, vertexai, minimax, deepseek, zai, atlas, volcano, openai-codex, claude-code, github-copilot
HINDSIGHT_API_LLM_PROVIDER=openai
HINDSIGHT_API_LLM_API_KEY=your-api-key-here
HINDSIGHT_API_LLM_MODEL=gpt-4o-mini
Expand Down Expand Up @@ -85,6 +85,11 @@ HINDSIGHT_API_LLM_MODEL=gpt-4o-mini
# For debugging otherwise-unreproducible rejected calls. Off by default.
# HINDSIGHT_API_LLM_DEBUG_DUMP_4XX=false

# Example: aimlapi.com configuration (OpenAI-compatible gateway, https://api.aimlapi.com/v1)
# HINDSIGHT_API_LLM_PROVIDER=aimlapi
# HINDSIGHT_API_LLM_API_KEY=your-aimlapi-api-key
# HINDSIGHT_API_LLM_MODEL=openai/gpt-5-mini # catalog at https://api.aimlapi.com/v1/models

# Example: Anthropic Claude configuration
# HINDSIGHT_API_LLM_PROVIDER=anthropic
# HINDSIGHT_API_LLM_API_KEY=your-anthropic-api-key
Expand Down Expand Up @@ -307,7 +312,7 @@ HINDSIGHT_API_LOG_LEVEL=info
# HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_DEFAULT_HEADERS=

# Embeddings Configuration (Optional - uses local by default)
# Provider: "local" (default), "onnx", "tei", "openai", "cohere", "google", "openrouter", "zeroentropy", "litellm", or "litellm-sdk"
# Provider: "aimlapi", "local" (default), "onnx", "tei", "openai", "cohere", "google", "openrouter", "zeroentropy", "litellm", or "litellm-sdk"
# HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
# For local provider:
# HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
Expand Down
52 changes: 52 additions & 0 deletions hindsight-api-slim/hindsight_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from dataclasses import dataclass, field, fields
from datetime import datetime, timezone
from typing import Any, Literal
from urllib.parse import urlparse

from dotenv import find_dotenv, load_dotenv

Expand Down Expand Up @@ -449,6 +450,11 @@ def _parse_boolean_env(env_name: str, default: bool) -> bool:
ENV_EMBEDDINGS_REQUESTY_API_KEY = "HINDSIGHT_API_EMBEDDINGS_REQUESTY_API_KEY"
ENV_EMBEDDINGS_REQUESTY_MODEL = "HINDSIGHT_API_EMBEDDINGS_REQUESTY_MODEL"

# AI/ML API configuration (aimlapi.com — OpenAI-compatible gateway; embeddings)
ENV_AIMLAPI_API_KEY = "HINDSIGHT_API_AIMLAPI_API_KEY"
ENV_EMBEDDINGS_AIMLAPI_API_KEY = "HINDSIGHT_API_EMBEDDINGS_AIMLAPI_API_KEY"
ENV_EMBEDDINGS_AIMLAPI_MODEL = "HINDSIGHT_API_EMBEDDINGS_AIMLAPI_MODEL"

# ZeroEntropy configuration (embeddings)
ENV_EMBEDDINGS_ZEROENTROPY_API_KEY = "HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_API_KEY"
ENV_EMBEDDINGS_ZEROENTROPY_MODEL = "HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_MODEL"
Expand Down Expand Up @@ -955,6 +961,7 @@ def _parse_worker_slot_reservations() -> dict[str, int]:
"volcano": "doubao-pro-32k",
"openrouter": "qwen/qwen3.5-9b",
"requesty": "openai/gpt-4o-mini",
"aimlapi": "openai/gpt-5-mini",
"fireworks": "accounts/fireworks/models/llama-v3p1-8b-instruct",
"nous": "deepseek/deepseek-v4-flash",
"xai-oauth": "grok-4.5",
Expand Down Expand Up @@ -1218,6 +1225,44 @@ def _parse_strategy_boosts(raw: str | None) -> dict[str, str]:
# Requesty defaults
DEFAULT_EMBEDDINGS_REQUESTY_MODEL = "openai/text-embedding-3-small"

# AI/ML API defaults
DEFAULT_EMBEDDINGS_AIMLAPI_MODEL = "openai/text-embedding-3-small"

# Attribution headers sent with AI/ML API requests. The gateway reads them to
# attribute traffic to the calling application; a request without them is
# indistinguishable from anonymous use. They identify Hindsight — nothing about
# the operator or their data is in them — and are applied only when the resolved
# base URL is the aimlapi.com host below, so they can never ride a request to a
# different provider or to a proxy that merely fronts the same wire format.
AIMLAPI_ATTRIBUTION_HOST = "api.aimlapi.com"
AIMLAPI_ATTRIBUTION_HEADERS: dict[str, str] = {
"HTTP-Referer": "https://github.com/vectorize-io/hindsight",
"X-Title": "Hindsight",
"X-AIMLAPI-Partner-ID": "part_fO6J5vQsER4jNM0YFhQkld2H",
"X-AIMLAPI-Source": "agent/hindsight",
}


def aimlapi_default_headers(
provider: str,
base_url: str | None,
default_headers: dict[str, str] | None,
) -> dict[str, str] | None:
"""Merge the AI/ML API attribution headers *under* operator-supplied headers.

Returns ``default_headers`` unchanged for any other provider or host, so no
existing provider's request shape moves. When it does apply it builds a new
dict: ``AIMLAPI_ATTRIBUTION_HEADERS`` is never mutated (it is shared by every
client in the process), and an operator who set the same key in
``HINDSIGHT_API_LLM_DEFAULT_HEADERS`` still wins.
"""
if provider != "aimlapi" or not base_url:
return default_headers
if (urlparse(base_url).hostname or "").lower() != AIMLAPI_ATTRIBUTION_HOST:
return default_headers
return {**AIMLAPI_ATTRIBUTION_HEADERS, **(default_headers or {})}


# ZeroEntropy defaults
DEFAULT_EMBEDDINGS_ZEROENTROPY_MODEL = "zembed-1"
# Shared between embeddings (zembed-1) and reranker (zerank-*) — the host is the same.
Expand Down Expand Up @@ -2645,6 +2690,8 @@ class HindsightConfig:
embeddings_openrouter_model: str
embeddings_requesty_api_key: str | None
embeddings_requesty_model: str
embeddings_aimlapi_api_key: str | None
embeddings_aimlapi_model: str
embeddings_litellm_api_base: str
embeddings_litellm_api_key: str | None
embeddings_litellm_model: str
Expand Down Expand Up @@ -3815,6 +3862,11 @@ def from_env(cls) -> "HindsightConfig":
or os.getenv(ENV_REQUESTY_API_KEY)
or os.getenv(ENV_LLM_API_KEY),
embeddings_requesty_model=os.getenv(ENV_EMBEDDINGS_REQUESTY_MODEL, DEFAULT_EMBEDDINGS_REQUESTY_MODEL),
# AI/ML API embeddings (with fallback to shared AI/ML API key, then LLM key)
embeddings_aimlapi_api_key=os.getenv(ENV_EMBEDDINGS_AIMLAPI_API_KEY)
or os.getenv(ENV_AIMLAPI_API_KEY)
or os.getenv(ENV_LLM_API_KEY),
embeddings_aimlapi_model=os.getenv(ENV_EMBEDDINGS_AIMLAPI_MODEL, DEFAULT_EMBEDDINGS_AIMLAPI_MODEL),
# ZeroEntropy embeddings
embeddings_zeroentropy_api_key=os.getenv(ENV_EMBEDDINGS_ZEROENTROPY_API_KEY)
or os.getenv("ZEROENTROPY_API_KEY"),
Expand Down
28 changes: 27 additions & 1 deletion hindsight-api-slim/hindsight_api/engine/embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
ENV_EMBEDDINGS_ZEROENTROPY_DIMENSIONS,
ENV_EMBEDDINGS_ZEROENTROPY_ENCODING_FORMAT,
ENV_LLM_API_KEY,
aimlapi_default_headers,
)
from .bank_attribution import apply_bank_attribution
from .local_device import (
Expand Down Expand Up @@ -951,6 +952,7 @@ def __init__(
max_retries: int = 3,
query_prefix: str = "",
passage_prefix: str = "",
default_headers: dict[str, str] | None = None,
):
"""
Initialize OpenAI embeddings client.
Expand All @@ -964,6 +966,8 @@ def __init__(
max_retries: Maximum number of retries for failed requests (default: 3)
query_prefix: Prefix prepended to recall/search queries (default: none)
passage_prefix: Prefix prepended to retained document text (default: none)
default_headers: Custom headers passed to the OpenAI client. None (the
default) sends no extra headers, so every existing caller is unchanged.
"""
self.api_key = api_key
self.model = model
Expand All @@ -973,6 +977,7 @@ def __init__(
self.max_retries = max_retries
self.query_prefix = query_prefix
self.passage_prefix = passage_prefix
self.default_headers = default_headers
self._client = None
self._dimension: int | None = None

Expand Down Expand Up @@ -1003,6 +1008,8 @@ async def initialize(self) -> None:
# Parse query parameters from base_url (e.g. ?api-version=xxx for Azure OpenAI)
# and pass them as default_query so they're included in every request.
client_kwargs = {"api_key": self.api_key, "max_retries": self.max_retries}
if self.default_headers:
client_kwargs["default_headers"] = self.default_headers
if self.base_url:
parsed = urlparse(self.base_url)
if parsed.query:
Expand Down Expand Up @@ -2199,6 +2206,24 @@ def create_embeddings_from_env() -> Embeddings:
query_prefix=query_prefix,
passage_prefix=passage_prefix,
)
elif provider == "aimlapi":
api_key = config.embeddings_aimlapi_api_key
if not api_key:
raise ValueError(
"HINDSIGHT_API_EMBEDDINGS_AIMLAPI_API_KEY, HINDSIGHT_API_AIMLAPI_API_KEY, "
f"or {ENV_LLM_API_KEY} is required when {ENV_EMBEDDINGS_PROVIDER} is 'aimlapi'"
)
aimlapi_base_url = "https://api.aimlapi.com/v1"
return OpenAIEmbeddings(
api_key=api_key,
model=config.embeddings_aimlapi_model,
base_url=aimlapi_base_url,
batch_size=config.embeddings_openai_batch_size,
dimensions=config.embeddings_openai_dimensions,
query_prefix=query_prefix,
passage_prefix=passage_prefix,
default_headers=aimlapi_default_headers("aimlapi", aimlapi_base_url, None),
)
elif provider == "zeroentropy":
api_key = config.embeddings_zeroentropy_api_key
if not api_key:
Expand Down Expand Up @@ -2269,6 +2294,7 @@ def create_embeddings_from_env() -> Embeddings:
else:
raise ValueError(
f"Unknown embeddings provider: {provider}. "
f"Supported: 'local', 'onnx', 'tei', 'openai', 'openai-codex', 'openrouter', 'requesty', 'cohere', 'google', "
f"Supported: 'local', 'onnx', 'tei', 'openai', 'openai-codex', 'openrouter', 'requesty', 'aimlapi', "
f"'cohere', 'google', "
f"'zeroentropy', 'litellm', 'litellm-sdk'"
)
4 changes: 4 additions & 0 deletions hindsight-api-slim/hindsight_api/engine/llm_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,7 @@ def create_llm_provider(
"volcano",
"openrouter",
"requesty",
"aimlapi",
"zai",
"opencode-go",
"atlas",
Expand Down Expand Up @@ -962,6 +963,7 @@ def __init__(
"volcano",
"openrouter",
"requesty",
"aimlapi",
"zai",
"opencode-go",
"atlas",
Expand Down Expand Up @@ -990,6 +992,8 @@ def __init__(
self.base_url = "https://openrouter.ai/api/v1"
elif self.provider == "requesty":
self.base_url = "https://router.requesty.ai/v1"
elif self.provider == "aimlapi":
self.base_url = "https://api.aimlapi.com/v1"
elif self.provider == "zai":
self.base_url = "https://api.z.ai/api/coding/paas/v4"
elif self.provider == "opencode-go":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import httpx
from openai import APIConnectionError, APIStatusError, AsyncOpenAI, LengthFinishReasonError

from hindsight_api.config import DEFAULT_LLM_TIMEOUT, ENV_LLM_TIMEOUT
from hindsight_api.config import DEFAULT_LLM_TIMEOUT, ENV_LLM_TIMEOUT, aimlapi_default_headers
from hindsight_api.engine.bank_attribution import apply_bank_attribution
from hindsight_api.engine.cache_affinity import (
CacheAffinityMode,
Expand Down Expand Up @@ -735,6 +735,7 @@ def __init__(
"volcano",
"openrouter",
"requesty",
"aimlapi",
"zai",
"opencode-go",
"atlas",
Expand All @@ -761,6 +762,8 @@ def __init__(
self.base_url = "https://openrouter.ai/api/v1"
elif self.provider == "requesty":
self.base_url = "https://router.requesty.ai/v1"
elif self.provider == "aimlapi":
self.base_url = "https://api.aimlapi.com/v1"
elif self.provider == "zai":
self.base_url = "https://api.z.ai/api/coding/paas/v4"
elif self.provider == "opencode-go":
Expand Down Expand Up @@ -791,6 +794,7 @@ def __init__(
"deepseek",
"openrouter",
"requesty",
"aimlapi",
"zai",
"opencode-go",
"atlas",
Expand Down Expand Up @@ -820,6 +824,7 @@ def __init__(

# Create OpenAI client — extract query params from base_url (e.g. Azure api-version)
client_kwargs: dict[str, Any] = {"api_key": self.api_key, "max_retries": 0}
default_headers = aimlapi_default_headers(self.provider, self.base_url, default_headers)
if default_headers:
client_kwargs["default_headers"] = default_headers
if self.base_url:
Expand Down
Loading