diff --git a/.env.example b/.env.example index 97681cc5fd..bc2498d5bf 100644 --- a/.env.example +++ b/.env.example @@ -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 @@ -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 @@ -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 diff --git a/hindsight-api-slim/hindsight_api/config.py b/hindsight-api-slim/hindsight_api/config.py index 2e4d7fc817..0308ea0a1b 100644 --- a/hindsight-api-slim/hindsight_api/config.py +++ b/hindsight-api-slim/hindsight_api/config.py @@ -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 @@ -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" @@ -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", @@ -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. @@ -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 @@ -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"), diff --git a/hindsight-api-slim/hindsight_api/engine/embeddings.py b/hindsight-api-slim/hindsight_api/engine/embeddings.py index b05c9a7d74..2debd4c8ab 100644 --- a/hindsight-api-slim/hindsight_api/engine/embeddings.py +++ b/hindsight-api-slim/hindsight_api/engine/embeddings.py @@ -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 ( @@ -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. @@ -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 @@ -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 @@ -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: @@ -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: @@ -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'" ) diff --git a/hindsight-api-slim/hindsight_api/engine/llm_wrapper.py b/hindsight-api-slim/hindsight_api/engine/llm_wrapper.py index e9912d1022..0b6200a1c5 100644 --- a/hindsight-api-slim/hindsight_api/engine/llm_wrapper.py +++ b/hindsight-api-slim/hindsight_api/engine/llm_wrapper.py @@ -768,6 +768,7 @@ def create_llm_provider( "volcano", "openrouter", "requesty", + "aimlapi", "zai", "opencode-go", "atlas", @@ -962,6 +963,7 @@ def __init__( "volcano", "openrouter", "requesty", + "aimlapi", "zai", "opencode-go", "atlas", @@ -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": diff --git a/hindsight-api-slim/hindsight_api/engine/providers/openai_compatible_llm.py b/hindsight-api-slim/hindsight_api/engine/providers/openai_compatible_llm.py index 8d8642959a..57e460fc1f 100644 --- a/hindsight-api-slim/hindsight_api/engine/providers/openai_compatible_llm.py +++ b/hindsight-api-slim/hindsight_api/engine/providers/openai_compatible_llm.py @@ -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, @@ -735,6 +735,7 @@ def __init__( "volcano", "openrouter", "requesty", + "aimlapi", "zai", "opencode-go", "atlas", @@ -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": @@ -791,6 +794,7 @@ def __init__( "deepseek", "openrouter", "requesty", + "aimlapi", "zai", "opencode-go", "atlas", @@ -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: diff --git a/hindsight-api-slim/tests/test_aimlapi_provider.py b/hindsight-api-slim/tests/test_aimlapi_provider.py new file mode 100644 index 0000000000..c227840910 --- /dev/null +++ b/hindsight-api-slim/tests/test_aimlapi_provider.py @@ -0,0 +1,205 @@ +"""Tests for the aimlapi.com OpenAI-compatible LLM provider and its attribution headers.""" + +import re + +import pytest + + +def test_aimlapi_config_has_expected_default_model(monkeypatch): + """HindsightConfig should default aimlapi to a model that exists in the catalog.""" + from hindsight_api.config import PROVIDER_DEFAULT_MODELS, HindsightConfig, clear_config_cache + + monkeypatch.setenv("HINDSIGHT_API_LLM_PROVIDER", "aimlapi") + monkeypatch.delenv("HINDSIGHT_API_LLM_MODEL", raising=False) + clear_config_cache() + + try: + assert PROVIDER_DEFAULT_MODELS["aimlapi"] == "openai/gpt-5-mini" + config = HindsightConfig.from_env() + assert config.llm_provider == "aimlapi" + assert config.llm_model == "openai/gpt-5-mini" + finally: + clear_config_cache() + + +def test_aimlapi_llm_provider_from_env_has_expected_default_model(monkeypatch): + """LLMProvider.from_env should use the aimlapi provider default model and base URL.""" + from hindsight_api.config import clear_config_cache + from hindsight_api.engine.llm_wrapper import LLMProvider + + monkeypatch.setenv("HINDSIGHT_API_LLM_PROVIDER", "aimlapi") + monkeypatch.setenv("HINDSIGHT_API_LLM_API_KEY", "test-key") + monkeypatch.delenv("HINDSIGHT_API_LLM_MODEL", raising=False) + monkeypatch.delenv("HINDSIGHT_API_LLM_BASE_URL", raising=False) + clear_config_cache() + + try: + llm = LLMProvider.from_env() + assert llm.provider == "aimlapi" + assert llm.model == "openai/gpt-5-mini" + assert llm.base_url == "https://api.aimlapi.com/v1" + finally: + clear_config_cache() + + +def test_aimlapi_requires_api_key(): + """aimlapi is a cloud gateway and should require an API key.""" + from hindsight_api.engine.llm_wrapper import requires_api_key + + assert requires_api_key("aimlapi") is True + + +def test_aimlapi_uses_openai_compatible_provider_with_default_base_url(): + """The provider factory should route aimlapi to OpenAICompatibleLLM.""" + from hindsight_api.engine.llm_wrapper import LLMProvider + from hindsight_api.engine.providers.openai_compatible_llm import OpenAICompatibleLLM + + llm = LLMProvider( + provider="aimlapi", + api_key="test-key", + base_url="", + model="openai/gpt-5-mini", + ) + + assert llm.provider == "aimlapi" + assert llm.model == "openai/gpt-5-mini" + # /v1/completions does not exist on this gateway; only the /v1 root is declared + # and the OpenAI SDK appends /chat/completions to it. + assert llm.base_url == "https://api.aimlapi.com/v1" + assert not llm.base_url.endswith("/") + assert isinstance(llm._provider_impl, OpenAICompatibleLLM) + assert llm._provider_impl.base_url == "https://api.aimlapi.com/v1" + + +def test_aimlapi_rejects_missing_api_key(): + """aimlapi should fail fast without an API key, matching the other cloud gateways.""" + from hindsight_api.engine.llm_wrapper import LLMProvider + + with pytest.raises(ValueError, match="API key is required for aimlapi"): + LLMProvider( + provider="aimlapi", + api_key="", + base_url="", + model="openai/gpt-5-mini", + ) + + +def test_aimlapi_partner_id_matches_gateway_pattern(): + """A malformed partner id is dropped silently by the gateway, so assert its shape here. + + The gateway accepts ``^part_[A-Za-z0-9]{1,64}$`` and treats anything else as + untagged traffic without failing the request — a typo would be invisible at + runtime, which is why it is pinned in a test rather than only in review. + """ + from hindsight_api.config import AIMLAPI_ATTRIBUTION_HEADERS + + partner_id = AIMLAPI_ATTRIBUTION_HEADERS["X-AIMLAPI-Partner-ID"] + assert re.fullmatch(r"part_[A-Za-z0-9]{1,64}", partner_id), partner_id + # /, channel from a closed enum {web, agent, mcp}. + assert re.fullmatch(r"agent/[a-z0-9-]{1,32}", AIMLAPI_ATTRIBUTION_HEADERS["X-AIMLAPI-Source"]) + # HTTP-Referer / X-Title identify the calling application (Hindsight), not the gateway. + assert AIMLAPI_ATTRIBUTION_HEADERS["HTTP-Referer"] == "https://github.com/vectorize-io/hindsight" + assert AIMLAPI_ATTRIBUTION_HEADERS["X-Title"] == "Hindsight" + + +def test_aimlapi_attribution_headers_reach_the_client(): + """The OpenAI client for aimlapi should carry all four attribution headers.""" + from hindsight_api.config import AIMLAPI_ATTRIBUTION_HEADERS + from hindsight_api.engine.providers.openai_compatible_llm import OpenAICompatibleLLM + + llm = OpenAICompatibleLLM( + provider="aimlapi", + api_key="test-key", + base_url="", + model="openai/gpt-5-mini", + reasoning_effort=None, + ) + + sent = llm._client.default_headers + for header, value in AIMLAPI_ATTRIBUTION_HEADERS.items(): + assert sent.get(header) == value + + +def test_aimlapi_attribution_does_not_leak_to_other_providers(): + """Attribution must never ride a request to a different provider.""" + from hindsight_api.engine.providers.openai_compatible_llm import OpenAICompatibleLLM + + llm = OpenAICompatibleLLM( + provider="openrouter", + api_key="test-key", + base_url="", + model="qwen/qwen3.5-9b", + reasoning_effort=None, + ) + + assert "X-AIMLAPI-Partner-ID" not in llm._client.default_headers + + +def test_aimlapi_attribution_is_scoped_to_the_aimlapi_host(): + """A proxy fronting the same wire format must not receive our attribution.""" + from hindsight_api.engine.providers.openai_compatible_llm import OpenAICompatibleLLM + + llm = OpenAICompatibleLLM( + provider="aimlapi", + api_key="test-key", + base_url="https://gateway.internal.example.com/v1", + model="openai/gpt-5-mini", + reasoning_effort=None, + ) + + assert "X-AIMLAPI-Partner-ID" not in llm._client.default_headers + + +def test_aimlapi_attribution_merges_under_operator_headers(): + """Operator-supplied default headers win, and the shared constant is never mutated.""" + from hindsight_api.config import AIMLAPI_ATTRIBUTION_HEADERS, aimlapi_default_headers + + before = dict(AIMLAPI_ATTRIBUTION_HEADERS) + operator = {"X-Title": "Operator Override", "X-Component-Id": "hindsight"} + + merged = aimlapi_default_headers("aimlapi", "https://api.aimlapi.com/v1", operator) + + assert merged is not None + assert merged["X-Title"] == "Operator Override" + assert merged["X-Component-Id"] == "hindsight" + assert merged["X-AIMLAPI-Partner-ID"] == AIMLAPI_ATTRIBUTION_HEADERS["X-AIMLAPI-Partner-ID"] + # Neither the module constant nor the caller's dict was mutated in place. + assert AIMLAPI_ATTRIBUTION_HEADERS == before + assert operator == {"X-Title": "Operator Override", "X-Component-Id": "hindsight"} + + +def test_aimlapi_embeddings_carry_attribution(monkeypatch): + """The embeddings surface talks to the same gateway and should be tagged too.""" + from hindsight_api.config import AIMLAPI_ATTRIBUTION_HEADERS, clear_config_cache + from hindsight_api.engine.embeddings import create_embeddings_from_env + + monkeypatch.setenv("HINDSIGHT_API_EMBEDDINGS_PROVIDER", "aimlapi") + monkeypatch.setenv("HINDSIGHT_API_EMBEDDINGS_AIMLAPI_API_KEY", "test-key") + clear_config_cache() + + try: + embeddings = create_embeddings_from_env() + assert embeddings.base_url == "https://api.aimlapi.com/v1" + assert embeddings.model == "openai/text-embedding-3-small" + assert embeddings.default_headers == AIMLAPI_ATTRIBUTION_HEADERS + assert embeddings.default_headers is not AIMLAPI_ATTRIBUTION_HEADERS + finally: + clear_config_cache() + + +def test_aimlapi_embeddings_require_a_key(monkeypatch): + """Missing key should name every env var that can supply one.""" + from hindsight_api.config import clear_config_cache + from hindsight_api.engine.embeddings import create_embeddings_from_env + + monkeypatch.setenv("HINDSIGHT_API_EMBEDDINGS_PROVIDER", "aimlapi") + monkeypatch.delenv("HINDSIGHT_API_EMBEDDINGS_AIMLAPI_API_KEY", raising=False) + monkeypatch.delenv("HINDSIGHT_API_AIMLAPI_API_KEY", raising=False) + monkeypatch.delenv("HINDSIGHT_API_LLM_API_KEY", raising=False) + clear_config_cache() + + try: + with pytest.raises(ValueError, match="HINDSIGHT_API_EMBEDDINGS_AIMLAPI_API_KEY"): + create_embeddings_from_env() + finally: + clear_config_cache() diff --git a/hindsight-docs/docs/developer/configuration.md b/hindsight-docs/docs/developer/configuration.md index eabc31575c..65da3b34e9 100644 --- a/hindsight-docs/docs/developer/configuration.md +++ b/hindsight-docs/docs/developer/configuration.md @@ -268,7 +268,7 @@ For non-English banks (especially CJK) and the language/extraction-language trad | Variable | Description | Default | |----------|-------------|---------| -| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `openai`, `openai-responses`, `openai-codex`, `claude-code`, `github-copilot`, `anthropic`, `gemini`, `groq`, `minimax`, `deepseek`, `zai`, `opencode-go`, `nous`, `xai-oauth`, `fireworks`, `ollama`, `ollama-cloud`, `lmstudio`, `llamacpp`, `vertexai`, `bedrock`, `litellm`, `litellmrouter`, `volcano`, `openrouter`, `requesty`, `none` | `openai` | +| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `aimlapi`, `openai`, `openai-responses`, `openai-codex`, `claude-code`, `github-copilot`, `anthropic`, `gemini`, `groq`, `minimax`, `deepseek`, `zai`, `opencode-go`, `nous`, `xai-oauth`, `fireworks`, `ollama`, `ollama-cloud`, `lmstudio`, `llamacpp`, `vertexai`, `bedrock`, `litellm`, `litellmrouter`, `volcano`, `openrouter`, `requesty`, `none` | `openai` | | `HINDSIGHT_API_LLM_API_KEY` | API key for providers that require one; unused by `github-copilot` | - | | `HINDSIGHT_API_LLM_MODEL` | Model name | `gpt-5-mini` | | `HINDSIGHT_API_LLM_BASE_URL` | Custom LLM endpoint | Provider default | @@ -319,6 +319,11 @@ When `HINDSIGHT_API_LLM_PROVIDER=ollama`, Hindsight no longer sends the previous **Provider Examples** ```bash +# aimlapi.com (recommended for breadth of model choice behind one key) +export HINDSIGHT_API_LLM_PROVIDER=aimlapi +export HINDSIGHT_API_LLM_API_KEY=your-aimlapi-api-key +export HINDSIGHT_API_LLM_MODEL=openai/gpt-5-mini + # Groq (recommended for fast inference) export HINDSIGHT_API_LLM_PROVIDER=groq export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx @@ -713,9 +718,9 @@ server-level only (not overridable per tenant/bank) and a change requires a rest | Variable | Description | Default | |----------|-------------|---------| -| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `local`, `onnx`, `tei`, `openai`, `openai-codex`, `openrouter`, `requesty`, `cohere`, `google`, `zeroentropy`, `litellm`, or `litellm-sdk` | `local` | +| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `aimlapi`, `local`, `onnx`, `tei`, `openai`, `openai-codex`, `openrouter`, `requesty`, `cohere`, `google`, `zeroentropy`, `litellm`, or `litellm-sdk` | `local` | | `HINDSIGHT_API_EMBEDDINGS_MAX_INPUT_TOKENS` | Applies to **every** provider. If set, truncate each text to this many tokens (counted with `HINDSIGHT_API_TOKENIZER_ENCODING`, approximate) before embedding. Set it to the model's real input limit (e.g. `8192` for Bedrock Titan V2, or a llama.cpp server's context, with a little headroom) so oversized content is truncated instead of failing the embed call permanently. Off by default. (Deprecated alias: `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MAX_INPUT_TOKENS`.) | - | -| `HINDSIGHT_API_EMBEDDINGS_QUERY_PREFIX` | Text prepended to every search before it is embedded. Set it when your endpoint serves an asymmetric model that expects a search instruction — e.g. `task: search result \| query: ` for `google/embeddinggemma-300m`, or `query: ` for E5. Applies to the providers that only accept plain text (`tei`, `openai`, `openai-codex`, `openrouter`, `requesty`, `litellm`, `litellm-sdk`); see the note below for the ones that don't need it. Trailing spaces are kept as written. | - (no prefix) | +| `HINDSIGHT_API_EMBEDDINGS_QUERY_PREFIX` | Text prepended to every search before it is embedded. Set it when your endpoint serves an asymmetric model that expects a search instruction — e.g. `task: search result \| query: ` for `google/embeddinggemma-300m`, or `query: ` for E5. Applies to the providers that only accept plain text (`tei`, `openai`, `openai-codex`, `openrouter`, `requesty`, `aimlapi`, `litellm`, `litellm-sdk`); see the note below for the ones that don't need it. Trailing spaces are kept as written. | - (no prefix) | | `HINDSIGHT_API_EMBEDDINGS_PASSAGE_PREFIX` | Text prepended to every stored memory/document before it is embedded — e.g. `title: none \| text: ` for `google/embeddinggemma-300m`, or `passage: ` for E5. Same providers as above. Trailing spaces are kept as written. | - (no prefix) | | `HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL` | Model for local provider. Models that ship their own search-text and stored-text instructions (e.g. the Qwen3-Embedding family) have them applied automatically — see the note below. | `BAAI/bge-small-en-v1.5` | | `HINDSIGHT_API_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE` | Allow loading models with custom code (security risk, disabled by default) | `false` | @@ -748,6 +753,8 @@ server-level only (not overridable per tenant/bank) and a change requires a rest | `HINDSIGHT_API_EMBEDDINGS_OPENROUTER_API_KEY` | OpenRouter API key for embeddings (falls back to `HINDSIGHT_API_OPENROUTER_API_KEY`, then `HINDSIGHT_API_LLM_API_KEY`) | - | | `HINDSIGHT_API_EMBEDDINGS_REQUESTY_API_KEY` | Requesty API key for embeddings (falls back to `HINDSIGHT_API_REQUESTY_API_KEY`, then `HINDSIGHT_API_LLM_API_KEY`) | - | | `HINDSIGHT_API_EMBEDDINGS_REQUESTY_MODEL` | Requesty embedding model | `openai/text-embedding-3-small` | +| `HINDSIGHT_API_EMBEDDINGS_AIMLAPI_API_KEY` | aimlapi.com API key for embeddings (falls back to `HINDSIGHT_API_AIMLAPI_API_KEY`, then `HINDSIGHT_API_LLM_API_KEY`) | - | +| `HINDSIGHT_API_EMBEDDINGS_AIMLAPI_MODEL` | aimlapi.com embedding model | `openai/text-embedding-3-small` | | `HINDSIGHT_API_EMBEDDINGS_OPENROUTER_MODEL` | OpenRouter embedding model | `perplexity/pplx-embed-v1-0.6b` | | `HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_API_KEY` | ZeroEntropy API key for embeddings | - | | `HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_MODEL` | ZeroEntropy embedding model | `zembed-1` | diff --git a/hindsight-docs/src/data/llmProviders.json b/hindsight-docs/src/data/llmProviders.json index 8bdf69b688..aa333d7ad5 100644 --- a/hindsight-docs/src/data/llmProviders.json +++ b/hindsight-docs/src/data/llmProviders.json @@ -1,4 +1,5 @@ [ + {"id": "aimlapi", "label": "aimlapi.com", "iconKey": "openai-compatible", "defaultModel": "openai/gpt-5-mini"}, {"id": "openai", "label": "OpenAI", "iconKey": "openai", "defaultModel": "gpt-4o-mini", "batchApi": true}, {"id": "openai-responses", "label": "OpenAI Responses", "iconKey": "openai", "defaultModel": "gpt-5.6"}, {"id": "anthropic", "label": "Anthropic", "iconKey": "anthropic", "defaultModel": "claude-haiku-4-5"}, diff --git a/hindsight-embed/hindsight_embed/control_center/providers.py b/hindsight-embed/hindsight_embed/control_center/providers.py index 5e337a9b0c..504d370dd6 100644 --- a/hindsight-embed/hindsight_embed/control_center/providers.py +++ b/hindsight-embed/hindsight_embed/control_center/providers.py @@ -20,6 +20,7 @@ class ProviderInfo: # Ordered for display in the wizard dropdown. Mirrors the providers that # hindsight-api's PROVIDER_DEFAULT_MODELS supports. PROVIDER_CATALOG: tuple[ProviderInfo, ...] = ( + ProviderInfo("aimlapi", "aimlapi.com", needs_api_key=True, default_base_url="https://api.aimlapi.com/v1"), ProviderInfo("openai", "OpenAI", needs_api_key=True, default_base_url="https://api.openai.com/v1"), ProviderInfo("anthropic", "Anthropic", needs_api_key=True, default_base_url="https://api.anthropic.com"), ProviderInfo("gemini", "Google Gemini", needs_api_key=True), diff --git a/hindsight-embed/hindsight_embed/env.example b/hindsight-embed/hindsight_embed/env.example index 97681cc5fd..bc2498d5bf 100644 --- a/hindsight-embed/hindsight_embed/env.example +++ b/hindsight-embed/hindsight_embed/env.example @@ -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 @@ -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 @@ -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 diff --git a/skills/hindsight-docs/references/developer/configuration.md b/skills/hindsight-docs/references/developer/configuration.md index bf9c15a5b0..fdd3aff2b1 100644 --- a/skills/hindsight-docs/references/developer/configuration.md +++ b/skills/hindsight-docs/references/developer/configuration.md @@ -268,7 +268,7 @@ For non-English banks (especially CJK) and the language/extraction-language trad | Variable | Description | Default | |----------|-------------|---------| -| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `openai`, `openai-responses`, `openai-codex`, `claude-code`, `github-copilot`, `anthropic`, `gemini`, `groq`, `minimax`, `deepseek`, `zai`, `opencode-go`, `nous`, `xai-oauth`, `fireworks`, `ollama`, `ollama-cloud`, `lmstudio`, `llamacpp`, `vertexai`, `bedrock`, `litellm`, `litellmrouter`, `volcano`, `openrouter`, `requesty`, `none` | `openai` | +| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `aimlapi`, `openai`, `openai-responses`, `openai-codex`, `claude-code`, `github-copilot`, `anthropic`, `gemini`, `groq`, `minimax`, `deepseek`, `zai`, `opencode-go`, `nous`, `xai-oauth`, `fireworks`, `ollama`, `ollama-cloud`, `lmstudio`, `llamacpp`, `vertexai`, `bedrock`, `litellm`, `litellmrouter`, `volcano`, `openrouter`, `requesty`, `none` | `openai` | | `HINDSIGHT_API_LLM_API_KEY` | API key for providers that require one; unused by `github-copilot` | - | | `HINDSIGHT_API_LLM_MODEL` | Model name | `gpt-5-mini` | | `HINDSIGHT_API_LLM_BASE_URL` | Custom LLM endpoint | Provider default | @@ -319,6 +319,11 @@ When `HINDSIGHT_API_LLM_PROVIDER=ollama`, Hindsight no longer sends the previous **Provider Examples** ```bash +# aimlapi.com (recommended for breadth of model choice behind one key) +export HINDSIGHT_API_LLM_PROVIDER=aimlapi +export HINDSIGHT_API_LLM_API_KEY=your-aimlapi-api-key +export HINDSIGHT_API_LLM_MODEL=openai/gpt-5-mini + # Groq (recommended for fast inference) export HINDSIGHT_API_LLM_PROVIDER=groq export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx @@ -713,9 +718,9 @@ server-level only (not overridable per tenant/bank) and a change requires a rest | Variable | Description | Default | |----------|-------------|---------| -| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `local`, `onnx`, `tei`, `openai`, `openai-codex`, `openrouter`, `requesty`, `cohere`, `google`, `zeroentropy`, `litellm`, or `litellm-sdk` | `local` | +| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `aimlapi`, `local`, `onnx`, `tei`, `openai`, `openai-codex`, `openrouter`, `requesty`, `cohere`, `google`, `zeroentropy`, `litellm`, or `litellm-sdk` | `local` | | `HINDSIGHT_API_EMBEDDINGS_MAX_INPUT_TOKENS` | Applies to **every** provider. If set, truncate each text to this many tokens (counted with `HINDSIGHT_API_TOKENIZER_ENCODING`, approximate) before embedding. Set it to the model's real input limit (e.g. `8192` for Bedrock Titan V2, or a llama.cpp server's context, with a little headroom) so oversized content is truncated instead of failing the embed call permanently. Off by default. (Deprecated alias: `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MAX_INPUT_TOKENS`.) | - | -| `HINDSIGHT_API_EMBEDDINGS_QUERY_PREFIX` | Text prepended to every search before it is embedded. Set it when your endpoint serves an asymmetric model that expects a search instruction — e.g. `task: search result \| query: ` for `google/embeddinggemma-300m`, or `query: ` for E5. Applies to the providers that only accept plain text (`tei`, `openai`, `openai-codex`, `openrouter`, `requesty`, `litellm`, `litellm-sdk`); see the note below for the ones that don't need it. Trailing spaces are kept as written. | - (no prefix) | +| `HINDSIGHT_API_EMBEDDINGS_QUERY_PREFIX` | Text prepended to every search before it is embedded. Set it when your endpoint serves an asymmetric model that expects a search instruction — e.g. `task: search result \| query: ` for `google/embeddinggemma-300m`, or `query: ` for E5. Applies to the providers that only accept plain text (`tei`, `openai`, `openai-codex`, `openrouter`, `requesty`, `aimlapi`, `litellm`, `litellm-sdk`); see the note below for the ones that don't need it. Trailing spaces are kept as written. | - (no prefix) | | `HINDSIGHT_API_EMBEDDINGS_PASSAGE_PREFIX` | Text prepended to every stored memory/document before it is embedded — e.g. `title: none \| text: ` for `google/embeddinggemma-300m`, or `passage: ` for E5. Same providers as above. Trailing spaces are kept as written. | - (no prefix) | | `HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL` | Model for local provider. Models that ship their own search-text and stored-text instructions (e.g. the Qwen3-Embedding family) have them applied automatically — see the note below. | `BAAI/bge-small-en-v1.5` | | `HINDSIGHT_API_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE` | Allow loading models with custom code (security risk, disabled by default) | `false` | @@ -748,6 +753,8 @@ server-level only (not overridable per tenant/bank) and a change requires a rest | `HINDSIGHT_API_EMBEDDINGS_OPENROUTER_API_KEY` | OpenRouter API key for embeddings (falls back to `HINDSIGHT_API_OPENROUTER_API_KEY`, then `HINDSIGHT_API_LLM_API_KEY`) | - | | `HINDSIGHT_API_EMBEDDINGS_REQUESTY_API_KEY` | Requesty API key for embeddings (falls back to `HINDSIGHT_API_REQUESTY_API_KEY`, then `HINDSIGHT_API_LLM_API_KEY`) | - | | `HINDSIGHT_API_EMBEDDINGS_REQUESTY_MODEL` | Requesty embedding model | `openai/text-embedding-3-small` | +| `HINDSIGHT_API_EMBEDDINGS_AIMLAPI_API_KEY` | aimlapi.com API key for embeddings (falls back to `HINDSIGHT_API_AIMLAPI_API_KEY`, then `HINDSIGHT_API_LLM_API_KEY`) | - | +| `HINDSIGHT_API_EMBEDDINGS_AIMLAPI_MODEL` | aimlapi.com embedding model | `openai/text-embedding-3-small` | | `HINDSIGHT_API_EMBEDDINGS_OPENROUTER_MODEL` | OpenRouter embedding model | `perplexity/pplx-embed-v1-0.6b` | | `HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_API_KEY` | ZeroEntropy API key for embeddings | - | | `HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_MODEL` | ZeroEntropy embedding model | `zembed-1` | diff --git a/skills/hindsight-docs/references/developer/models.md b/skills/hindsight-docs/references/developer/models.md index 7b3c186493..37cb9089fb 100644 --- a/skills/hindsight-docs/references/developer/models.md +++ b/skills/hindsight-docs/references/developer/models.md @@ -19,6 +19,7 @@ Used for fact extraction, entity resolution, mental model consolidation, and ans **Supported providers:** +- aimlapi.com - OpenAI - OpenAI Responses - Anthropic @@ -102,6 +103,7 @@ Beyond basic generation, some providers support optional features that lower cos | Provider | Batch API | Explicit prompt caching | |----------|:---------:|:-----------------------:| +| aimlapi.com (`aimlapi`) | — | — | | OpenAI (`openai`) | ✅ | — | | OpenAI Responses (`openai-responses`) | — | — | | Anthropic (`anthropic`) | — | — | @@ -168,6 +170,7 @@ Each provider has a recommended default model that's used when `HINDSIGHT_API_LL | Provider | Default Model | |----------|--------------| +| `aimlapi` | `openai/gpt-5-mini` | | `openai` | `gpt-4o-mini` | | `openai-responses` | `gpt-5.6` | | `anthropic` | `claude-haiku-4-5` | diff --git a/skills/hindsight-docs/references/faq.md b/skills/hindsight-docs/references/faq.md index df1126cb68..93c252c896 100644 --- a/skills/hindsight-docs/references/faq.md +++ b/skills/hindsight-docs/references/faq.md @@ -69,6 +69,7 @@ Browse all supported integrations in the Integrations Hub. ### Which LLM providers are supported? +- aimlapi.com - OpenAI - OpenAI Responses - Anthropic