diff --git a/autogpt_platform/backend/backend/api/features/aimlapi/__init__.py b/autogpt_platform/backend/backend/api/features/aimlapi/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/autogpt_platform/backend/backend/api/features/aimlapi/config.py b/autogpt_platform/backend/backend/api/features/aimlapi/config.py new file mode 100644 index 000000000000..ac779ee9143a --- /dev/null +++ b/autogpt_platform/backend/backend/api/features/aimlapi/config.py @@ -0,0 +1,77 @@ +"""AIMLAPI endpoints + partner attribution for the "Get API key" flow. + +Production values are compiled in; every value is overridable via an +``AIMLAPI_*`` environment variable, so the same build runs against staging by +changing only the endpoint/partner env. No staging URL is hard-coded. +""" + +import os +from dataclasses import dataclass + +# Provisioned AutoGPT partner — the same id is valid on staging and production, +# so it ships as the compiled-in default. Override with AIMLAPI_PARTNER_ID only +# for a staging-only test id. +DEFAULT_AIMLAPI_PARTNER_ID = "part_T70zDIEvQLKSMzMQ7asjdtKR" +DEFAULT_AIMLAPI_PARTNER_NAME = "AutoGPT" + +# Client identifier reported on every AIMLAPI request (analytics + Mailchimp). +AIMLAPI_SOURCE = "agent/autogpt" +# Shown on the consent screen so the user sees which app is asking for a key. +AGENT_NAME = "AutoGPT" +# Spend cap presented on the consent screen, in USD minor units ($10). +DEFAULT_AIMLAPI_REQUESTED_USD_LIMIT_MINOR = 1000 + + +def _env_or_default(name: str, default: str) -> str: + value = (os.getenv(name) or "").strip() + return value or default + + +@dataclass(frozen=True, slots=True) +class AimlapiEndpoints: + app_base_url: str + verification_base_url: str + + +def resolve_endpoints() -> AimlapiEndpoints: + return AimlapiEndpoints( + # Host of the device-authorization + token endpoints. + app_base_url=_env_or_default( + "AIMLAPI_APP_URL", "https://app.aimlapi.com" + ).rstrip("/"), + # Base of the browser consent page, which lives in the web app under + # ``/app`` (the bare host is a static marketing site). The create + # response returns a production URL even on staging, so the consent URL + # is always rebuilt from this base (default = prod, override for + # staging, e.g. https://staging.aimlapi.com/app). + verification_base_url=_env_or_default( + "AIMLAPI_VERIFICATION_BASE_URL", "https://aimlapi.com/app" + ).rstrip("/"), + ) + + +def resolve_partner_id() -> str: + return _env_or_default("AIMLAPI_PARTNER_ID", DEFAULT_AIMLAPI_PARTNER_ID) + + +def resolve_partner_name() -> str: + return _env_or_default("AIMLAPI_PARTNER_NAME", DEFAULT_AIMLAPI_PARTNER_NAME) + + +def resolve_requested_usd_limit_minor() -> int: + raw = os.getenv("AIMLAPI_REQUESTED_USD_LIMIT_MINOR") + if raw is None: + return DEFAULT_AIMLAPI_REQUESTED_USD_LIMIT_MINOR + try: + value = int(raw) + except (TypeError, ValueError): + return DEFAULT_AIMLAPI_REQUESTED_USD_LIMIT_MINOR + return value if value > 0 else DEFAULT_AIMLAPI_REQUESTED_USD_LIMIT_MINOR + + +def attribution_headers() -> dict[str, str]: + """Headers that mark AIMLAPI traffic as agent-sourced for this partner.""" + return { + "X-AIMLAPI-Source": AIMLAPI_SOURCE, + "X-AIMLAPI-Partner-ID": resolve_partner_id(), + } diff --git a/autogpt_platform/backend/backend/api/features/aimlapi/router.py b/autogpt_platform/backend/backend/api/features/aimlapi/router.py new file mode 100644 index 000000000000..30ad76b5a1f4 --- /dev/null +++ b/autogpt_platform/backend/backend/api/features/aimlapi/router.py @@ -0,0 +1,93 @@ +"""API endpoints for the AIMLAPI "Get API key" device-authorization flow. + +``start`` opens an authorization the user approves in the browser; ``poll`` +returns the issued key once approved. The device code lives in a server-side +map keyed by request id — the browser only ever sees the request id and the +consent URL, so no redirect URI has to be registered for a given deployment. +""" + +import time +from typing import Annotated + +from autogpt_libs.auth import get_user_id +from fastapi import APIRouter, HTTPException, Security +from pydantic import BaseModel + +from backend.api.features.aimlapi.service import ( + TERMINAL_FAILURE_STATUSES, + AimlapiAuthError, + AuthorizationRequest, + poll_authorization, + start_authorization, +) + +router = APIRouter() + +# request_id -> pending authorization. In-memory and per-process on purpose: +# the device code must never reach the browser, and the flow is short-lived +# (~15 min). A multi-worker deployment would need a shared store; a single +# instance is the common self-hosted case. +_PENDING: dict[str, AuthorizationRequest] = {} + + +def _prune(now: float) -> None: + for request_id in [rid for rid, a in _PENDING.items() if now >= a.expires_at]: + _PENDING.pop(request_id, None) + + +class AuthorizeStartResponse(BaseModel): + request_id: str + verification_uri: str + interval: int + expires_in: int + + +class AuthorizePollRequest(BaseModel): + request_id: str + + +class AuthorizePollResponse(BaseModel): + status: str + api_key: str | None = None + + +@router.post("/authorize/start") +async def authorize_start( + user_id: Annotated[str, Security(get_user_id)], +) -> AuthorizeStartResponse: + """Begin a device authorization; the caller opens ``verification_uri``.""" + now = time.time() + _prune(now) + try: + authorization = await start_authorization() + except AimlapiAuthError as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + _PENDING[authorization.request_id] = authorization + return AuthorizeStartResponse( + request_id=authorization.request_id, + verification_uri=authorization.verification_uri, + interval=authorization.interval, + expires_in=max(0, int(authorization.expires_at - now)), + ) + + +@router.post("/authorize/poll") +async def authorize_poll( + body: AuthorizePollRequest, + user_id: Annotated[str, Security(get_user_id)], +) -> AuthorizePollResponse: + """Exchange the server-held device code for the issued key, if approved.""" + now = time.time() + _prune(now) + authorization = _PENDING.get(body.request_id) + if authorization is None: + # Unknown or already-expired request id: report 'expired' rather than + # 404 so the frontend shows the same "start again" state. + return AuthorizePollResponse(status="expired") + try: + result = await poll_authorization(authorization) + except AimlapiAuthError as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + if result.status == "ready" or result.status in TERMINAL_FAILURE_STATUSES: + _PENDING.pop(body.request_id, None) + return AuthorizePollResponse(status=result.status, api_key=result.api_key or None) diff --git a/autogpt_platform/backend/backend/api/features/aimlapi/service.py b/autogpt_platform/backend/backend/api/features/aimlapi/service.py new file mode 100644 index 000000000000..6ebb7a2b7728 --- /dev/null +++ b/autogpt_platform/backend/backend/api/features/aimlapi/service.py @@ -0,0 +1,182 @@ +"""AIMLAPI "Get API key" via OAuth 2.0 Device Authorization Grant (RFC 8628). + +The app creates an authorization request, the user approves it on the aimlapi +consent page in the browser, and the app polls a token endpoint until the +issued API key comes back. The device code and the issued key stay server-side +— the browser only opens the consent URL, so no redirect URI or loopback +listener is needed (this is what makes the flow safe for arbitrary self-hosted +origins). +""" + +import time +from dataclasses import dataclass +from urllib.parse import urlencode, urlparse, urlunparse + +import httpx + +from backend.api.features.aimlapi.config import ( + AGENT_NAME, + attribution_headers, + resolve_endpoints, + resolve_partner_id, + resolve_partner_name, + resolve_requested_usd_limit_minor, +) + +DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code" +HTTP_TIMEOUT_SECONDS = 15.0 +# Terminal, non-recoverable outcomes reported by the token endpoint. +TERMINAL_FAILURE_STATUSES = { + "cancelled", + "canceled", + "denied", + "error", + "expired", + "failed", + "rejected", +} + + +class AimlapiAuthError(RuntimeError): + """A safe, user-presentable failure of the AIMLAPI authorization flow.""" + + +@dataclass(frozen=True, slots=True) +class AuthorizationRequest: + request_id: str + device_code: str + verification_uri: str + interval: int + expires_at: float + + +@dataclass(frozen=True, slots=True) +class AuthorizationPollResult: + status: str + api_key: str = "" + + +def _positive_int(value: object, default: int) -> int: + if isinstance(value, bool) or not isinstance(value, (int, float, str)): + return default + try: + parsed = int(value) + except (ValueError, OverflowError): + return default + return parsed if parsed > 0 else default + + +def _response_json(response: httpx.Response) -> dict: + try: + data = response.json() + except ValueError as exc: + raise AimlapiAuthError("AIMLAPI returned an invalid response") from exc + if not isinstance(data, dict): + raise AimlapiAuthError("AIMLAPI returned an invalid response") + return data + + +def _verification_uri(request_id: str) -> str: + """Rebuild the consent URL from the configured verification base. + + The create response returns a production consent URL even on staging, so + the URL opened in the browser is always derived from + ``AIMLAPI_VERIFICATION_BASE_URL`` rather than trusted from the response. + ``source`` rides on the URL so a sign-up during consent is attributed to + this integration (headers cannot cross the browser OAuth redirect). + """ + from backend.api.features.aimlapi.config import AIMLAPI_SOURCE + + endpoint = resolve_endpoints().verification_base_url + parsed = urlparse(endpoint) + if not parsed.scheme or not parsed.netloc: + raise AimlapiAuthError("AIMLAPI verification URL is invalid") + path = f'{parsed.path.rstrip("/")}/agent/authorize' + query = urlencode({"request": request_id, "source": AIMLAPI_SOURCE}) + return urlunparse((parsed.scheme, parsed.netloc, path, "", query, "")) + + +async def start_authorization() -> AuthorizationRequest: + """Create a device authorization and return what the browser/poller need.""" + endpoints = resolve_endpoints() + payload = { + "partnerId": resolve_partner_id(), + "partnerName": resolve_partner_name(), + "agentName": AGENT_NAME, + "returnUrl": endpoints.verification_base_url, + "requestedUsdLimitMinor": resolve_requested_usd_limit_minor(), + } + try: + async with httpx.AsyncClient(timeout=HTTP_TIMEOUT_SECONDS) as client: + response = await client.post( + f"{endpoints.app_base_url}/v3/agent-auth/authorizations", + json=payload, + headers=attribution_headers(), + ) + except httpx.HTTPError as exc: + raise AimlapiAuthError("Unable to start AIMLAPI authorization") from exc + + if response.status_code not in (200, 201): + raise AimlapiAuthError( + f"AIMLAPI authorization failed with HTTP {response.status_code}" + ) + + data = _response_json(response) + request_id = str(data.get("requestId") or "").strip() + device_code = str(data.get("deviceCode") or "").strip() + if not request_id or not device_code: + raise AimlapiAuthError("AIMLAPI authorization response is incomplete") + + interval = _positive_int(data.get("interval"), 5) + expires_in = _positive_int(data.get("expiresIn"), 900) + return AuthorizationRequest( + request_id=request_id, + device_code=device_code, + verification_uri=_verification_uri(request_id), + interval=interval, + expires_at=time.time() + expires_in, + ) + + +async def poll_authorization( + authorization: AuthorizationRequest, +) -> AuthorizationPollResult: + """Exchange the device code for the issued key, or report the wait state.""" + if time.time() >= authorization.expires_at: + return AuthorizationPollResult(status="expired") + + endpoints = resolve_endpoints() + payload = { + "partnerId": resolve_partner_id(), + "deviceCode": authorization.device_code, + "grant_type": DEVICE_CODE_GRANT, + } + try: + async with httpx.AsyncClient(timeout=HTTP_TIMEOUT_SECONDS) as client: + response = await client.post( + f"{endpoints.app_base_url}/v3/agent-auth/token", + json=payload, + headers=attribution_headers(), + ) + except httpx.HTTPError as exc: + raise AimlapiAuthError("Unable to check AIMLAPI authorization") from exc + + if response.status_code not in (200, 201): + raise AimlapiAuthError( + f"AIMLAPI authorization check failed with HTTP {response.status_code}" + ) + + data = _response_json(response) + status = str(data.get("status") or "").strip().lower() + api_key = str( + data.get("apiKey") + or data.get("api_key") + or data.get("access_token") + or data.get("key") + or "" + ).strip() + if api_key: + return AuthorizationPollResult(status="ready", api_key=api_key) + if status in TERMINAL_FAILURE_STATUSES: + return AuthorizationPollResult(status=status) + return AuthorizationPollResult(status=status or "pending") diff --git a/autogpt_platform/backend/backend/api/features/builder/db.py b/autogpt_platform/backend/backend/api/features/builder/db.py index 921007bf11e8..933a499f6cab 100644 --- a/autogpt_platform/backend/backend/api/features/builder/db.py +++ b/autogpt_platform/backend/backend/api/features/builder/db.py @@ -588,6 +588,11 @@ def _build_marketplace_items( return results +# Providers surfaced at the top of the builder integration list (marked +# "Recommended" in the UI). Order within this tuple is the display priority. +FEATURED_PROVIDERS: tuple[str, ...] = ("aiml_api",) + + def get_providers( query: str = "", page: int = 1, @@ -601,7 +606,14 @@ def get_providers( all_providers = _get_all_providers() - for provider in all_providers.values(): + # Recommended providers are surfaced first in the builder's integration list. + # sorted() is stable, so non-featured providers keep their existing order. + ordered_providers = sorted( + all_providers.values(), + key=lambda p: 0 if p.name.value in FEATURED_PROVIDERS else 1, + ) + + for provider in ordered_providers: if ( query not in provider.name.value.lower() and query not in provider.description.lower() diff --git a/autogpt_platform/backend/backend/api/rest_api.py b/autogpt_platform/backend/backend/api/rest_api.py index 5aaf85d1aa21..352a09102bf8 100644 --- a/autogpt_platform/backend/backend/api/rest_api.py +++ b/autogpt_platform/backend/backend/api/rest_api.py @@ -93,6 +93,7 @@ from .external.fastapi_app import external_api from .features.analytics import router as analytics_router +from .features.aimlapi.router import router as aimlapi_router from .features.integrations.router import router as integrations_router from .middleware.security import SecurityHeadersMiddleware from .utils.cors import build_cors_params @@ -386,6 +387,11 @@ async def validation_error_handler( prefix="/api/integrations", tags=["v1", "integrations"], ) +app.include_router( + aimlapi_router, + prefix="/api/aimlapi", + tags=["aimlapi"], +) app.include_router( analytics_router, prefix="/api/analytics", diff --git a/autogpt_platform/backend/backend/blocks/_static_provider_configs.py b/autogpt_platform/backend/backend/blocks/_static_provider_configs.py index a733298448ed..d19e0e2fd55b 100644 --- a/autogpt_platform/backend/backend/blocks/_static_provider_configs.py +++ b/autogpt_platform/backend/backend/blocks/_static_provider_configs.py @@ -21,7 +21,11 @@ _STATIC_PROVIDER_CONFIGS: dict[str, tuple[str, tuple[CredentialsType, ...]]] = { # LLM providers that share blocks/llm.py - "aiml_api": ("Unified access to 100+ AI models", ("api_key",)), + "aiml_api": ( + "One-click access to 1,000+ AI models for text, image, video, audio, " + "and embeddings using a single API key.", + ("api_key",), + ), "anthropic": ("Claude language models", ("api_key",)), "codex": ("Use your ChatGPT plan with Codex App Server", ("oauth2",)), "groq": ("Fast LLM inference", ("api_key",)), diff --git a/autogpt_platform/backend/backend/blocks/llm.py b/autogpt_platform/backend/backend/blocks/llm.py index 067dacaac424..80b24d216a19 100644 --- a/autogpt_platform/backend/backend/blocks/llm.py +++ b/autogpt_platform/backend/backend/blocks/llm.py @@ -23,6 +23,10 @@ BlockSchemaInput, BlockSchemaOutput, ) +from backend.data.llm_registry.llm_models import AIML_HOTTEST_MODELS as AIML_HOTTEST_MODELS +from backend.data.llm_registry.llm_models import ( + AIML_TOKEN_PRICING as AIML_TOKEN_PRICING, +) from backend.data.llm_registry.llm_models import DEFAULT_LLM_MODEL as DEFAULT_LLM_MODEL from backend.data.llm_registry.llm_models import ( LEGACY_MODEL_MAPPINGS as LEGACY_MODEL_MAPPINGS, diff --git a/autogpt_platform/backend/backend/data/block_cost_config.py b/autogpt_platform/backend/backend/data/block_cost_config.py index 20cdc7551f71..462bd4eff646 100644 --- a/autogpt_platform/backend/backend/data/block_cost_config.py +++ b/autogpt_platform/backend/backend/data/block_cost_config.py @@ -39,6 +39,7 @@ from backend.blocks.jina.fact_checker import FactCheckerBlock from backend.blocks.jina.search import ExtractWebsiteContentBlock, SearchTheWebBlock from backend.blocks.llm import ( + AIML_TOKEN_PRICING, MODEL_METADATA, AIConversationBlock, AIListGeneratorBlock, @@ -175,6 +176,14 @@ def _token_cost_from_catalog() -> dict[LLMModel, TokenRate]: TOKEN_COST: dict[LLMModel, TokenRate] = _token_cost_from_catalog() +# Per-token rates for the dynamically-injected AIMLAPI models, derived from the +# catalog's published USD price. Credits/1M = USD/1M × 100 (1 credit ≈ $0.01) +# × 1.5 margin — the same conversion the static entries above use. +for _aiml_model, (_in_usd, _out_usd) in AIML_TOKEN_PRICING.items(): + TOKEN_COST[_aiml_model] = TokenRate( + input=round(_in_usd * 150), output=round(_out_usd * 150) + ) + def compute_token_credits( input_data: BlockInput, stats: "NodeExecutionStats | None" diff --git a/autogpt_platform/backend/backend/data/llm_registry/aiml_catalog.py b/autogpt_platform/backend/backend/data/llm_registry/aiml_catalog.py new file mode 100644 index 000000000000..85dbb6eab382 --- /dev/null +++ b/autogpt_platform/backend/backend/data/llm_registry/aiml_catalog.py @@ -0,0 +1,280 @@ +"""AIMLAPI (aiml_api) dynamic model catalog. + +AIMLAPI is a model aggregator: a single key serves models from many creators +(OpenAI, Anthropic, DeepSeek, Meta, …) through one OpenAI-compatible endpoint. +Rather than hand-maintaining each model as a static ``LlmModel`` member, this +module loads the chat catalog from the public, keyless ``GET /v1/models`` and +lets ``llm.py`` inject the results as ``aiml_api`` members at import time. + +Boot must never depend on the network: the live fetch is best-effort with a +short timeout, and a snapshot committed next to this module +(``data/aiml_models_snapshot.json``) is the guaranteed fallback. On a fresh +deploy the live fetch refreshes the set; if it is unreachable the snapshot +keeps the model list intact. +""" + +import json +import logging +import os +import re +from pathlib import Path +from typing import NamedTuple + +import requests + +logger = logging.getLogger(__name__) + +# Same base URL (and override) the inference transport uses, so the catalog and +# the calls that follow it always target the same environment. +_INFERENCE_BASE = os.getenv("AIMLAPI_INFERENCE_URL", "https://api.aimlapi.com/v1") +_MODELS_URL = f"{_INFERENCE_BASE.rstrip('/')}/models" +_FETCH_TIMEOUT_SECONDS = 5 +_SNAPSHOT_PATH = Path(__file__).parent / "data" / "aiml_models_snapshot.json" + +# Only chat models belong in the LLM block dropdown. +_CHAT_TYPE = "openai/chat-completions" + +# Mirrors ``catalog_model._SLUG_PATTERN`` — a catalog id that cannot satisfy it +# would fail payload validation and take the whole catalog down with it. +_SLUG_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9/._:-]{0,199}$") + + +class AimlModel(NamedTuple): + id: str # exact model string sent to the API (also the LlmModel value) + name: str # human display title + developer: str # model author, used for creator grouping + icon + context_window: int + max_output_tokens: int | None + input_usd_per_1m: float | None + output_usd_per_1m: float | None + is_hottest: bool + + +def member_name(model_id: str) -> str: + """Derive an ``LlmModel`` member name from a raw model id. + + ``"anthropic/claude-opus-4.5"`` -> ``"AIML_ANTHROPIC_CLAUDE_OPUS_4_5"``. + Names are prefixed so they can never collide with the static members. + """ + slug = "".join(ch if ch.isalnum() else "_" for ch in model_id) + while "__" in slug: + slug = slug.replace("__", "_") + return "AIML_" + slug.strip("_").upper() + + +def _token_prices(pricing: dict) -> tuple[float | None, float | None]: + """Extract (input, output) USD-per-1M-token from a model's pricing block. + + AIMLAPI tags both input and output token charges with ``measure:"output"`` + and distinguishes them by ``origin`` — ``provided`` = user-supplied (input) + tokens, ``generated`` = model-produced (output) tokens. + """ + input_price = output_price = None + for unit in (pricing or {}).get("units", []): + if unit.get("name") != "token" or unit.get("price") is None: + continue + per = unit.get("per") or 1_000_000 + usd_per_1m = round(unit["price"] * (1_000_000 / per), 6) + origin = unit.get("origin") + if origin == "provided" and input_price is None: + input_price = usd_per_1m + elif origin == "generated" and output_price is None: + output_price = usd_per_1m + return input_price, output_price + + +def _normalize(entry: dict) -> AimlModel | None: + if entry.get("type") != _CHAT_TYPE: + return None + modalities = entry.get("modalities") or {} + if "text" not in (modalities.get("output") or []): + return None + model_id = entry.get("id") + if not model_id: + return None + info = entry.get("info") or {} + input_price, output_price = _token_prices(entry.get("pricing")) + return AimlModel( + id=model_id, + name=info.get("name") or model_id, + developer=info.get("developer") or "AIMLAPI", + context_window=int(info.get("contextLength") or 0), + max_output_tokens=info.get("outputMax") or None, + input_usd_per_1m=input_price, + output_usd_per_1m=output_price, + is_hottest=bool(info.get("isHottest")), + ) + + +def _from_snapshot_row(row: dict) -> AimlModel: + return AimlModel( + id=row["id"], + name=row.get("name") or row["id"], + developer=row.get("developer") or "AIMLAPI", + context_window=int(row.get("context_length") or 0), + max_output_tokens=row.get("output_max") or None, + input_usd_per_1m=row.get("input_usd_1m"), + output_usd_per_1m=row.get("output_usd_1m"), + is_hottest=bool(row.get("is_hottest")), + ) + + +def _dedupe_hottest_first(models: list[AimlModel]) -> list[AimlModel]: + # Hottest first (stable) so the deduper keeps the hottest variant, then drop + # both exact-id repeats and API alias twins — the catalog exposes the same + # model under several id spellings (e.g. ``gemini-3.6-flash`` vs + # ``gemini-3-6-flash``), which share one (developer, display name). + ordered = sorted(models, key=lambda m: 0 if m.is_hottest else 1) + seen_ids: set[str] = set() + seen_names: set[tuple[str, str]] = set() + unique: list[AimlModel] = [] + for model in ordered: + name_key = (model.developer, model.name) + if model.id in seen_ids or name_key in seen_names: + continue + seen_ids.add(model.id) + seen_names.add(name_key) + unique.append(model) + return unique + + +def _load_snapshot() -> list[AimlModel]: + try: + payload = json.loads(_SNAPSHOT_PATH.read_text()) + return _dedupe_hottest_first( + [_from_snapshot_row(row) for row in payload.get("models", [])] + ) + except (OSError, ValueError, KeyError) as exc: + logger.warning("AIMLAPI model snapshot unavailable: %s", exc) + return [] + + +def _fetch_live() -> list[AimlModel]: + # HEADERS.md: the attribution pair goes on EVERY aimlapi.com request, the + # catalog included — not just inference and sign-up. Imported lazily and + # from the feature config so there is exactly one definition of the pair; + # that module is stdlib-only, so this costs nothing at import time. + from backend.api.features.aimlapi.config import attribution_headers + + response = requests.get( + _MODELS_URL, + params={"include": "capabilities,modalities,pricing"}, + headers=attribution_headers(), + timeout=_FETCH_TIMEOUT_SECONDS, + ) + response.raise_for_status() + data = response.json().get("data") or [] + parsed = [m for m in (_normalize(entry) for entry in data) if m is not None] + return _dedupe_hottest_first(parsed) + + +_cache: list[AimlModel] | None = None + + +def load_aiml_catalog() -> list[AimlModel]: + """Return the AIMLAPI chat catalog (cached for the process lifetime). + + Tries the live endpoint first, falls back to the committed snapshot, and + finally to an empty list — never raises, so importing ``llm.py`` cannot be + blocked by the network. + """ + global _cache + if _cache is not None: + return _cache + try: + live = _fetch_live() + if live: + _cache = live + logger.info("Loaded %d AIMLAPI models from live catalog", len(live)) + return _cache + logger.warning("AIMLAPI live catalog empty; using snapshot") + except (requests.RequestException, ValueError) as exc: + logger.warning("AIMLAPI live catalog fetch failed (%s); using snapshot", exc) + _cache = _load_snapshot() + return _cache + + +# --------------------------------------------------------------------------- +# Catalog injection +# --------------------------------------------------------------------------- +# +# Upstream moved model facts into a catalog-as-code file (``catalog.py``) whose +# ``_build_catalog()`` returns a static ``CatalogPayload``. AIMLAPI is an +# aggregator — one key serves hundreds of models that change weekly — so +# hand-authoring each of them there would go stale between deploys. Instead the +# loader above projects the live catalog into the same ``CatalogModel`` shape +# and ``_build_catalog()`` appends the result, so every downstream consumer +# (registry view, block metadata, cost config) sees AIMLAPI models through the +# ordinary catalog path with no special-casing. + + +def creator_name(developer: str) -> str: + """Slugify a catalog ``developer`` into a ``CatalogCreator.name``. + + The schema constrains creator names to ``^[a-z0-9][a-z0-9._-]{0,99}$``, + while the AIMLAPI catalog reports free-form display names ("Z.ai", + "Mistral AI"). Collapse anything else to "-" and trim. + """ + slug = "".join(ch if ch.isalnum() or ch in "._-" else "-" for ch in developer.lower()) + while "--" in slug: + slug = slug.replace("--", "-") + slug = slug.strip("-._") + return slug[:100] or "unknown" + + +def _price_tier(output_usd_per_1m: float) -> int: + return 1 if output_usd_per_1m <= 5 else 2 if output_usd_per_1m <= 25 else 3 + + +def aiml_catalog_additions( + known_creators: set[str], known_slugs: set[str] +) -> tuple[list, list]: + """Build ``(creators, models)`` to append to the canonical payload. + + ``known_slugs`` are the slugs the static catalog already declares — an + aggregator entry must never shadow a native model and silently reroute + existing graphs through AIMLAPI, so those are skipped. ``known_creators`` + keeps the creator list free of duplicate FK rows. + """ + # Imported here: ``catalog_model`` is schema-only, but keeping the import + # local means the loader stays usable (and testable) without pydantic. + from backend.data.llm_registry.catalog_model import CatalogCreator, CatalogModel, CatalogModelCost + + creators: list[CatalogCreator] = [] + models: list[CatalogModel] = [] + seen_creators = set(known_creators) + + for model in load_aiml_catalog(): + if model.id in known_slugs: + continue + if not _SLUG_RE.match(model.id): + logger.warning("skipping AIMLAPI model with unsupported slug: %s", model.id) + continue + + creator = creator_name(model.developer) + if creator not in seen_creators: + seen_creators.add(creator) + creators.append( + CatalogCreator(name=creator, display_name=model.developer) + ) + + output_usd = model.output_usd_per_1m or 0.0 + models.append( + CatalogModel( + slug=model.id, + display_name=model.name, + provider="aiml_api", + creator=creator, + # The schema requires a positive window; the catalog omits it + # for some entries, so fall back to the common 128k default + # rather than dropping an otherwise-serviceable model. + context_window=model.context_window or 128_000, + max_output_tokens=model.max_output_tokens or None, + price_tier=_price_tier(output_usd), + # Flat per-run tier. Actual billing settles against the + # per-token TOKEN_COST rates derived from AIML_TOKEN_PRICING; + # this keeps the boot-time cost-completeness guard satisfied. + cost=CatalogModelCost(run_credits=_price_tier(output_usd)), + ) + ) + return creators, models diff --git a/autogpt_platform/backend/backend/data/llm_registry/catalog.py b/autogpt_platform/backend/backend/data/llm_registry/catalog.py index 78ee995de907..8791a0f83f2a 100644 --- a/autogpt_platform/backend/backend/data/llm_registry/catalog.py +++ b/autogpt_platform/backend/backend/data/llm_registry/catalog.py @@ -17,10 +17,13 @@ place model facts live. """ +import logging from datetime import datetime, timezone +from backend.data.llm_registry.aiml_catalog import aiml_catalog_additions from backend.data.llm_registry.catalog_model import ( CATALOG_SCHEMA_VERSION, + MAX_CATALOG_MODELS, CatalogCreator, CatalogModel, CatalogModelCost, @@ -28,6 +31,8 @@ CatalogProvider, ) +logger = logging.getLogger(__name__) + _catalog_cache: CatalogPayload | None = None @@ -44,7 +49,47 @@ def get_catalog() -> CatalogPayload: return _catalog_cache + def _build_catalog() -> CatalogPayload: + """Canonical catalog, extended with the AIMLAPI aggregator models. + + AIMLAPI resells hundreds of models behind one key and its line-up moves + faster than our deploy cadence, so those entries are loaded (live, with a + committed snapshot fallback) instead of hand-authored above. Everything + downstream — the registry view, block metadata, cost config — reads them + through the ordinary catalog path. + + This is the one place the catalog is not purely file-derived; the loader + never raises and never blocks boot, so a failed fetch degrades to the + snapshot and then to the static catalog alone. + """ + static = _build_static_catalog() + creators, models = aiml_catalog_additions( + {c.name for c in static.creators}, {m.slug for m in static.models} + ) + if not models: + return static + budget = MAX_CATALOG_MODELS - len(static.models) + if len(models) > budget: + logger.warning( + "AIMLAPI catalog yielded %d models, truncating to the remaining " + "catalog budget of %d", + len(models), + budget, + ) + models = models[:budget] + # ``model_copy`` rather than a fresh ``CatalogPayload``: each injected + # entry was already validated at construction, and re-validating the full + # several-hundred-model payload on every boot buys nothing. + return static.model_copy( + update={ + "creators": [*static.creators, *creators], + "models": [*static.models, *models], + } + ) + + +def _build_static_catalog() -> CatalogPayload: return CatalogPayload( schema_version=CATALOG_SCHEMA_VERSION, generated_at=datetime(2026, 7, 18, tzinfo=timezone.utc), diff --git a/autogpt_platform/backend/backend/data/llm_registry/data/aiml_models_snapshot.json b/autogpt_platform/backend/backend/data/llm_registry/data/aiml_models_snapshot.json new file mode 100644 index 000000000000..97f7e805ee78 --- /dev/null +++ b/autogpt_platform/backend/backend/data/llm_registry/data/aiml_models_snapshot.json @@ -0,0 +1,3366 @@ +{ + "source": "GET https://api.aimlapi.com/v1/models?include=capabilities,modalities,pricing (type=openai/chat-completions, output text)", + "count": 336, + "models": [ + { + "id": "openai/gpt-5.6-luna-pro", + "name": "GPT-5.6 Luna Pro", + "developer": "Open AI", + "context_length": 1050000, + "output_max": 128000, + "input_usd_1m": 2.6, + "output_usd_1m": 15.6, + "is_hottest": true + }, + { + "id": "openai/gpt-5.6-terra-pro", + "name": "GPT-5.6 Terra Pro", + "developer": "Open AI", + "context_length": 1050000, + "output_max": 128000, + "input_usd_1m": 6.5, + "output_usd_1m": 39.0, + "is_hottest": true + }, + { + "id": "openai/gpt-5.6-sol-pro", + "name": "GPT-5.6 Sol Pro", + "developer": "Open AI", + "context_length": 1050000, + "output_max": 128000, + "input_usd_1m": 13.0, + "output_usd_1m": 78.0, + "is_hottest": true + }, + { + "id": "anthropic/claude-fable-5", + "name": "Claude Fable 5", + "developer": "Anthropic", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 13.0, + "output_usd_1m": 65.0, + "is_hottest": true + }, + { + "id": "anthropic/claude-sonnet-5", + "name": "Claude Sonnet 5", + "developer": "Anthropic", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 2.6, + "output_usd_1m": 13.0, + "is_hottest": true + }, + { + "id": "anthropic/claude-opus-5", + "name": "Claude Opus 5", + "developer": "Anthropic", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 6.5, + "output_usd_1m": 32.5, + "is_hottest": true + }, + { + "id": "deepseek/deepseek-v4-pro", + "name": "DeepSeek V4 Pro", + "developer": "DeepSeek AI", + "context_length": 1000000, + "output_max": 384000, + "input_usd_1m": 0.5655, + "output_usd_1m": 1.131, + "is_hottest": true + }, + { + "id": "google/gemini-3.6-flash", + "name": "Gemini 3.6 Flash", + "developer": "Google", + "context_length": 1048576, + "output_max": 65536, + "input_usd_1m": 1.95, + "output_usd_1m": 9.75, + "is_hottest": true + }, + { + "id": "google/gemini-3-6-flash", + "name": "Gemini 3.6 Flash", + "developer": "Google", + "context_length": 1048576, + "output_max": 65536, + "input_usd_1m": 1.95, + "output_usd_1m": 9.75, + "is_hottest": true + }, + { + "id": "zhipu/glm-5.2", + "name": "GLM 5.2", + "developer": "Zhipu AI", + "context_length": 1000000, + "output_max": 131072, + "input_usd_1m": 1.82, + "output_usd_1m": 5.72, + "is_hottest": true + }, + { + "id": "zhipu/glm-5-2", + "name": "GLM 5.2", + "developer": "Zhipu AI", + "context_length": 1000000, + "output_max": 131072, + "input_usd_1m": 1.82, + "output_usd_1m": 5.72, + "is_hottest": true + }, + { + "id": "alibaba/glm-5.2", + "name": "GLM 5.2", + "developer": "Zhipu AI", + "context_length": 1000000, + "output_max": 131072, + "input_usd_1m": 1.82, + "output_usd_1m": 5.72, + "is_hottest": true + }, + { + "id": "alibaba/qwen3.7-max", + "name": "Qwen3.7 Max", + "developer": "Alibaba Cloud", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 2.6, + "output_usd_1m": 7.8, + "is_hottest": true + }, + { + "id": "minimax/minimax-m3", + "name": "MiniMax M3", + "developer": "Minimax AI", + "context_length": 524288, + "output_max": 524288, + "input_usd_1m": 0.39, + "output_usd_1m": 1.56, + "is_hottest": true + }, + { + "id": "moonshot/kimi-k3", + "name": "Kimi K3", + "developer": "Moonshot", + "context_length": 1048576, + "output_max": null, + "input_usd_1m": 3.9, + "output_usd_1m": 19.5, + "is_hottest": true + }, + { + "id": "x-ai/grok-4-5", + "name": "Grok 4.5", + "developer": "X AI", + "context_length": 500000, + "output_max": null, + "input_usd_1m": 2.6, + "output_usd_1m": 7.8, + "is_hottest": true + }, + { + "id": "openai/gpt-3.5-turbo", + "name": "GPT-3.5 Turbo", + "developer": "Open AI", + "context_length": 16000, + "output_max": null, + "input_usd_1m": 0.65, + "output_usd_1m": 1.95, + "is_hottest": false + }, + { + "id": "openai/gpt-3.5-turbo-0125", + "name": "GPT-3.5 Turbo", + "developer": "Open AI", + "context_length": 16000, + "output_max": null, + "input_usd_1m": 0.65, + "output_usd_1m": 1.95, + "is_hottest": false + }, + { + "id": "openai/gpt-3.5-turbo-1106", + "name": "GPT-3.5 Turbo", + "developer": "Open AI", + "context_length": 16000, + "output_max": null, + "input_usd_1m": 1.3, + "output_usd_1m": 2.6, + "is_hottest": false + }, + { + "id": "openai/gpt-4", + "name": "GPT-4", + "developer": "Open AI", + "context_length": 8000, + "output_max": null, + "input_usd_1m": 39.0, + "output_usd_1m": 78.0, + "is_hottest": false + }, + { + "id": "openai/gpt-4.1-mini", + "name": "GPT-4.1 Mini", + "developer": "Open AI", + "context_length": 1000000, + "output_max": null, + "input_usd_1m": 0.52, + "output_usd_1m": 2.08, + "is_hottest": false + }, + { + "id": "openai/gpt-4.1-mini-2025-04-14", + "name": "GPT-4.1 Mini", + "developer": "Open AI", + "context_length": 1000000, + "output_max": null, + "input_usd_1m": 0.52, + "output_usd_1m": 2.08, + "is_hottest": false + }, + { + "id": "openai/gpt-4.1-nano", + "name": "GPT-4.1 Nano", + "developer": "Open AI", + "context_length": 1000000, + "output_max": null, + "input_usd_1m": 0.13, + "output_usd_1m": 0.52, + "is_hottest": false + }, + { + "id": "openai/gpt-4.1-nano-2025-04-14", + "name": "GPT-4.1 Nano", + "developer": "Open AI", + "context_length": 1000000, + "output_max": null, + "input_usd_1m": 0.13, + "output_usd_1m": 0.52, + "is_hottest": false + }, + { + "id": "openai/gpt-4.1", + "name": "GPT-4.1", + "developer": "Open AI", + "context_length": 1047576, + "output_max": 32768, + "input_usd_1m": 2.6, + "output_usd_1m": 10.4, + "is_hottest": false + }, + { + "id": "openai/gpt-4.1-2025-04-14", + "name": "GPT-4.1", + "developer": "Open AI", + "context_length": 1047576, + "output_max": 32768, + "input_usd_1m": 2.6, + "output_usd_1m": 10.4, + "is_hottest": false + }, + { + "id": "openai/gpt-4-turbo", + "name": "GPT-4 Turbo", + "developer": "Open AI", + "context_length": 128000, + "output_max": null, + "input_usd_1m": 13.0, + "output_usd_1m": 39.0, + "is_hottest": false + }, + { + "id": "openai/gpt-4-turbo-2024-04-09", + "name": "GPT-4 Turbo", + "developer": "Open AI", + "context_length": 128000, + "output_max": null, + "input_usd_1m": 13.0, + "output_usd_1m": 39.0, + "is_hottest": false + }, + { + "id": "openai/gpt-4o-mini", + "name": "GPT-4o Mini", + "developer": "Open AI", + "context_length": 128000, + "output_max": null, + "input_usd_1m": 0.195, + "output_usd_1m": 0.78, + "is_hottest": false + }, + { + "id": "openai/gpt-4o-mini-2024-07-18", + "name": "GPT-4o Mini", + "developer": "Open AI", + "context_length": 128000, + "output_max": null, + "input_usd_1m": 0.195, + "output_usd_1m": 0.78, + "is_hottest": false + }, + { + "id": "openai/gpt-4o", + "name": "GPT-4o", + "developer": "Open AI", + "context_length": 128000, + "output_max": 16384, + "input_usd_1m": 3.25, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "openai/gpt-4o-2024-08-06", + "name": "GPT-4o", + "developer": "Open AI", + "context_length": 128000, + "output_max": 16384, + "input_usd_1m": 3.25, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "openai/gpt-4o-2024-11-20", + "name": "GPT-4o", + "developer": "Open AI", + "context_length": 128000, + "output_max": 16384, + "input_usd_1m": 3.25, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "openai/gpt-4o-2024-05-13", + "name": "GPT-4o", + "developer": "Open AI", + "context_length": 128000, + "output_max": 16384, + "input_usd_1m": 6.5, + "output_usd_1m": 19.5, + "is_hottest": false + }, + { + "id": "openai/o1", + "name": "o1 Preview", + "developer": "Open AI", + "context_length": 128000, + "output_max": null, + "input_usd_1m": 19.5, + "output_usd_1m": 78.0, + "is_hottest": false + }, + { + "id": "openai/o1-2024-12-17", + "name": "o1 Preview", + "developer": "Open AI", + "context_length": 128000, + "output_max": null, + "input_usd_1m": 19.5, + "output_usd_1m": 78.0, + "is_hottest": false + }, + { + "id": "openai/o3-mini", + "name": "o3 Mini", + "developer": "Open AI", + "context_length": 200000, + "output_max": null, + "input_usd_1m": 1.43, + "output_usd_1m": 5.72, + "is_hottest": false + }, + { + "id": "openai/o3-mini-2025-01-31", + "name": "o3 Mini", + "developer": "Open AI", + "context_length": 200000, + "output_max": null, + "input_usd_1m": 1.43, + "output_usd_1m": 5.72, + "is_hottest": false + }, + { + "id": "openai/o4-mini-2025-04-16", + "name": "o4 Mini", + "developer": "Open AI", + "context_length": 200000, + "output_max": 100000, + "input_usd_1m": 1.43, + "output_usd_1m": 5.72, + "is_hottest": false + }, + { + "id": "openai/gpt-5-nano-2025-08-07", + "name": "GPT-5 Nano", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 0.065, + "output_usd_1m": 0.52, + "is_hottest": false + }, + { + "id": "openai/gpt-5.1-2025-11-13", + "name": "GPT-5.1", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 1.625, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "openai/gpt-5.2-2025-12-11", + "name": "GPT-5.2", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 2.275, + "output_usd_1m": 18.2, + "is_hottest": false + }, + { + "id": "openai/gpt-5.2-chat-latest", + "name": "GPT-5.2 Chat Latest", + "developer": "Open AI", + "context_length": 128000, + "output_max": 16000, + "input_usd_1m": 2.275, + "output_usd_1m": 18.2, + "is_hottest": false + }, + { + "id": "openai/gpt-5.4-2026-03-05", + "name": "GPT-5.4", + "developer": "Open AI", + "context_length": 1050000, + "output_max": 128000, + "input_usd_1m": 3.25, + "output_usd_1m": 19.5, + "is_hottest": false + }, + { + "id": "openai/gpt-5.5-2026-04-23", + "name": "GPT-5.5", + "developer": "Open AI", + "context_length": 1050000, + "output_max": 128000, + "input_usd_1m": 6.5, + "output_usd_1m": 39.0, + "is_hottest": false + }, + { + "id": "openai/gpt-5.6-sol", + "name": "GPT-5.6 Sol", + "developer": "Open AI", + "context_length": 1050000, + "output_max": 128000, + "input_usd_1m": 6.5, + "output_usd_1m": 39.0, + "is_hottest": false + }, + { + "id": "openai/gpt-5.6-terra", + "name": "GPT-5.6 Terra", + "developer": "Open AI", + "context_length": 1050000, + "output_max": 128000, + "input_usd_1m": 3.25, + "output_usd_1m": 19.5, + "is_hottest": false + }, + { + "id": "openai/gpt-5.6-luna", + "name": "GPT-5.6 Luna", + "developer": "Open AI", + "context_length": 1050000, + "output_max": 128000, + "input_usd_1m": 1.3, + "output_usd_1m": 7.8, + "is_hottest": false + }, + { + "id": "openai/gpt-audio", + "name": "Chat GPT audio", + "developer": "Open AI", + "context_length": 128000, + "output_max": null, + "input_usd_1m": 3.25, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "openai/gpt-audio-2025-08-28", + "name": "Chat GPT audio", + "developer": "Open AI", + "context_length": 128000, + "output_max": null, + "input_usd_1m": 3.25, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "openai/gpt-audio-mini", + "name": "Chat GPT mini audio", + "developer": "Open AI", + "context_length": 128000, + "output_max": null, + "input_usd_1m": 0.78, + "output_usd_1m": 3.12, + "is_hottest": false + }, + { + "id": "openai/gpt-audio-mini-2025-10-06", + "name": "Chat GPT mini audio", + "developer": "Open AI", + "context_length": 128000, + "output_max": null, + "input_usd_1m": 0.78, + "output_usd_1m": 3.12, + "is_hottest": false + }, + { + "id": "openai/o3-2025-04-16", + "name": "o3", + "developer": "Open AI", + "context_length": 200000, + "output_max": 100000, + "input_usd_1m": 2.6, + "output_usd_1m": 10.4, + "is_hottest": false + }, + { + "id": "openai/gpt-5-2025-08-07", + "name": "GPT-5", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 1.625, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "openai/gpt-5-mini-2025-08-07", + "name": "GPT-5 Mini", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 0.325, + "output_usd_1m": 2.6, + "is_hottest": false + }, + { + "id": "openai/gpt-oss-120b", + "name": "GPT OSS 120B", + "developer": "Open AI", + "context_length": 131000, + "output_max": 131072, + "input_usd_1m": 0.455, + "output_usd_1m": 1.235, + "is_hottest": false + }, + { + "id": "openai/gpt-oss-20b", + "name": "GPT OSS 20B", + "developer": "Open AI", + "context_length": 131000, + "output_max": 131072, + "input_usd_1m": 0.0975, + "output_usd_1m": 0.39, + "is_hottest": false + }, + { + "id": "openai/gpt-5", + "name": "GPT-5", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 1.7875, + "output_usd_1m": 14.3, + "is_hottest": false + }, + { + "id": "openai/gpt-5-mini", + "name": "GPT-5 Mini", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 0.3575, + "output_usd_1m": 2.86, + "is_hottest": false + }, + { + "id": "openai/gpt-5-nano", + "name": "GPT-5 Nano", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 0.0715, + "output_usd_1m": 0.572, + "is_hottest": false + }, + { + "id": "openai/gpt-chat-latest", + "name": "GPT Chat Latest", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 6.5, + "output_usd_1m": 39.0, + "is_hottest": false + }, + { + "id": "openai/gpt-5.5-pro", + "name": "GPT-5.5 Pro", + "developer": "Open AI", + "context_length": 1050000, + "output_max": 128000, + "input_usd_1m": 39.0, + "output_usd_1m": 234.0, + "is_hottest": false + }, + { + "id": "openai/gpt-5.4-image-2", + "name": "GPT-5.4 Image 2", + "developer": "Open AI", + "context_length": 272000, + "output_max": 128000, + "input_usd_1m": 10.4, + "output_usd_1m": 19.5, + "is_hottest": false + }, + { + "id": "openai/gpt-5.4-nano", + "name": "GPT-5.4 Nano", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 0.26, + "output_usd_1m": 1.625, + "is_hottest": false + }, + { + "id": "openai/gpt-5.4-mini", + "name": "GPT-5.4 Mini", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 1.95, + "output_usd_1m": 11.7, + "is_hottest": false + }, + { + "id": "openai/gpt-5.4-pro", + "name": "GPT-5.4 Pro", + "developer": "Open AI", + "context_length": 1050000, + "output_max": 128000, + "input_usd_1m": 39.0, + "output_usd_1m": 234.0, + "is_hottest": false + }, + { + "id": "openai/gpt-5.3-chat", + "name": "GPT-5.3 Chat", + "developer": "Open AI", + "context_length": 128000, + "output_max": 16384, + "input_usd_1m": 2.275, + "output_usd_1m": 18.2, + "is_hottest": false + }, + { + "id": "openai/gpt-5.3-codex", + "name": "GPT-5.3 Codex", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 4.55, + "output_usd_1m": 36.4, + "is_hottest": false + }, + { + "id": "openai/gpt-5.2-codex", + "name": "GPT-5.2 Codex", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 4.55, + "output_usd_1m": 36.4, + "is_hottest": false + }, + { + "id": "openai/gpt-5.2-chat", + "name": "GPT-5.2 Chat", + "developer": "Open AI", + "context_length": 128000, + "output_max": 16384, + "input_usd_1m": 2.275, + "output_usd_1m": 18.2, + "is_hottest": false + }, + { + "id": "openai/gpt-5.2-pro", + "name": "GPT-5.2 Pro", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 27.3, + "output_usd_1m": 218.4, + "is_hottest": false + }, + { + "id": "openai/gpt-5.1-codex-max", + "name": "GPT-5.1-Codex-Max", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 3.25, + "output_usd_1m": 26.0, + "is_hottest": false + }, + { + "id": "openai/gpt-5.1-codex", + "name": "GPT-5.1 Codex", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 3.25, + "output_usd_1m": 26.0, + "is_hottest": false + }, + { + "id": "openai/gpt-5.1-codex-mini", + "name": "GPT-5.1 Codex Mini", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 0.325, + "output_usd_1m": 2.6, + "is_hottest": false + }, + { + "id": "openai/gpt-oss-safeguard-20b", + "name": "gpt-oss-safeguard-20b", + "developer": "Open AI", + "context_length": 131072, + "output_max": 65536, + "input_usd_1m": 0.0975, + "output_usd_1m": 0.39, + "is_hottest": false + }, + { + "id": "openai/gpt-5-image-mini", + "name": "GPT-5 Image Mini", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 3.25, + "output_usd_1m": 2.6, + "is_hottest": false + }, + { + "id": "openai/gpt-5-image", + "name": "GPT-5 Image", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 13.0, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "openai/gpt-5-pro", + "name": "GPT-5 Pro", + "developer": "Open AI", + "context_length": 400000, + "output_max": 272000, + "input_usd_1m": 19.5, + "output_usd_1m": 156.0, + "is_hottest": false + }, + { + "id": "openai/o3-pro", + "name": "o3 Pro", + "developer": "Open AI", + "context_length": 200000, + "output_max": null, + "input_usd_1m": 26.0, + "output_usd_1m": 104.0, + "is_hottest": false + }, + { + "id": "openai/o1-pro", + "name": "o1-pro", + "developer": "Open AI", + "context_length": 200000, + "output_max": 100000, + "input_usd_1m": 195.0, + "output_usd_1m": 780.0, + "is_hottest": false + }, + { + "id": "openai/gpt-4-turbo-preview", + "name": "GPT-4 Turbo Preview", + "developer": "Open AI", + "context_length": 128000, + "output_max": 4096, + "input_usd_1m": 13.0, + "output_usd_1m": 39.0, + "is_hottest": false + }, + { + "id": "openai/gpt-latest", + "name": "GPT Latest", + "developer": "Open AI", + "context_length": 1050000, + "output_max": 128000, + "input_usd_1m": 6.5, + "output_usd_1m": 39.0, + "is_hottest": false + }, + { + "id": "openai/gpt-mini-latest", + "name": "GPT Mini Latest", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 0.975, + "output_usd_1m": 5.85, + "is_hottest": false + }, + { + "id": "openai/gpt-5-1", + "name": "GPT-5.1", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 1.625, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "openai/gpt-5-2", + "name": "GPT-5.2", + "developer": "Open AI", + "context_length": 400000, + "output_max": 128000, + "input_usd_1m": 2.275, + "output_usd_1m": 18.2, + "is_hottest": false + }, + { + "id": "openai/gpt-5-2-chat-latest", + "name": "GPT-5.2 Chat Latest", + "developer": "Open AI", + "context_length": 128000, + "output_max": 16000, + "input_usd_1m": 2.275, + "output_usd_1m": 18.2, + "is_hottest": false + }, + { + "id": "openai/gpt-5-4", + "name": "GPT-5.4", + "developer": "Open AI", + "context_length": 1050000, + "output_max": 128000, + "input_usd_1m": 3.25, + "output_usd_1m": 19.5, + "is_hottest": false + }, + { + "id": "openai/gpt-5-5", + "name": "GPT-5.5", + "developer": "Open AI", + "context_length": 1050000, + "output_max": 128000, + "input_usd_1m": 6.5, + "output_usd_1m": 39.0, + "is_hottest": false + }, + { + "id": "anthropic/claude-opus-4-1-20250805", + "name": "Claude 4.1 Opus", + "developer": "Anthropic", + "context_length": 200000, + "output_max": 32000, + "input_usd_1m": 19.5, + "output_usd_1m": 97.5, + "is_hottest": false + }, + { + "id": "anthropic/claude-sonnet-4-5-20250929", + "name": "Claude 4.5 Sonnet", + "developer": "Anthropic", + "context_length": 200000, + "output_max": 64000, + "input_usd_1m": 3.9, + "output_usd_1m": 19.5, + "is_hottest": false + }, + { + "id": "anthropic/claude-haiku-4-5-20251001", + "name": "Claude 4.5 Haiku", + "developer": "Anthropic", + "context_length": 200000, + "output_max": 64000, + "input_usd_1m": 1.3, + "output_usd_1m": 6.5, + "is_hottest": false + }, + { + "id": "anthropic/claude-opus-4-5-20251101", + "name": "Claude 4.5 Opus", + "developer": "Anthropic", + "context_length": 200000, + "output_max": 64000, + "input_usd_1m": 6.5, + "output_usd_1m": 32.5, + "is_hottest": false + }, + { + "id": "anthropic/claude-opus-4-6", + "name": "Claude 4.6 Opus", + "developer": "Anthropic", + "context_length": 200000, + "output_max": 128000, + "input_usd_1m": 6.5, + "output_usd_1m": 32.5, + "is_hottest": false + }, + { + "id": "anthropic/claude-sonnet-4-6", + "name": "Claude 4.6 Sonnet", + "developer": "Anthropic", + "context_length": 200000, + "output_max": 64000, + "input_usd_1m": 3.9, + "output_usd_1m": 19.5, + "is_hottest": false + }, + { + "id": "anthropic/claude-opus-4-7", + "name": "Claude 4.7 Opus", + "developer": "Anthropic", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 6.5, + "output_usd_1m": 32.5, + "is_hottest": false + }, + { + "id": "anthropic/claude-opus-4-8", + "name": "Claude 4.8 Opus", + "developer": "Anthropic", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 6.5, + "output_usd_1m": 32.5, + "is_hottest": false + }, + { + "id": "anthropic/claude-opus-4.8-fast", + "name": "Claude Opus 4.8 (Fast)", + "developer": "Anthropic", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 13.0, + "output_usd_1m": 65.0, + "is_hottest": false + }, + { + "id": "anthropic/claude-opus-4.8", + "name": "Claude 4.8 Opus", + "developer": "Anthropic", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 7.15, + "output_usd_1m": 35.75, + "is_hottest": false + }, + { + "id": "anthropic/claude-opus-4.7-fast", + "name": "Claude Opus 4.7 (Fast)", + "developer": "Anthropic", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 39.0, + "output_usd_1m": 195.0, + "is_hottest": false + }, + { + "id": "anthropic/claude-opus-4.7", + "name": "Claude 4.7 Opus", + "developer": "Anthropic", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 7.15, + "output_usd_1m": 35.75, + "is_hottest": false + }, + { + "id": "anthropic/claude-sonnet-4.6", + "name": "Claude 4.6 Sonnet", + "developer": "Anthropic", + "context_length": 200000, + "output_max": 64000, + "input_usd_1m": 4.29, + "output_usd_1m": 21.45, + "is_hottest": false + }, + { + "id": "anthropic/claude-opus-4.5", + "name": "Claude 4.5 Opus", + "developer": "Anthropic", + "context_length": 200000, + "output_max": 64000, + "input_usd_1m": 7.15, + "output_usd_1m": 35.75, + "is_hottest": false + }, + { + "id": "anthropic/claude-haiku-4.5", + "name": "Claude 4.5 Haiku", + "developer": "Anthropic", + "context_length": 200000, + "output_max": 64000, + "input_usd_1m": 1.43, + "output_usd_1m": 7.15, + "is_hottest": false + }, + { + "id": "anthropic/claude-sonnet-4.5", + "name": "Claude 4.5 Sonnet", + "developer": "Anthropic", + "context_length": 200000, + "output_max": 64000, + "input_usd_1m": 4.29, + "output_usd_1m": 21.45, + "is_hottest": false + }, + { + "id": "anthropic/claude-opus-4.1", + "name": "Claude 4.1 Opus", + "developer": "Anthropic", + "context_length": 200000, + "output_max": 32000, + "input_usd_1m": 19.5, + "output_usd_1m": 97.5, + "is_hottest": false + }, + { + "id": "anthropic/claude-opus-4", + "name": "Claude Opus 4", + "developer": "Anthropic", + "context_length": 200000, + "output_max": 32000, + "input_usd_1m": 19.5, + "output_usd_1m": 97.5, + "is_hottest": false + }, + { + "id": "anthropic/claude-sonnet-4", + "name": "Claude Sonnet 4", + "developer": "Anthropic", + "context_length": 1000000, + "output_max": 64000, + "input_usd_1m": 3.9, + "output_usd_1m": 19.5, + "is_hottest": false + }, + { + "id": "anthropic/claude-3-haiku", + "name": "Claude 3 Haiku", + "developer": "Anthropic", + "context_length": 200000, + "output_max": 4096, + "input_usd_1m": 0.325, + "output_usd_1m": 1.625, + "is_hottest": false + }, + { + "id": "anthropic/claude-fable-latest", + "name": "Claude Fable Latest", + "developer": "Anthropic", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 13.0, + "output_usd_1m": 65.0, + "is_hottest": false + }, + { + "id": "anthropic/claude-haiku-latest", + "name": "Claude Haiku Latest", + "developer": "Anthropic", + "context_length": 200000, + "output_max": 64000, + "input_usd_1m": 1.3, + "output_usd_1m": 6.5, + "is_hottest": false + }, + { + "id": "anthropic/claude-sonnet-latest", + "name": "Claude Sonnet Latest", + "developer": "Anthropic", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 2.6, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "anthropic/claude-opus-latest", + "name": "Claude Opus Latest", + "developer": "Anthropic", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 6.5, + "output_usd_1m": 32.5, + "is_hottest": false + }, + { + "id": "bytedance/seed-1-8", + "name": "Seed 1.8", + "developer": "ByteDance", + "context_length": 256000, + "output_max": 224000, + "input_usd_1m": 0.325, + "output_usd_1m": 2.6, + "is_hottest": false + }, + { + "id": "bytedance/seed-2-0-pro", + "name": "Dola-Seed 2.0 Pro", + "developer": "ByteDance", + "context_length": 256000, + "output_max": 128000, + "input_usd_1m": 0.65, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "bytedance/seed-2-0-lite", + "name": "Dola-Seed 2.0 Lite", + "developer": "ByteDance", + "context_length": 256000, + "output_max": 128000, + "input_usd_1m": 0.325, + "output_usd_1m": 2.6, + "is_hottest": false + }, + { + "id": "bytedance/seed-2-0-mini", + "name": "Dola-Seed 2.0 Mini", + "developer": "ByteDance", + "context_length": 256000, + "output_max": 128000, + "input_usd_1m": 0.13, + "output_usd_1m": 0.52, + "is_hottest": false + }, + { + "id": "bytedance/seed-2-0-code-preview", + "name": "Dola-Seed 2.0 Code", + "developer": "ByteDance", + "context_length": 256000, + "output_max": 128000, + "input_usd_1m": 0.65, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "bytedance/dola-seed-2-0-pro", + "name": "Dola-Seed 2.0 Pro", + "developer": "ByteDance", + "context_length": 256000, + "output_max": 128000, + "input_usd_1m": 0.65, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "bytedance/dola-seed-2-0-lite", + "name": "Dola-Seed 2.0 Lite", + "developer": "ByteDance", + "context_length": 256000, + "output_max": 128000, + "input_usd_1m": 0.325, + "output_usd_1m": 2.6, + "is_hottest": false + }, + { + "id": "bytedance/dola-seed-2-0-mini", + "name": "Dola-Seed 2.0 Mini", + "developer": "ByteDance", + "context_length": 256000, + "output_max": 128000, + "input_usd_1m": 0.13, + "output_usd_1m": 0.52, + "is_hottest": false + }, + { + "id": "bytedance/dola-seed-2-0-code", + "name": "Dola-Seed 2.0 Code", + "developer": "ByteDance", + "context_length": 256000, + "output_max": 128000, + "input_usd_1m": 0.65, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "deepseek/deepseek-reasoner", + "name": "DeepSeek R1", + "developer": "DeepSeek AI", + "context_length": 128000, + "output_max": 127000, + "input_usd_1m": 0.364, + "output_usd_1m": 0.546, + "is_hottest": false + }, + { + "id": "deepseek/deepseek-chat", + "name": "DeepSeek V3", + "developer": "DeepSeek AI", + "context_length": 128000, + "output_max": 124000, + "input_usd_1m": 0.364, + "output_usd_1m": 0.546, + "is_hottest": false + }, + { + "id": "deepseek/deepseek-v4-flash", + "name": "DeepSeek V4 Flash", + "developer": "DeepSeek AI", + "context_length": 1000000, + "output_max": 384000, + "input_usd_1m": 0.182, + "output_usd_1m": 0.364, + "is_hottest": false + }, + { + "id": "deepseek/deepseek-chat-v3.1", + "name": "DeepSeek V3.1", + "developer": "DeepSeek AI", + "context_length": 128000, + "output_max": 8000, + "input_usd_1m": 0.845, + "output_usd_1m": 2.21, + "is_hottest": false + }, + { + "id": "deepseek/deepseek-non-reasoner-v3.1-terminus", + "name": "DeepSeek V3.1", + "developer": "DeepSeek AI", + "context_length": 128000, + "output_max": 8000, + "input_usd_1m": 0.351, + "output_usd_1m": 1.235, + "is_hottest": false + }, + { + "id": "deepseek/deepseek-reasoner-v3.1-terminus", + "name": "DeepSeek V3.1", + "developer": "DeepSeek AI", + "context_length": 128000, + "output_max": 8000, + "input_usd_1m": 0.351, + "output_usd_1m": 1.235, + "is_hottest": false + }, + { + "id": "deepseek/deepseek-r1", + "name": "DeepSeek Reasoner V3.1", + "developer": "DeepSeek AI", + "context_length": 128000, + "output_max": 64000, + "input_usd_1m": 0.65, + "output_usd_1m": 2.795, + "is_hottest": false + }, + { + "id": "deepseek/deepseek-non-thinking-v3.2-exp", + "name": "DeepSeek V3.2 Exp", + "developer": "DeepSeek AI", + "context_length": 128000, + "output_max": 64000, + "input_usd_1m": 0.351, + "output_usd_1m": 0.533, + "is_hottest": false + }, + { + "id": "deepseek/deepseek-thinking-v3.2-exp", + "name": "DeepSeek V3.2 Exp", + "developer": "DeepSeek AI", + "context_length": 128000, + "output_max": 64000, + "input_usd_1m": 0.351, + "output_usd_1m": 0.533, + "is_hottest": false + }, + { + "id": "meituan/longcat-2.0", + "name": "LongCat-2.0", + "developer": "Meituan", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 0.975, + "output_usd_1m": 3.835, + "is_hottest": false + }, + { + "id": "google/gemini-2.5-flash", + "name": "Gemini 2.5 Flash", + "developer": "Google", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 0.39, + "output_usd_1m": 3.25, + "is_hottest": false + }, + { + "id": "google/gemini-3-flash-preview", + "name": "Gemini 3 Flash Preview", + "developer": "Google", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 0.65, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "google/gemini-3.5-flash", + "name": "Gemini 3.5 Flash", + "developer": "Google", + "context_length": 1048576, + "output_max": 65536, + "input_usd_1m": 0.65, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "google/gemini-2.5-flash-lite", + "name": "Gemini 2.5 Flash Lite Preview", + "developer": "Google", + "context_length": 1000000, + "output_max": 1048576, + "input_usd_1m": 0.0975, + "output_usd_1m": 0.39, + "is_hottest": false + }, + { + "id": "google/gemini-3.1-flash-lite", + "name": "Gemini 3.1 Flash Lite", + "developer": "Google", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 0.325, + "output_usd_1m": 1.95, + "is_hottest": false + }, + { + "id": "google/gemini-3.5-flash-lite", + "name": "Gemini 3.5 Flash Lite", + "developer": "Google", + "context_length": 1048576, + "output_max": 65536, + "input_usd_1m": 0.39, + "output_usd_1m": 3.25, + "is_hottest": false + }, + { + "id": "google/gemini-2.5-pro", + "name": "Gemini 2.5 Pro", + "developer": "Google", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 1.625, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "google/gemini-3.1-pro-preview", + "name": "Gemini 3.1 Pro Preview", + "developer": "Google", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 2.6, + "output_usd_1m": 15.6, + "is_hottest": false + }, + { + "id": "google/gemma-4-26b-a4b-it-maas", + "name": "Gemma 4 26B A4B IT", + "developer": "Google", + "context_length": 262144, + "output_max": 128000, + "input_usd_1m": 0.195, + "output_usd_1m": 0.78, + "is_hottest": false + }, + { + "id": "google/gemma-3n-e4b-it", + "name": "Gemma 3n 4B", + "developer": "Google", + "context_length": 8192, + "output_max": 2048, + "input_usd_1m": 0.026, + "output_usd_1m": 0.052, + "is_hottest": false + }, + { + "id": "google/gemma-3-4b-it", + "name": "Gemma 3 4B", + "developer": "Google", + "context_length": 131000, + "output_max": 32768, + "input_usd_1m": 0.065, + "output_usd_1m": 0.13, + "is_hottest": false + }, + { + "id": "google/gemma-3-12b-it", + "name": "Gemma 3 12B", + "developer": "Google", + "context_length": 131000, + "output_max": 96000, + "input_usd_1m": 0.065, + "output_usd_1m": 0.195, + "is_hottest": false + }, + { + "id": "google/gemma-3-27b-it", + "name": "Gemma 3 27B", + "developer": "Google", + "context_length": 128000, + "output_max": 131072, + "input_usd_1m": 0.195, + "output_usd_1m": 0.598, + "is_hottest": false + }, + { + "id": "google/gemma-4-31b-it", + "name": "Gemma 4 31B Instruct", + "developer": "Google", + "context_length": 256000, + "output_max": 262144, + "input_usd_1m": 1.287, + "output_usd_1m": 1.937, + "is_hottest": false + }, + { + "id": "google/gemma-4-26b-a4b-it", + "name": "Gemma 4 26B A4B IT", + "developer": "Google", + "context_length": 262144, + "output_max": 128000, + "input_usd_1m": 0.195, + "output_usd_1m": 0.78, + "is_hottest": false + }, + { + "id": "google/gemini-3.1-flash-lite-image", + "name": "Nano Banana 2 Lite (Gemini 3.1 Flash Lite Image)", + "developer": "Google", + "context_length": 65536, + "output_max": 66000, + "input_usd_1m": 0.325, + "output_usd_1m": 1.95, + "is_hottest": false + }, + { + "id": "google/gemini-3.1-flash-image", + "name": "Nano Banana 2 (Gemini 3.1 Flash Image)", + "developer": "Google", + "context_length": 131072, + "output_max": 32768, + "input_usd_1m": 0.65, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "google/gemini-3-pro-image", + "name": "Nano Banana Pro (Gemini 3 Pro Image)", + "developer": "Google", + "context_length": 65536, + "output_max": 32768, + "input_usd_1m": 2.6, + "output_usd_1m": 15.6, + "is_hottest": false + }, + { + "id": "google/gemini-3.1-flash-lite-preview", + "name": "Gemini 3.1 Flash Lite Preview", + "developer": "Google", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 0.325, + "output_usd_1m": 1.95, + "is_hottest": false + }, + { + "id": "google/gemini-3.1-flash-image-preview", + "name": "Nano Banana 2 (Gemini 3.1 Flash Image Preview)", + "developer": "Google", + "context_length": 131072, + "output_max": 32768, + "input_usd_1m": 0.65, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "google/gemini-3.1-pro-preview-customtools", + "name": "Gemini 3.1 Pro Preview Custom Tools", + "developer": "Google", + "context_length": 1048756, + "output_max": 65536, + "input_usd_1m": 2.6, + "output_usd_1m": 15.6, + "is_hottest": false + }, + { + "id": "google/gemini-3-pro-image-preview", + "name": "Nano Banana Pro (Gemini 3 Pro Image Preview)", + "developer": "Google", + "context_length": 65536, + "output_max": 32768, + "input_usd_1m": 2.6, + "output_usd_1m": 15.6, + "is_hottest": false + }, + { + "id": "google/gemini-2.5-flash-image", + "name": "Nano Banana (Gemini 2.5 Flash Image)", + "developer": "Google", + "context_length": 32768, + "output_max": 32768, + "input_usd_1m": 0.702, + "output_usd_1m": 5.85, + "is_hottest": false + }, + { + "id": "google/gemini-2.5-pro-preview", + "name": "Gemini 2.5 Pro Preview 06-05", + "developer": "Google", + "context_length": 1048576, + "output_max": 65536, + "input_usd_1m": 1.625, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "google/gemini-2.5-pro-preview-05-06", + "name": "Gemini 2.5 Pro Preview 05-06", + "developer": "Google", + "context_length": 1048576, + "output_max": 65535, + "input_usd_1m": 1.625, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "google/gemma-2-27b-it", + "name": "Gemma 2 27B", + "developer": "Google", + "context_length": 8192, + "output_max": 2048, + "input_usd_1m": 0.845, + "output_usd_1m": 0.845, + "is_hottest": false + }, + { + "id": "google/gemini-2.5-flash-lite-preview", + "name": "Gemini 2.5 Flash Lite Preview", + "developer": "Google", + "context_length": 1000000, + "output_max": 1048576, + "input_usd_1m": 0.0975, + "output_usd_1m": 0.39, + "is_hottest": false + }, + { + "id": "google/gemini-3-1-flash-lite-preview", + "name": "Gemini 3.1 Flash Lite Preview", + "developer": "Google", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 0.325, + "output_usd_1m": 1.95, + "is_hottest": false + }, + { + "id": "google/gemini-3-5-flash", + "name": "Gemini 3.5 Flash", + "developer": "Google", + "context_length": 1048576, + "output_max": 65536, + "input_usd_1m": 0.65, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "google/gemini-3-5-flash-lite", + "name": "Gemini 3.5 Flash Lite", + "developer": "Google", + "context_length": 1048576, + "output_max": 65536, + "input_usd_1m": 0.39, + "output_usd_1m": 3.25, + "is_hottest": false + }, + { + "id": "google/gemini-3-1-flash-lite", + "name": "Gemini 3.1 Flash Lite", + "developer": "Google", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 0.325, + "output_usd_1m": 1.95, + "is_hottest": false + }, + { + "id": "google/gemini-pro-latest", + "name": "Gemini Pro Latest", + "developer": "Google", + "context_length": 1048576, + "output_max": 65536, + "input_usd_1m": 2.6, + "output_usd_1m": 15.6, + "is_hottest": false + }, + { + "id": "google/gemini-flash-latest", + "name": "Gemini Flash Latest", + "developer": "Google", + "context_length": 1048576, + "output_max": 65536, + "input_usd_1m": 1.95, + "output_usd_1m": 11.7, + "is_hottest": false + }, + { + "id": "meta/muse-spark-1.1", + "name": "Muse Spark 1.1", + "developer": "Meta", + "context_length": 1048576, + "output_max": null, + "input_usd_1m": 1.625, + "output_usd_1m": 5.525, + "is_hottest": false + }, + { + "id": "meta/muse-spark-1.2", + "name": "Muse Spark 1.2", + "developer": "Meta", + "context_length": 1048576, + "output_max": null, + "input_usd_1m": 1.625, + "output_usd_1m": 5.525, + "is_hottest": false + }, + { + "id": "meta-llama/Llama-3.3-70B-Instruct-Turbo", + "name": "Llama 3.3 70B Instruct Turbo", + "developer": "Meta", + "context_length": 128000, + "output_max": 127000, + "input_usd_1m": 1.144, + "output_usd_1m": 1.144, + "is_hottest": false + }, + { + "id": "meta-llama/llama-3.3-70b-versatile", + "name": "Llama 3.3 70B Versatile", + "developer": "Meta", + "context_length": 131072, + "output_max": 32768, + "input_usd_1m": 0.767, + "output_usd_1m": 1.027, + "is_hottest": false + }, + { + "id": "mistralai/mistral-nemo", + "name": "Mistral NeMo", + "developer": "Mistral AI", + "context_length": 128000, + "output_max": 131072, + "input_usd_1m": 0.195, + "output_usd_1m": 0.221, + "is_hottest": false + }, + { + "id": "mistralai/mistral-medium-3-5", + "name": "Medium 3.5", + "developer": "Mistral AI", + "context_length": 262144, + "output_max": null, + "input_usd_1m": 1.95, + "output_usd_1m": 9.75, + "is_hottest": false + }, + { + "id": "mistralai/mistral-small-2603", + "name": "Small 4", + "developer": "Mistral AI", + "context_length": 262144, + "output_max": null, + "input_usd_1m": 0.24375, + "output_usd_1m": 0.975, + "is_hottest": false + }, + { + "id": "mistralai/ministral-14b-2512", + "name": "Ministral 3 14B 2512", + "developer": "Mistral AI", + "context_length": 262144, + "output_max": null, + "input_usd_1m": 0.455, + "output_usd_1m": 0.455, + "is_hottest": false + }, + { + "id": "mistralai/ministral-8b-2512", + "name": "Ministral 3 8B 2512", + "developer": "Mistral AI", + "context_length": 262144, + "output_max": null, + "input_usd_1m": 0.39, + "output_usd_1m": 0.39, + "is_hottest": false + }, + { + "id": "mistralai/ministral-3b-2512", + "name": "Ministral 3 3B 2512", + "developer": "Mistral AI", + "context_length": 131072, + "output_max": null, + "input_usd_1m": 0.195, + "output_usd_1m": 0.195, + "is_hottest": false + }, + { + "id": "mistralai/mistral-large-2512", + "name": "Large 3 2512", + "developer": "Mistral AI", + "context_length": 262144, + "output_max": null, + "input_usd_1m": 0.65, + "output_usd_1m": 1.95, + "is_hottest": false + }, + { + "id": "mistralai/voxtral-small-24b-2507", + "name": "Voxtral Small 24B 2507", + "developer": "Mistral AI", + "context_length": 32000, + "output_max": null, + "input_usd_1m": 0.13, + "output_usd_1m": 0.39, + "is_hottest": false + }, + { + "id": "mistralai/mistral-medium-3.1", + "name": "Medium 3.1", + "developer": "Mistral AI", + "context_length": 131072, + "output_max": null, + "input_usd_1m": 0.52, + "output_usd_1m": 2.6, + "is_hottest": false + }, + { + "id": "mistralai/codestral-2508", + "name": "Codestral 2508", + "developer": "Mistral AI", + "context_length": 256000, + "output_max": null, + "input_usd_1m": 0.39, + "output_usd_1m": 1.17, + "is_hottest": false + }, + { + "id": "mistralai/mistral-small-3.2-24b-instruct", + "name": "Small 3.2 24B", + "developer": "Mistral AI", + "context_length": 128000, + "output_max": 16384, + "input_usd_1m": 0.0975, + "output_usd_1m": 0.26, + "is_hottest": false + }, + { + "id": "mistralai/mistral-medium-3", + "name": "Medium 3", + "developer": "Mistral AI", + "context_length": 131072, + "output_max": null, + "input_usd_1m": 0.52, + "output_usd_1m": 2.6, + "is_hottest": false + }, + { + "id": "mistralai/mistral-small-3.1-24b-instruct", + "name": "Small 3.1 24B", + "developer": "Mistral AI", + "context_length": 128000, + "output_max": 128000, + "input_usd_1m": 0.4563, + "output_usd_1m": 0.7215, + "is_hottest": false + }, + { + "id": "mistralai/mistral-saba", + "name": "Saba", + "developer": "Mistral AI", + "context_length": 32768, + "output_max": null, + "input_usd_1m": 0.26, + "output_usd_1m": 0.78, + "is_hottest": false + }, + { + "id": "mistralai/mistral-small-24b-instruct-2501", + "name": "Small 3", + "developer": "Mistral AI", + "context_length": 32768, + "output_max": 16384, + "input_usd_1m": 0.065, + "output_usd_1m": 0.104, + "is_hottest": false + }, + { + "id": "mistralai/mistral-large-2407", + "name": "Large 2407", + "developer": "Mistral AI", + "context_length": 131072, + "output_max": null, + "input_usd_1m": 2.6, + "output_usd_1m": 7.8, + "is_hottest": false + }, + { + "id": "mistralai/mixtral-8x22b-instruct", + "name": "Mixtral 8x22B Instruct", + "developer": "Mistral AI", + "context_length": 65536, + "output_max": null, + "input_usd_1m": 2.6, + "output_usd_1m": 7.8, + "is_hottest": false + }, + { + "id": "mistralai/mistral-large", + "name": "Large", + "developer": "Mistral AI", + "context_length": 128000, + "output_max": null, + "input_usd_1m": 2.6, + "output_usd_1m": 7.8, + "is_hottest": false + }, + { + "id": "zhipu/glm-5.1", + "name": "GLM 5.1", + "developer": "Zhipu AI", + "context_length": 200000, + "output_max": 128000, + "input_usd_1m": 1.82, + "output_usd_1m": 5.72, + "is_hottest": false + }, + { + "id": "zhipu/glm-5-1", + "name": "GLM 5.1", + "developer": "Zhipu AI", + "context_length": 200000, + "output_max": 128000, + "input_usd_1m": 1.82, + "output_usd_1m": 5.72, + "is_hottest": false + }, + { + "id": "zhipu/glm-5", + "name": "GLM 5", + "developer": "Zhipu AI", + "context_length": 200000, + "output_max": 128000, + "input_usd_1m": 1.3, + "output_usd_1m": 4.16, + "is_hottest": false + }, + { + "id": "zhipu/glm-4.7", + "name": "GLM 4.7", + "developer": "Zhipu AI", + "context_length": 200000, + "output_max": 128000, + "input_usd_1m": 0.78, + "output_usd_1m": 2.86, + "is_hottest": false + }, + { + "id": "zhipu/glm-4.6", + "name": "GLM 4.6", + "developer": "Zhipu AI", + "context_length": 200000, + "output_max": 128000, + "input_usd_1m": 0.78, + "output_usd_1m": 2.86, + "is_hottest": false + }, + { + "id": "zhipu/glm-4.5", + "name": "GLM 4.5", + "developer": "Zhipu AI", + "context_length": 128000, + "output_max": 98000, + "input_usd_1m": 0.78, + "output_usd_1m": 2.86, + "is_hottest": false + }, + { + "id": "zhipu/glm-4.5-air", + "name": "GLM 4.5 Air", + "developer": "Zhipu AI", + "context_length": 128000, + "output_max": 98000, + "input_usd_1m": 0.26, + "output_usd_1m": 1.43, + "is_hottest": false + }, + { + "id": "alibaba/glm-5.2-fast-preview", + "name": "GLM 5.2 Fast Preview", + "developer": "Zhipu AI", + "context_length": 1000000, + "output_max": 131072, + "input_usd_1m": 4.55, + "output_usd_1m": 14.3, + "is_hottest": false + }, + { + "id": "alibaba/qwen-max", + "name": "Qwen Max", + "developer": "Alibaba Cloud", + "context_length": 32000, + "output_max": 8192, + "input_usd_1m": 2.08, + "output_usd_1m": 8.32, + "is_hottest": false + }, + { + "id": "alibaba/qwen-plus", + "name": "Qwen Plus", + "developer": "Alibaba Cloud", + "context_length": 131000, + "output_max": 16384, + "input_usd_1m": 0.52, + "output_usd_1m": 1.56, + "is_hottest": false + }, + { + "id": "alibaba/qwen-turbo", + "name": "Qwen Turbo", + "developer": "Alibaba Cloud", + "context_length": 1000000, + "output_max": 16384, + "input_usd_1m": 0.065, + "output_usd_1m": 0.26, + "is_hottest": false + }, + { + "id": "alibaba/qwen3-32b", + "name": "Qwen3 32B", + "developer": "Alibaba Cloud", + "context_length": 131000, + "output_max": 16384, + "input_usd_1m": 0.208, + "output_usd_1m": 0.832, + "is_hottest": false + }, + { + "id": "alibaba/qwen3-235b-a22b-thinking-2507", + "name": "Qwen3 Thinking 2507", + "developer": "Alibaba Cloud", + "context_length": 32000, + "output_max": 16000, + "input_usd_1m": 0.299, + "output_usd_1m": 2.99, + "is_hottest": false + }, + { + "id": "alibaba/qwen3.5-flash", + "name": "Qwen3.5 Flash", + "developer": "Alibaba Cloud", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 0.13, + "output_usd_1m": 0.52, + "is_hottest": false + }, + { + "id": "alibaba/qwen3.5-plus", + "name": "Qwen3.5 Plus", + "developer": "Alibaba Cloud", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 0.52, + "output_usd_1m": 3.12, + "is_hottest": false + }, + { + "id": "alibaba/qwen3-coder-480b-a35b-instruct", + "name": "Qwen3 Coder", + "developer": "Alibaba Cloud", + "context_length": 262000, + "output_max": 65536, + "input_usd_1m": 1.95, + "output_usd_1m": 9.75, + "is_hottest": false + }, + { + "id": "alibaba/qwen3-next-80b-a3b-thinking", + "name": "Qwen3 Next 80B A3B Thinking", + "developer": "Alibaba Cloud", + "context_length": 126976, + "output_max": 81920, + "input_usd_1m": 0.195, + "output_usd_1m": 1.56, + "is_hottest": false + }, + { + "id": "alibaba/qwen3-next-80b-a3b-instruct", + "name": "Qwen3 Next 80B A3B Instruct", + "developer": "Alibaba Cloud", + "context_length": 129024, + "output_max": 16384, + "input_usd_1m": 0.195, + "output_usd_1m": 1.56, + "is_hottest": false + }, + { + "id": "alibaba/qwen3-max-preview", + "name": "Qwen3 Max Preview", + "developer": "Alibaba Cloud", + "context_length": 252000, + "output_max": 32768, + "input_usd_1m": 1.56, + "output_usd_1m": 7.8, + "is_hottest": false + }, + { + "id": "alibaba/qwen3-max", + "name": "Qwen3 Max Instruct", + "developer": "Alibaba Cloud", + "context_length": 262144, + "output_max": 65536, + "input_usd_1m": 1.56, + "output_usd_1m": 7.8, + "is_hottest": false + }, + { + "id": "alibaba/qwen3-vl-plus", + "name": "Qwen3 VL Plus", + "developer": "Alibaba Cloud", + "context_length": 262144, + "output_max": 32768, + "input_usd_1m": 0.26, + "output_usd_1m": 2.08, + "is_hottest": false + }, + { + "id": "alibaba/qwen3-vl-flash", + "name": "Qwen3 VL Flash", + "developer": "Alibaba Cloud", + "context_length": 262144, + "output_max": 32768, + "input_usd_1m": 0.065, + "output_usd_1m": 0.52, + "is_hottest": false + }, + { + "id": "alibaba/qwen3-vl-32b-thinking", + "name": "Qwen3 VL 32B Thinking", + "developer": "Alibaba Cloud", + "context_length": 126000, + "output_max": 32768, + "input_usd_1m": 0.91, + "output_usd_1m": 10.92, + "is_hottest": false + }, + { + "id": "alibaba/qwen3-vl-32b-instruct", + "name": "Qwen3 VL 32B Instruct", + "developer": "Alibaba Cloud", + "context_length": 126000, + "output_max": 32768, + "input_usd_1m": 0.91, + "output_usd_1m": 3.64, + "is_hottest": false + }, + { + "id": "alibaba/qwen3-omni-30b-a3b-captioner", + "name": "Qwen3 Omni Captioner", + "developer": "Alibaba Cloud", + "context_length": 65536, + "output_max": 32768, + "input_usd_1m": 4.953, + "output_usd_1m": 3.978, + "is_hottest": false + }, + { + "id": "alibaba/qwen3.5-omni-plus", + "name": "Qwen3.5 Omni Plus", + "developer": "Alibaba Cloud", + "context_length": 256000, + "output_max": 192000, + "input_usd_1m": 1.82, + "output_usd_1m": 10.79, + "is_hottest": false + }, + { + "id": "alibaba/qwen3.5-omni-flash", + "name": "Qwen3.5 Omni Flash", + "developer": "Alibaba Cloud", + "context_length": 256000, + "output_max": 192000, + "input_usd_1m": 0.52, + "output_usd_1m": 2.86, + "is_hottest": false + }, + { + "id": "alibaba/qwen3.6-max-preview", + "name": "Qwen3.6 Max Preview", + "developer": "Alibaba Cloud", + "context_length": 262144, + "output_max": 65536, + "input_usd_1m": 0.65, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "alibaba/qwen3.8-max", + "name": "Qwen3.8 Max", + "developer": "Alibaba Cloud", + "context_length": 1000000, + "output_max": 131072, + "input_usd_1m": 2.6, + "output_usd_1m": 7.8, + "is_hottest": false + }, + { + "id": "alibaba/qwen3.7-plus", + "name": "Qwen3.7-Plus", + "developer": "Alibaba Cloud", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 0.416, + "output_usd_1m": 1.664, + "is_hottest": false + }, + { + "id": "alibaba/qwen3.6-plus", + "name": "Qwen3.6 Plus", + "developer": "Alibaba Cloud", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 0.52, + "output_usd_1m": 3.12, + "is_hottest": false + }, + { + "id": "alibaba/qwen3.6-flash", + "name": "Qwen3.6 Flash", + "developer": "Alibaba Cloud", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 0.325, + "output_usd_1m": 1.95, + "is_hottest": false + }, + { + "id": "alibaba/qwen3.6-27b", + "name": "Qwen3.6 27B", + "developer": "Alibaba Cloud", + "context_length": 256000, + "output_max": 252000, + "input_usd_1m": 0.78, + "output_usd_1m": 4.68, + "is_hottest": false + }, + { + "id": "alibaba/qwen3.6-35b-a3b", + "name": "Qwen3.6 35B A3B", + "developer": "Alibaba Cloud", + "context_length": 256000, + "output_max": 252000, + "input_usd_1m": 0.4875, + "output_usd_1m": 2.925, + "is_hottest": false + }, + { + "id": "zhipu/glm-5-2-fast-preview", + "name": "GLM 5.2 Fast Preview", + "developer": "Zhipu AI", + "context_length": 1000000, + "output_max": 131072, + "input_usd_1m": 4.55, + "output_usd_1m": 14.3, + "is_hottest": false + }, + { + "id": "alibaba/qwen3.5-plus-20260218", + "name": "Qwen3.5 Plus", + "developer": "Alibaba Cloud", + "context_length": 1000000, + "output_max": 65536, + "input_usd_1m": 0.52, + "output_usd_1m": 3.12, + "is_hottest": false + }, + { + "id": "Qwen/Qwen2.5-7B-Instruct-Turbo", + "name": "Qwen2.5 7B Instruct Turbo", + "developer": "Alibaba Cloud", + "context_length": 32000, + "output_max": 31000, + "input_usd_1m": 0.39, + "output_usd_1m": 0.39, + "is_hottest": false + }, + { + "id": "Qwen/Qwen3-235B-A22B-Thinking-2507", + "name": "Qwen3 Next 80B A3B Thinking", + "developer": "Alibaba Cloud", + "context_length": 126976, + "output_max": 81920, + "input_usd_1m": 0.845, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", + "name": "Qwen3 Coder", + "developer": "Alibaba Cloud", + "context_length": 262000, + "output_max": 65536, + "input_usd_1m": 2.6, + "output_usd_1m": 2.6, + "is_hottest": false + }, + { + "id": "minimax/MiniMax-Text-01", + "name": "Text 01", + "developer": "Minimax AI", + "context_length": 1000000, + "output_max": 40000, + "input_usd_1m": 0.26, + "output_usd_1m": 1.43, + "is_hottest": false + }, + { + "id": "minimax/m1", + "name": "MiniMax M1", + "developer": "Minimax AI", + "context_length": 1000000, + "output_max": 40960, + "input_usd_1m": 0.52, + "output_usd_1m": 2.86, + "is_hottest": false + }, + { + "id": "minimax/m2", + "name": "MiniMax M2", + "developer": "Minimax AI", + "context_length": 1000000, + "output_max": 40960, + "input_usd_1m": 0.39, + "output_usd_1m": 1.56, + "is_hottest": false + }, + { + "id": "minimax/m2-her", + "name": "MiniMax M2-her", + "developer": "Minimax AI", + "context_length": 64000, + "output_max": 2048, + "input_usd_1m": 0.39, + "output_usd_1m": 1.56, + "is_hottest": false + }, + { + "id": "minimax/m2-1", + "name": "MiniMax M2.1", + "developer": "Minimax AI", + "context_length": 204000, + "output_max": 204000, + "input_usd_1m": 0.39, + "output_usd_1m": 1.56, + "is_hottest": false + }, + { + "id": "minimax/m2-1-highspeed", + "name": "MiniMax M2.1 Highspeed", + "developer": "Minimax AI", + "context_length": 204800, + "output_max": 204800, + "input_usd_1m": 0.78, + "output_usd_1m": 3.12, + "is_hottest": false + }, + { + "id": "minimax/m2-5-20260218", + "name": "MiniMax M2.5", + "developer": "Minimax AI", + "context_length": 204800, + "output_max": 204800, + "input_usd_1m": 0.39, + "output_usd_1m": 1.56, + "is_hottest": false + }, + { + "id": "minimax/m2-7-20260402", + "name": "MiniMax M2.7", + "developer": "Minimax AI", + "context_length": 204800, + "output_max": 204800, + "input_usd_1m": 0.39, + "output_usd_1m": 1.56, + "is_hottest": false + }, + { + "id": "minimax/m2-7-highspeed", + "name": "MiniMax M2.7 Highspeed", + "developer": "Minimax AI", + "context_length": 204800, + "output_max": 204800, + "input_usd_1m": 0.78, + "output_usd_1m": 3.12, + "is_hottest": false + }, + { + "id": "minimax/m2-5-highspeed-20260218", + "name": "MiniMax M2.5 Highspeed", + "developer": "Minimax AI", + "context_length": 204800, + "output_max": 204800, + "input_usd_1m": 0.78, + "output_usd_1m": 3.12, + "is_hottest": false + }, + { + "id": "moonshot/kimi-k2-5", + "name": "Kimi K2.5", + "developer": "Moonshot", + "context_length": 262144, + "output_max": 131000, + "input_usd_1m": 0.78, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "moonshot/kimi-k2-6", + "name": "Kimi K2.6", + "developer": "Moonshot", + "context_length": 262000, + "output_max": 260000, + "input_usd_1m": 1.235, + "output_usd_1m": 5.2, + "is_hottest": false + }, + { + "id": "moonshot/kimi-k2-7-code", + "name": "Kimi K2.7 Code", + "developer": "Moonshot", + "context_length": 262144, + "output_max": 260000, + "input_usd_1m": 1.235, + "output_usd_1m": 5.2, + "is_hottest": false + }, + { + "id": "moonshot/kimi-k2-7-code-highspeed", + "name": "Kimi K2.7 Code Highspeed", + "developer": "Moonshot", + "context_length": 262144, + "output_max": 260000, + "input_usd_1m": 2.47, + "output_usd_1m": 10.4, + "is_hottest": false + }, + { + "id": "anthracite-org/magnum-v4-72b", + "name": "Magnum v4 72B", + "developer": "Anthracite", + "context_length": 32000, + "output_max": 16384, + "input_usd_1m": 3.9, + "output_usd_1m": 6.5, + "is_hottest": false + }, + { + "id": "gryphe/mythomax-l2-13b", + "name": "MythoMax 13B", + "developer": "Gryphe", + "context_length": 4096, + "output_max": 4096, + "input_usd_1m": 2.4375, + "output_usd_1m": 2.4375, + "is_hottest": false + }, + { + "id": "baidu/ernie-4.5-vl-424b-a47b", + "name": "Ernie 4.5 VL 424B A47B", + "developer": "Baidu", + "context_length": 123000, + "output_max": 123000, + "input_usd_1m": 0.5577, + "output_usd_1m": 1.677, + "is_hottest": false + }, + { + "id": "baidu/ernie-5.0", + "name": "ERNIE 5.0", + "developer": "Baidu", + "context_length": 128000, + "output_max": 65536, + "input_usd_1m": 1.82, + "output_usd_1m": 7.28, + "is_hottest": false + }, + { + "id": "nvidia/nemotron-3-nano-30b-a3b", + "name": "Nemotron 3 Nano 30B A3B", + "developer": "Nvidia", + "context_length": 262144, + "output_max": 228000, + "input_usd_1m": 0.065, + "output_usd_1m": 0.26, + "is_hottest": false + }, + { + "id": "nvidia/nemotron-3-super-120b-a12b", + "name": "Nemotron 3 Super 120B A12B", + "developer": "Nvidia", + "context_length": 262144, + "output_max": 262144, + "input_usd_1m": 0.117, + "output_usd_1m": 0.585, + "is_hottest": false + }, + { + "id": "nvidia/nemotron-3-ultra-550b-a55b", + "name": "Nemotron 3 Ultra 550B A55B", + "developer": "Nvidia", + "context_length": 1000000, + "output_max": 32768, + "input_usd_1m": 0.65, + "output_usd_1m": 3.25, + "is_hottest": false + }, + { + "id": "nousresearch/hermes-4-405b", + "name": "Hermes 4 405B", + "developer": "NousResearch", + "context_length": 131072, + "output_max": 16000, + "input_usd_1m": 1.3, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "nousresearch/hermes-4-70b", + "name": "Hermes 4 70B", + "developer": "NousResearch", + "context_length": 131072, + "output_max": null, + "input_usd_1m": 0.169, + "output_usd_1m": 0.52, + "is_hottest": false + }, + { + "id": "nousresearch/hermes-3-llama-3.1-70b", + "name": "Hermes 3 70B Instruct", + "developer": "NousResearch", + "context_length": 131072, + "output_max": 16384, + "input_usd_1m": 0.91, + "output_usd_1m": 0.91, + "is_hottest": false + }, + { + "id": "nousresearch/hermes-3-llama-3.1-405b", + "name": "Hermes 3 405B Instruct", + "developer": "NousResearch", + "context_length": 131072, + "output_max": 16384, + "input_usd_1m": 1.3, + "output_usd_1m": 1.3, + "is_hottest": false + }, + { + "id": "cohere/command-a", + "name": "Command A", + "developer": "Cohere", + "context_length": 256000, + "output_max": 256000, + "input_usd_1m": 3.25, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "cohere/command-r7b-12-2024", + "name": "Command R7B (12-2024)", + "developer": "Cohere", + "context_length": 128000, + "output_max": 4000, + "input_usd_1m": 0.04875, + "output_usd_1m": 0.195, + "is_hottest": false + }, + { + "id": "cohere/command-r-08-2024", + "name": "Command R (08-2024)", + "developer": "Cohere", + "context_length": 128000, + "output_max": 4000, + "input_usd_1m": 0.195, + "output_usd_1m": 0.78, + "is_hottest": false + }, + { + "id": "cohere/command-r-plus-08-2024", + "name": "Command R+ (08-2024)", + "developer": "Cohere", + "context_length": 128000, + "output_max": 4000, + "input_usd_1m": 3.25, + "output_usd_1m": 13.0, + "is_hottest": false + }, + { + "id": "perplexity/sonar", + "name": "Sonar", + "developer": "Perplexity", + "context_length": 128000, + "output_max": 100000, + "input_usd_1m": 1.3, + "output_usd_1m": 1.3, + "is_hottest": false + }, + { + "id": "perplexity/sonar-pro", + "name": "Sonar Pro", + "developer": "Perplexity", + "context_length": 200000, + "output_max": 100000, + "input_usd_1m": 3.9, + "output_usd_1m": 19.5, + "is_hottest": false + }, + { + "id": "x-ai/grok-3-beta", + "name": "Grok 3 Beta", + "developer": "X AI", + "context_length": 131000, + "output_max": 130000, + "input_usd_1m": 3.9, + "output_usd_1m": 19.5, + "is_hottest": false + }, + { + "id": "x-ai/grok-3-mini-beta", + "name": "Grok 3 Beta Mini", + "developer": "X AI", + "context_length": 131000, + "output_max": 130000, + "input_usd_1m": 0.39, + "output_usd_1m": 0.65, + "is_hottest": false + }, + { + "id": "x-ai/grok-4-fast-reasoning", + "name": "Grok 4 Fast Reasoning", + "developer": "X AI", + "context_length": 2000000, + "output_max": 1999000, + "input_usd_1m": 0.26, + "output_usd_1m": 0.65, + "is_hottest": false + }, + { + "id": "x-ai/grok-4-fast-non-reasoning", + "name": "Grok 4 Fast Non-Reasoning", + "developer": "X AI", + "context_length": 2000000, + "output_max": 1999000, + "input_usd_1m": 0.26, + "output_usd_1m": 0.65, + "is_hottest": false + }, + { + "id": "x-ai/grok-4-1-fast-reasoning", + "name": "Grok 4.1 Fast Reasoning", + "developer": "X AI", + "context_length": 2000000, + "output_max": 1999000, + "input_usd_1m": 0.26, + "output_usd_1m": 0.65, + "is_hottest": false + }, + { + "id": "x-ai/grok-4-1-fast-non-reasoning", + "name": "Grok 4.1 Fast Non-Reasoning", + "developer": "X AI", + "context_length": 2000000, + "output_max": 1999000, + "input_usd_1m": 0.26, + "output_usd_1m": 0.65, + "is_hottest": false + }, + { + "id": "x-ai/grok-4-3", + "name": "Grok 4.3", + "developer": "X AI", + "context_length": 1000000, + "output_max": 1000000, + "input_usd_1m": 1.625, + "output_usd_1m": 3.25, + "is_hottest": false + }, + { + "id": "x-ai/grok-4-20-0309-reasoning", + "name": "Grok 4.20 Reasoning", + "developer": "X AI", + "context_length": 2000000, + "output_max": 1999000, + "input_usd_1m": 2.6, + "output_usd_1m": 7.8, + "is_hottest": false + }, + { + "id": "x-ai/grok-4-20-0309-non-reasoning", + "name": "Grok 4.20 Non-Reasoning", + "developer": "X AI", + "context_length": 2000000, + "output_max": 1999000, + "input_usd_1m": 2.6, + "output_usd_1m": 7.8, + "is_hottest": false + }, + { + "id": "x-ai/grok-code-fast-1", + "name": "Grok Code Fast 1", + "developer": "X AI", + "context_length": 256000, + "output_max": 255000, + "input_usd_1m": 0.26, + "output_usd_1m": 1.95, + "is_hottest": false + }, + { + "id": "x-ai/grok-build-0-1", + "name": "Grok Build 0.1", + "developer": "X AI", + "context_length": 0, + "output_max": null, + "input_usd_1m": 1.3, + "output_usd_1m": 2.6, + "is_hottest": false + }, + { + "id": "mistral/labs-leanstral-1-5", + "name": "Leanstral 1.5", + "developer": "Mistral AI", + "context_length": 256000, + "output_max": null, + "input_usd_1m": 0.0, + "output_usd_1m": 0.0, + "is_hottest": false + }, + { + "id": "mistral/leanstral-1-5", + "name": "Leanstral 1.5", + "developer": "Mistral AI", + "context_length": 256000, + "output_max": null, + "input_usd_1m": 0.0, + "output_usd_1m": 0.0, + "is_hottest": false + }, + { + "id": "xiaomi/mimo-v2.5", + "name": "MiMo V2.5", + "developer": "Xiaomi", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 0.52, + "output_usd_1m": 2.6, + "is_hottest": false + }, + { + "id": "xiaomi/mimo-v2.5-pro", + "name": "MiMo V2.5 Pro", + "developer": "Xiaomi", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 1.3, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "stepfun/step-3.7-flash", + "name": "Step 3.7 Flash", + "developer": "StepFun", + "context_length": 256000, + "output_max": 256000, + "input_usd_1m": 0.26, + "output_usd_1m": 1.495, + "is_hottest": false + }, + { + "id": "sakana/fugu-ultra", + "name": "Fugu Ultra", + "developer": "Sakana AI", + "context_length": 1000000, + "output_max": 128000, + "input_usd_1m": 6.5, + "output_usd_1m": 39.0, + "is_hottest": false + }, + { + "id": "tencent/hy3", + "name": "Hy3", + "developer": "Tencent", + "context_length": 262144, + "output_max": 131072, + "input_usd_1m": 0.26, + "output_usd_1m": 1.04, + "is_hottest": false + }, + { + "id": "ai21/jamba-large-1.7", + "name": "Jamba Large 1.7", + "developer": "AI21", + "context_length": 256000, + "output_max": 4096, + "input_usd_1m": 2.6, + "output_usd_1m": 10.4, + "is_hottest": false + }, + { + "id": "aion-labs/aion-3.0-mini", + "name": "Aion-3.0-Mini", + "developer": "AionLabs", + "context_length": 131072, + "output_max": 32768, + "input_usd_1m": 0.91, + "output_usd_1m": 1.82, + "is_hottest": false + }, + { + "id": "aion-labs/aion-2.0", + "name": "Aion-2.0", + "developer": "AionLabs", + "context_length": 131072, + "output_max": 32768, + "input_usd_1m": 1.04, + "output_usd_1m": 2.08, + "is_hottest": false + }, + { + "id": "aion-labs/aion-3.0", + "name": "Aion-3.0", + "developer": "AionLabs", + "context_length": 131072, + "output_max": 32768, + "input_usd_1m": 3.9, + "output_usd_1m": 7.8, + "is_hottest": false + }, + { + "id": "aion-labs/aion-rp-llama-3.1-8b", + "name": "Aion-RP 1.0 (8B)", + "developer": "AionLabs", + "context_length": 32768, + "output_max": 32768, + "input_usd_1m": 1.04, + "output_usd_1m": 2.08, + "is_hottest": false + }, + { + "id": "allenai/olmo-3-32b-think", + "name": "Olmo 3 32B Think", + "developer": "AllenAI", + "context_length": 65536, + "output_max": 65536, + "input_usd_1m": 0.195, + "output_usd_1m": 0.65, + "is_hottest": false + }, + { + "id": "amazon/nova-2-lite-v1", + "name": "Nova 2 Lite", + "developer": "Amazon", + "context_length": 1000000, + "output_max": 65535, + "input_usd_1m": 0.39, + "output_usd_1m": 3.25, + "is_hottest": false + }, + { + "id": "amazon/nova-premier-v1", + "name": "Nova Premier 1.0", + "developer": "Amazon", + "context_length": 1000000, + "output_max": 32000, + "input_usd_1m": 3.25, + "output_usd_1m": 16.25, + "is_hottest": false + }, + { + "id": "amazon/nova-lite-v1", + "name": "Nova Lite 1.0", + "developer": "Amazon", + "context_length": 300000, + "output_max": 5120, + "input_usd_1m": 0.078, + "output_usd_1m": 0.312, + "is_hottest": false + }, + { + "id": "amazon/nova-micro-v1", + "name": "Nova Micro 1.0", + "developer": "Amazon", + "context_length": 128000, + "output_max": 5120, + "input_usd_1m": 0.0455, + "output_usd_1m": 0.182, + "is_hottest": false + }, + { + "id": "amazon/nova-pro-v1", + "name": "Nova Pro 1.0", + "developer": "Amazon", + "context_length": 300000, + "output_max": 5120, + "input_usd_1m": 1.04, + "output_usd_1m": 4.16, + "is_hottest": false + }, + { + "id": "arcee-ai/trinity-large-thinking", + "name": "Trinity Large Thinking", + "developer": "Arcee AI", + "context_length": 262144, + "output_max": 80000, + "input_usd_1m": 0.325, + "output_usd_1m": 1.04, + "is_hottest": false + }, + { + "id": "arcee-ai/virtuoso-large", + "name": "Virtuoso Large", + "developer": "Arcee AI", + "context_length": 131072, + "output_max": 64000, + "input_usd_1m": 0.975, + "output_usd_1m": 1.56, + "is_hottest": false + }, + { + "id": "bytedance-seed/seed-2.0-lite", + "name": "Seed-2.0-Lite", + "developer": "ByteDance", + "context_length": 262144, + "output_max": 131072, + "input_usd_1m": 0.325, + "output_usd_1m": 2.6, + "is_hottest": false + }, + { + "id": "bytedance-seed/seed-2.0-mini", + "name": "Seed-2.0-Mini", + "developer": "ByteDance", + "context_length": 262144, + "output_max": 131072, + "input_usd_1m": 0.13, + "output_usd_1m": 0.52, + "is_hottest": false + }, + { + "id": "bytedance-seed/seed-1.6-flash", + "name": "Seed 1.6 Flash", + "developer": "ByteDance", + "context_length": 262144, + "output_max": 32768, + "input_usd_1m": 0.0975, + "output_usd_1m": 0.39, + "is_hottest": false + }, + { + "id": "bytedance-seed/seed-1.6", + "name": "Seed 1.6", + "developer": "ByteDance", + "context_length": 262144, + "output_max": 32768, + "input_usd_1m": 0.325, + "output_usd_1m": 2.6, + "is_hottest": false + }, + { + "id": "cognitivecomputations/dolphin-mistral-24b-venice-edition", + "name": "Venice Uncensored", + "developer": "Venice", + "context_length": 128000, + "output_max": null, + "input_usd_1m": 0.26, + "output_usd_1m": 1.17, + "is_hottest": false + }, + { + "id": "deepcogito/cogito-v2.1-671b", + "name": "Cogito v2.1 671B", + "developer": "Deep Cogito", + "context_length": 128000, + "output_max": null, + "input_usd_1m": 1.625, + "output_usd_1m": 1.625, + "is_hottest": false + }, + { + "id": "ibm-granite/granite-4.1-8b", + "name": "Granite 4.1 8B", + "developer": "IBM", + "context_length": 131072, + "output_max": 131072, + "input_usd_1m": 0.065, + "output_usd_1m": 0.13, + "is_hottest": false + }, + { + "id": "ibm-granite/granite-4.0-h-micro", + "name": "Granite 4.0 Micro", + "developer": "IBM", + "context_length": 131000, + "output_max": 131000, + "input_usd_1m": 0.0221, + "output_usd_1m": 0.1456, + "is_hottest": false + }, + { + "id": "inception/mercury-2", + "name": "Mercury 2", + "developer": "Inception", + "context_length": 128000, + "output_max": 50000, + "input_usd_1m": 0.325, + "output_usd_1m": 0.975, + "is_hottest": false + }, + { + "id": "inclusionai/ring-2.6-1t", + "name": "Ring-2.6-1T", + "developer": "inclusionAI", + "context_length": 262144, + "output_max": 65536, + "input_usd_1m": 0.0975, + "output_usd_1m": 0.8125, + "is_hottest": false + }, + { + "id": "inclusionai/ling-2.6-1t", + "name": "Ling-2.6-1T", + "developer": "inclusionAI", + "context_length": 262144, + "output_max": 32768, + "input_usd_1m": 0.0975, + "output_usd_1m": 0.8125, + "is_hottest": false + }, + { + "id": "inclusionai/ling-2.6-flash", + "name": "Ling-2.6-flash", + "developer": "inclusionAI", + "context_length": 262144, + "output_max": 32768, + "input_usd_1m": 0.013, + "output_usd_1m": 0.039, + "is_hottest": false + }, + { + "id": "inclusionai/ling-3.0-flash", + "name": "Ling-3.0-flash", + "developer": "inclusionAI", + "context_length": 262144, + "output_max": 32768, + "input_usd_1m": 0.0, + "output_usd_1m": 0.0, + "is_hottest": false + }, + { + "id": "kwaipilot/kat-coder-pro-v2", + "name": "KAT-Coder-Pro V2", + "developer": "Kwaipilot", + "context_length": 256000, + "output_max": 80000, + "input_usd_1m": 0.39, + "output_usd_1m": 1.56, + "is_hottest": false + }, + { + "id": "kwaipilot/kat-coder-pro-v2.5", + "name": "KAT-Coder-Pro V2.5", + "developer": "Kwaipilot", + "context_length": 256000, + "output_max": null, + "input_usd_1m": 0.962, + "output_usd_1m": 3.848, + "is_hottest": false + }, + { + "id": "kwaipilot/kat-coder-air-v2.5", + "name": "KAT-Coder-Air V2.5", + "developer": "Kwaipilot", + "context_length": 256000, + "output_max": null, + "input_usd_1m": 0.195, + "output_usd_1m": 0.78, + "is_hottest": false + }, + { + "id": "mancer/weaver", + "name": "Weaver (alpha)", + "developer": "Mancer", + "context_length": 8000, + "output_max": 2000, + "input_usd_1m": 0.975, + "output_usd_1m": 1.3, + "is_hottest": false + }, + { + "id": "moonshotai/kimi-latest", + "name": "Kimi Latest", + "developer": "Moonshot", + "context_length": 262144, + "output_max": 262144, + "input_usd_1m": 0.858, + "output_usd_1m": 4.433, + "is_hottest": false + }, + { + "id": "moonshotai/kimi-k2-thinking", + "name": "Kimi K2 Thinking", + "developer": "Moonshot", + "context_length": 262144, + "output_max": 100352, + "input_usd_1m": 0.78, + "output_usd_1m": 3.25, + "is_hottest": false + }, + { + "id": "moonshotai/kimi-k2-0905", + "name": "Kimi K2 0905", + "developer": "Moonshot", + "context_length": 262144, + "output_max": 100352, + "input_usd_1m": 1.3, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "moonshotai/kimi-k2", + "name": "Kimi K2 0711", + "developer": "Moonshot", + "context_length": 131072, + "output_max": 100352, + "input_usd_1m": 0.741, + "output_usd_1m": 2.99, + "is_hottest": false + }, + { + "id": "morph/morph-v3-large", + "name": "V3 Large", + "developer": "Morph", + "context_length": 262144, + "output_max": 131072, + "input_usd_1m": 1.17, + "output_usd_1m": 2.47, + "is_hottest": false + }, + { + "id": "morph/morph-v3-fast", + "name": "V3 Fast", + "developer": "Morph", + "context_length": 81920, + "output_max": 38000, + "input_usd_1m": 1.04, + "output_usd_1m": 1.56, + "is_hottest": false + }, + { + "id": "nex-agi/nex-n2-mini", + "name": "Nex-N2-Mini", + "developer": "Nex AGI", + "context_length": 262144, + "output_max": 262144, + "input_usd_1m": 0.0325, + "output_usd_1m": 0.13, + "is_hottest": false + }, + { + "id": "nex-agi/nex-n2-pro", + "name": "Nex-N2-Pro", + "developer": "Nex AGI", + "context_length": 262144, + "output_max": 262144, + "input_usd_1m": 0.65, + "output_usd_1m": 3.25, + "is_hottest": false + }, + { + "id": "perceptron/perceptron-mk1", + "name": "Mk1", + "developer": "Perceptron", + "context_length": 32768, + "output_max": 8192, + "input_usd_1m": 0.195, + "output_usd_1m": 1.95, + "is_hottest": false + }, + { + "id": "poolside/laguna-xs-2.1", + "name": "Laguna XS 2.1", + "developer": "Poolside", + "context_length": 262144, + "output_max": 32768, + "input_usd_1m": 0.078, + "output_usd_1m": 0.156, + "is_hottest": false + }, + { + "id": "poolside/laguna-s-2.1", + "name": "Laguna S 2.1", + "developer": "Poolside", + "context_length": 1048576, + "output_max": 131072, + "input_usd_1m": 0.078, + "output_usd_1m": 0.156, + "is_hottest": false + }, + { + "id": "rekaai/reka-edge", + "name": "Reka Edge", + "developer": "RekaAI", + "context_length": 16384, + "output_max": 16384, + "input_usd_1m": 0.13, + "output_usd_1m": 0.13, + "is_hottest": false + }, + { + "id": "rekaai/reka-flash-3", + "name": "Reka Flash 3", + "developer": "RekaAI", + "context_length": 65536, + "output_max": 65536, + "input_usd_1m": 0.13, + "output_usd_1m": 0.26, + "is_hottest": false + }, + { + "id": "relace/relace-search", + "name": "Search", + "developer": "Relace", + "context_length": 256000, + "output_max": 128000, + "input_usd_1m": 1.3, + "output_usd_1m": 3.9, + "is_hottest": false + }, + { + "id": "relace/relace-apply-3", + "name": "Apply 3", + "developer": "Relace", + "context_length": 256000, + "output_max": 128000, + "input_usd_1m": 1.105, + "output_usd_1m": 1.625, + "is_hottest": false + }, + { + "id": "sao10k/l3.3-euryale-70b", + "name": "Llama 3.3 Euryale 70B", + "developer": "Sao10K", + "context_length": 131072, + "output_max": 16384, + "input_usd_1m": 0.845, + "output_usd_1m": 0.975, + "is_hottest": false + }, + { + "id": "sao10k/l3.1-euryale-70b", + "name": "Llama 3.1 Euryale 70B v2.2", + "developer": "Sao10K", + "context_length": 131072, + "output_max": 16384, + "input_usd_1m": 1.88552, + "output_usd_1m": 1.88552, + "is_hottest": false + }, + { + "id": "sao10k/l3-lunaris-8b", + "name": "Llama 3 8B Lunaris", + "developer": "Sao10K", + "context_length": 8192, + "output_max": 16384, + "input_usd_1m": 0.065, + "output_usd_1m": 0.065, + "is_hottest": false + }, + { + "id": "thedrummer/cydonia-24b-v4.1", + "name": "Cydonia 24B V4.1", + "developer": "TheDrummer", + "context_length": 131072, + "output_max": 131072, + "input_usd_1m": 0.39, + "output_usd_1m": 0.65, + "is_hottest": false + }, + { + "id": "thedrummer/skyfall-36b-v2", + "name": "Skyfall 36B V2", + "developer": "TheDrummer", + "context_length": 32768, + "output_max": 32768, + "input_usd_1m": 0.715, + "output_usd_1m": 1.04, + "is_hottest": false + }, + { + "id": "thedrummer/unslopnemo-12b", + "name": "UnslopNemo 12B", + "developer": "TheDrummer", + "context_length": 32768, + "output_max": 32768, + "input_usd_1m": 0.52, + "output_usd_1m": 0.52, + "is_hottest": false + }, + { + "id": "thedrummer/rocinante-12b", + "name": "Rocinante 12B", + "developer": "TheDrummer", + "context_length": 32768, + "output_max": null, + "input_usd_1m": 0.325, + "output_usd_1m": 0.65, + "is_hottest": false + }, + { + "id": "thinkingmachines/inkling", + "name": "Inkling", + "developer": "Thinking Machines", + "context_length": 1048576, + "output_max": null, + "input_usd_1m": 1.3, + "output_usd_1m": 5.265, + "is_hottest": false + }, + { + "id": "thinkingmachines/inkling-small", + "name": "Inkling Small", + "developer": "Thinking Machines", + "context_length": 524288, + "output_max": 262144, + "input_usd_1m": 0.754, + "output_usd_1m": 1.872, + "is_hottest": false + }, + { + "id": "undi95/remm-slerp-l2-13b", + "name": "ReMM SLERP 13B", + "developer": "Undi95", + "context_length": 6144, + "output_max": 4096, + "input_usd_1m": 0.65, + "output_usd_1m": 0.975, + "is_hottest": false + }, + { + "id": "upstage/solar-pro-3", + "name": "Solar Pro 3", + "developer": "Upstage", + "context_length": 128000, + "output_max": null, + "input_usd_1m": 0.195, + "output_usd_1m": 0.78, + "is_hottest": false + }, + { + "id": "writer/palmyra-x5", + "name": "Palmyra X5", + "developer": "Writer", + "context_length": 1040000, + "output_max": 8192, + "input_usd_1m": 0.78, + "output_usd_1m": 7.8, + "is_hottest": false + }, + { + "id": "z-ai/glm-5-turbo", + "name": "GLM 5 Turbo", + "developer": "Zhipu AI", + "context_length": 262144, + "output_max": 131072, + "input_usd_1m": 1.56, + "output_usd_1m": 5.2, + "is_hottest": false + }, + { + "id": "z-ai/glm-5v-turbo", + "name": "GLM 5V Turbo", + "developer": "Zhipu AI", + "context_length": 202752, + "output_max": 131072, + "input_usd_1m": 1.56, + "output_usd_1m": 5.2, + "is_hottest": false + }, + { + "id": "z-ai/glm-4.7-flash", + "name": "GLM 4.7 Flash", + "developer": "Zhipu AI", + "context_length": 202752, + "output_max": 16384, + "input_usd_1m": 0.1625, + "output_usd_1m": 0.65, + "is_hottest": false + }, + { + "id": "z-ai/glm-4.6v", + "name": "GLM 4.6V", + "developer": "Zhipu AI", + "context_length": 131072, + "output_max": 32768, + "input_usd_1m": 0.39, + "output_usd_1m": 1.17, + "is_hottest": false + }, + { + "id": "z-ai/glm-4.5v", + "name": "GLM 4.5V", + "developer": "Zhipu AI", + "context_length": 65536, + "output_max": 16384, + "input_usd_1m": 0.78, + "output_usd_1m": 2.34, + "is_hottest": false + } + ] +} \ No newline at end of file diff --git a/autogpt_platform/backend/backend/data/llm_registry/llm_models.py b/autogpt_platform/backend/backend/data/llm_registry/llm_models.py index b77f1dd8d232..9b257598a926 100644 --- a/autogpt_platform/backend/backend/data/llm_registry/llm_models.py +++ b/autogpt_platform/backend/backend/data/llm_registry/llm_models.py @@ -14,6 +14,7 @@ from enum import Enum, EnumMeta from typing import Literal, NamedTuple +from backend.data.llm_registry.aiml_catalog import load_aiml_catalog, member_name from backend.data.llm_registry.catalog import get_catalog from backend.data.llm_registry.catalog_model import CatalogPayload @@ -314,6 +315,78 @@ def max_output_tokens(self) -> int | None: } +# --------------------------------------------------------------------------- +# AIMLAPI aggregator models +# --------------------------------------------------------------------------- +# +# ``catalog.py`` appends the AIMLAPI catalog to the payload, but block schemas +# serialize ``LLMModel`` members, and ``_build_model_metadata()`` joins catalog +# models to members BY SLUG — so an injected catalog entry is invisible until a +# member carrying that slug exists. These two steps are the same registration +# split across the enum and the catalog; keep them in sync. + +# Per-token USD list prices for the injected models, keyed by member. +# ``block_cost_config`` derives the TOKEN_COST rates that actually bill from +# this; the catalog's flat ``run_credits`` is only the completeness-guard tier. +AIML_TOKEN_PRICING: dict["LLMModel", tuple[float, float]] = {} +# Members the aggregator flags as "hottest" — the picker sorts these first. +AIML_HOTTEST_MODELS: set["LLMModel"] = set() + + +def _add_llm_model_member(name: str, value: str) -> "LLMModel": + """Add a member to the already-created ``LLMModel`` enum. + + ``Enum`` has no public extension API, so this writes the same three + structures ``EnumMeta`` populates. Done once at import, before anything + reads the enum. + """ + # ``LLMModel`` mixes in ``str``, so the member must be constructed through + # ``str.__new__`` — ``object.__new__`` refuses a variable-length subclass. + member = str.__new__(LLMModel, value) + member._name_ = name + member._value_ = value + LLMModel._member_map_[name] = member + LLMModel._value2member_map_[value] = member + LLMModel._member_names_.append(name) + type.__setattr__(LLMModel, name, member) + return member + + +def _register_aiml_models() -> None: + pricing = {model.id: model for model in load_aiml_catalog()} + for model in get_catalog().models: + if model.provider != "aiml_api": + continue + # Skip slugs that already resolve — directly or through + # ``_missing_``/``_OPENROUTER_ALIASES``. An aggregator entry must never + # shadow a native model and reroute existing graphs through AIMLAPI. + try: + LLMModel(model.slug) + continue + except ValueError: + pass + + name = unique = member_name(model.slug) + suffix = 2 + while unique in LLMModel._member_map_: + unique = f"{name}_{suffix}" + suffix += 1 + member = _add_llm_model_member(unique, model.slug) + + source = pricing.get(model.slug) + if source is None: + continue + AIML_TOKEN_PRICING[member] = ( + source.input_usd_per_1m or 0.0, + source.output_usd_per_1m or 0.0, + ) + if source.is_hottest: + AIML_HOTTEST_MODELS.add(member) + + +_register_aiml_models() + + def _build_model_metadata() -> dict["LLMModel", ModelMetadata]: """Project catalog facts into the block-facing metadata shape. @@ -406,7 +479,28 @@ def _default_model_from_catalog() -> LLMModel: raise ValueError("catalog declares no enabled block-selectable models") -DEFAULT_LLM_MODEL = _default_model_from_catalog() +def _default_aiml_model() -> "LLMModel": + """Default to an AIMLAPI model — this build is the aggregator build. + + Prefers a flagship, falls back to the first injected AIMLAPI model (the + catalog is hottest-first), and finally to the catalog's own recommended + model so a missing or empty AIMLAPI catalog can never break import. + """ + for candidate in ("openai/gpt-5.6-terra-pro", "anthropic/claude-sonnet-5"): + try: + member = LLMModel(candidate) + except ValueError: + continue + if member in AIML_TOKEN_PRICING: + return member + for member in AIML_HOTTEST_MODELS: + return member + for member in AIML_TOKEN_PRICING: + return member + return _default_model_from_catalog() + + +DEFAULT_LLM_MODEL = _default_aiml_model() # Family-aware mapping for legacy model values that have been retired from the # `LLMModel` enum. Used by both the Prisma migration that rewrites stored graph diff --git a/autogpt_platform/backend/backend/data/llm_registry/llm_models_test.py b/autogpt_platform/backend/backend/data/llm_registry/llm_models_test.py index d5a740b68489..87173c539f4c 100644 --- a/autogpt_platform/backend/backend/data/llm_registry/llm_models_test.py +++ b/autogpt_platform/backend/backend/data/llm_registry/llm_models_test.py @@ -8,10 +8,18 @@ from backend.data.llm_registry.llm_models import DEFAULT_LLM_MODEL, LLMModel -def test_default_model_is_the_catalog_recommendation(): - """DEFAULT_LLM_MODEL derives from the catalog's is_recommended flag — - the same fact must not be encoded twice.""" - assert DEFAULT_LLM_MODEL is LLMModel.GPT5_6_TERRA +def test_default_model_is_served_through_the_aggregator(): + """This is the aggregator build: the default must be reachable with the + single AIMLAPI key, not with a native per-provider credential. + + Upstream derives DEFAULT_LLM_MODEL from the catalog's is_recommended + flag (LLMModel.GPT5_6_TERRA). Here _default_aiml_model() takes + precedence so a fresh install works with the one key the user actually + has; the catalog recommendation stays the fallback when the AIMLAPI + catalog is empty. + """ + assert DEFAULT_LLM_MODEL in llm_models.AIML_TOKEN_PRICING + assert llm_models.MODEL_METADATA[DEFAULT_LLM_MODEL].provider == "aiml_api" def _schema_metadata() -> dict: diff --git a/autogpt_platform/backend/backend/util/llm/providers.py b/autogpt_platform/backend/backend/util/llm/providers.py index 509131aa668e..fc5adb310236 100644 --- a/autogpt_platform/backend/backend/util/llm/providers.py +++ b/autogpt_platform/backend/backend/util/llm/providers.py @@ -34,6 +34,7 @@ import functools import json as json_module import logging +import os from datetime import datetime, timezone from typing import Any, Literal, cast @@ -383,7 +384,9 @@ async def _dispatch_sync( ) if provider == "aiml_api": return await _call_openai_compat( - base_url="https://api.aimlapi.com/v2", + base_url=os.getenv( + "AIMLAPI_INFERENCE_URL", "https://api.aimlapi.com/v1" + ), model=model, api_key=api_key, messages=messages, @@ -398,6 +401,8 @@ async def _dispatch_sync( "X-Project": "AutoGPT", "X-Title": "AutoGPT", "HTTP-Referer": "https://github.com/Significant-Gravitas/AutoGPT", + "X-AIMLAPI-Source": "agent/autogpt", + "X-AIMLAPI-Partner-ID": "part_T70zDIEvQLKSMzMQ7asjdtKR", }, ) if provider == "v0": diff --git a/autogpt_platform/backend/backend/util/llm/providers_test.py b/autogpt_platform/backend/backend/util/llm/providers_test.py index 5b59cfef4e21..0c8993e18d30 100644 --- a/autogpt_platform/backend/backend/util/llm/providers_test.py +++ b/autogpt_platform/backend/backend/util/llm/providers_test.py @@ -727,6 +727,14 @@ async def test_dispatches_with_default_headers(self): # AI/ML expects branded headers on the client construction. ctor_kwargs = constructor.call_args.kwargs assert ctor_kwargs["default_headers"]["X-Project"] == "AutoGPT" + # AI/ML API is served under /v1 (the /v2 host does not exist). + assert ctor_kwargs["base_url"] == "https://api.aimlapi.com/v1" + # Attribution headers identify agent-originated traffic to the provider. + assert ctor_kwargs["default_headers"]["X-AIMLAPI-Source"] == "agent/autogpt" + assert ( + ctor_kwargs["default_headers"]["X-AIMLAPI-Partner-ID"] + == "part_T70zDIEvQLKSMzMQ7asjdtKR" + ) # --------------------------------------------------------------------------- diff --git a/autogpt_platform/frontend/Dockerfile b/autogpt_platform/frontend/Dockerfile index 8bea573a828c..41dd73f64bb8 100644 --- a/autogpt_platform/frontend/Dockerfile +++ b/autogpt_platform/frontend/Dockerfile @@ -17,6 +17,22 @@ ARG NEXT_PUBLIC_FRONTEND_BASE_URL # Keep Docker builds defaulting to false to avoid the memory hit. ARG NEXT_PUBLIC_SOURCEMAPS="false" ENV NEXT_PUBLIC_SOURCEMAPS=$NEXT_PUBLIC_SOURCEMAPS +# Public origin baked into the client bundle (Next inlines NEXT_PUBLIC_* at +# build). The browser API client calls `${NEXT_PUBLIC_FRONTEND_BASE_URL}/api/proxy`, +# so a hosted deployment must build with its real origin, not localhost. +ARG NEXT_PUBLIC_FRONTEND_BASE_URL="http://localhost:3000" +ENV NEXT_PUBLIC_FRONTEND_BASE_URL=$NEXT_PUBLIC_FRONTEND_BASE_URL +# AIMLAPI inference base the browser hits directly to verify an entered API key +# (GET /billing/balance). Defaults to production; a staging deploy overrides it. +ARG NEXT_PUBLIC_AIMLAPI_API_URL="https://api.aimlapi.com/v1" +ENV NEXT_PUBLIC_AIMLAPI_API_URL=$NEXT_PUBLIC_AIMLAPI_API_URL +# Partner id sent as X-AIMLAPI-Partner-ID on that same browser-side call. The +# production id is valid on staging too, so this default fits both; it is only +# overridden for a staging-only test id. Declared here because Next inlines +# NEXT_PUBLIC_* at build — without the ARG the override could not reach the +# bundle at all. +ARG NEXT_PUBLIC_AIMLAPI_PARTNER_ID="part_T70zDIEvQLKSMzMQ7asjdtKR" +ENV NEXT_PUBLIC_AIMLAPI_PARTNER_ID=$NEXT_PUBLIC_AIMLAPI_PARTNER_ID ENV NODE_ENV="production" # Merge env files appropriately based on environment RUN if [ -f .env.production ]; then \ diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/Integration.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/Integration.tsx index 32e63b8183c6..39ab2699d1d8 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/Integration.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/Integration.tsx @@ -1,6 +1,13 @@ +import { Badge } from "@/components/atoms/Badge/Badge"; import { Button } from "@/components/__legacy__/ui/button"; import { Skeleton } from "@/components/__legacy__/ui/skeleton"; import { beautifyString, cn } from "@/lib/utils"; + +// Providers surfaced with an "aimlapi.com" label + "Recommended" badge in the +// builder integration list. +export const RECOMMENDED_PROVIDERS: Record = { + aiml_api: "aimlapi.com", +}; import Image from "next/image"; import React, { ButtonHTMLAttributes } from "react"; @@ -46,11 +53,20 @@ export const Integration: IntegrationComponent = ({
- {title && ( -

- {beautifyString(title)} -

- )} +
+ {title && ( +

+ {title && RECOMMENDED_PROVIDERS[title] + ? RECOMMENDED_PROVIDERS[title] + : beautifyString(title)} +

+ )} + {title && RECOMMENDED_PROVIDERS[title] ? ( + + Recommended + + ) : null} +
{number_of_blocks} diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/IntegrationBlocks/IntegrationBlocks.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/IntegrationBlocks/IntegrationBlocks.tsx index 1934b568fa09..50b869af28dc 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/IntegrationBlocks/IntegrationBlocks.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/NewControlPanel/NewBlockMenu/IntegrationBlocks/IntegrationBlocks.tsx @@ -1,11 +1,13 @@ import { Button } from "@/components/__legacy__/ui/button"; -import React, { Fragment } from "react"; +import React, { Fragment, useMemo } from "react"; import { IntegrationBlock } from "../IntergrationBlock"; import { Skeleton } from "@/components/__legacy__/ui/skeleton"; import { useIntegrationBlocks } from "./useIntegrationBlocks"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; import { InfiniteScroll } from "@/components/contextual/InfiniteScroll/InfiniteScroll"; import { useBlockMenuStore } from "../../../../stores/blockMenuStore"; +import { beautifyString } from "@/lib/utils"; +import { RECOMMENDED_PROVIDERS } from "../Integration"; export const IntegrationBlocks = () => { const { integration, setIntegration } = useBlockMenuStore(); @@ -21,6 +23,18 @@ export const IntegrationBlocks = () => { refetch, } = useIntegrationBlocks(); + // Stagehand blocks reach the aimlapi.com list only through the shared LLM + // credential (their models are OpenAI/Anthropic-only), so keep them after the + // model blocks that actually run on the aimlapi.com key. + const orderedBlocks = useMemo(() => { + if (integration !== "aiml_api") return allBlocks; + const stagehandRank = (name?: string) => + name?.toLowerCase().startsWith("stagehand") ? 1 : 0; + return [...allBlocks].sort( + (a, b) => stagehandRank(a.name) - stagehandRank(b.name), + ); + }, [allBlocks, integration]); + if (blocksLoading) { return (
@@ -78,7 +92,9 @@ export const IntegrationBlocks = () => { /

- {integration} + {integration && RECOMMENDED_PROVIDERS[integration] + ? RECOMMENDED_PROVIDERS[integration] + : beautifyString(integration ?? "")}

@@ -86,7 +102,7 @@ export const IntegrationBlocks = () => {
- {allBlocks.map((block) => ( + {orderedBlocks.map((block) => ( + form.setValue("apiKey", key, { shouldValidate: true, shouldDirty: true }), + ); + if (isLoading || !supportsApiKey) { return null; } @@ -52,7 +60,7 @@ export function APIKeyCredentialsModal({ }} onClose={onClose} styling={{ - maxWidth: "25rem", + maxWidth: isAimlapi ? "34rem" : "25rem", }} > @@ -81,8 +89,49 @@ export function APIKeyCredentialsModal({ ( - <> + render={({ field }) => + isAimlapi ? ( +
+ + API Key + +
+
+ +
+ or + +
+ {oauthStatus === "success" && oauthMessage ? ( +

+ {oauthMessage} +

+ ) : null} + {oauthStatus === "error" && oauthMessage ? ( +

+ {oauthMessage} +

+ ) : null} +
+ ) : ( - - )} + ) + } /> @@ -56,16 +60,61 @@ export function ApiKeyConnectForm({ render={({ field }) => ( - + {isAimlapi ? ( +
+ + API key + +
+
+ +
+ or + +
+ {oauthStatus === "success" && oauthMessage ? ( +

+ {oauthMessage} +

+ ) : null} + {oauthStatus === "error" && oauthMessage ? ( +

+ {oauthMessage} +

+ ) : null} +
+ ) : ( + + )}
diff --git a/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/components/ConnectServiceDialog/components/DetailView/useApiKeyConnectForm.ts b/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/components/ConnectServiceDialog/components/DetailView/useApiKeyConnectForm.ts index 349c85d28eef..e87a5e7a52ae 100644 --- a/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/components/ConnectServiceDialog/components/DetailView/useApiKeyConnectForm.ts +++ b/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/components/ConnectServiceDialog/components/DetailView/useApiKeyConnectForm.ts @@ -10,11 +10,16 @@ import { postV1CreateCredentials, } from "@/app/api/__generated__/endpoints/integrations/integrations"; import { toast } from "@/components/molecules/Toast/use-toast"; +import { + useAimlapiGetApiKey, + validateAimlapiApiKey, +} from "@/hooks/useAimlapiGetApiKey"; import { apiKeyConnectSchema, type ApiKeyConnectFormValues } from "./schema"; interface Args { provider: string; + defaultTitle?: string; onSuccess: () => void; } @@ -25,23 +30,36 @@ function toUnixSeconds(value: string | undefined): number | undefined { return Math.floor(ms / 1000); } -export function useApiKeyConnectForm({ provider, onSuccess }: Args) { +export function useApiKeyConnectForm({ provider, defaultTitle, onSuccess }: Args) { const queryClient = useQueryClient(); const [isPending, setIsPending] = useState(false); const form = useForm({ resolver: zodResolver(apiKeyConnectSchema), - defaultValues: { title: "", apiKey: "", expiresAt: "" }, + defaultValues: { title: defaultTitle ?? "", apiKey: "", expiresAt: "" }, mode: "onChange", }); + const { getApiKey, oauthStatus, oauthMessage } = useAimlapiGetApiKey((key) => + form.setValue("apiKey", key, { shouldValidate: true, shouldDirty: true }), + ); + async function handleSubmit(values: ApiKeyConnectFormValues) { setIsPending(true); try { + if (provider === "aiml_api" && !(await validateAimlapiApiKey(values.apiKey))) { + form.setError("apiKey", { + message: "This aimlapi.com API key is invalid.", + }); + toast({ + title: "Invalid API key", + description: "This aimlapi.com API key is invalid.", + variant: "destructive", + }); + return; + } + // customMutator throws on non-2xx, so reaching this line means success. - // Trust HTTP semantics rather than pinning to a specific 2xx code — - // proxies / future backend changes can swap 201 ↔ 200 without this - // breaking and silently failing in production. await postV1CreateCredentials(provider, { provider, type: "api_key", @@ -67,5 +85,12 @@ export function useApiKeyConnectForm({ provider, onSuccess }: Args) { } } - return { form, handleSubmit, isPending }; + return { + form, + handleSubmit, + isPending, + getApiKey, + oauthStatus, + oauthMessage, + }; } diff --git a/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/components/ConnectServiceDialog/components/ProviderRow.tsx b/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/components/ConnectServiceDialog/components/ProviderRow.tsx index 99fa1de3e027..8c3d8e322ced 100644 --- a/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/components/ConnectServiceDialog/components/ProviderRow.tsx +++ b/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/components/ConnectServiceDialog/components/ProviderRow.tsx @@ -2,6 +2,7 @@ import Image from "next/image"; import { useState } from "react"; +import { Badge } from "@/components/atoms/Badge/Badge"; import type { ConnectableProvider } from "../helpers"; import { PlusSignIcon } from "@hugeicons/core-free-icons"; import { Icon } from "@/components/atoms/Icon/Icon"; @@ -41,8 +42,15 @@ export function ProviderRow({ provider, onSelect }: Props) { /> )} - - {provider.name} + + + {provider.name} + + {provider.recommended ? ( + + Recommended + + ) : null} {provider.description ?? provider.id} diff --git a/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/components/ConnectServiceDialog/helpers.ts b/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/components/ConnectServiceDialog/helpers.ts index f1c3d4227383..8a1cc239537f 100644 --- a/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/components/ConnectServiceDialog/helpers.ts +++ b/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/components/ConnectServiceDialog/helpers.ts @@ -7,11 +7,15 @@ export type AuthMethod = (typeof AuthType)[keyof typeof AuthType]; export { AuthType }; +// Providers surfaced first in the "Connect a service" list, marked "Recommended". +const RECOMMENDED_PROVIDERS: ReadonlySet = new Set(["aiml_api"]); + export interface ConnectableProvider { id: string; name: string; description?: string | null; supportedAuthTypes: AuthMethod[]; + recommended?: boolean; authProviderByType?: Partial>; searchTerms?: string[]; } @@ -44,6 +48,9 @@ export function toConnectableProviders( name: formatProviderName(displayProvider), description: item.description, supportedAuthTypes: [], + // Spread rather than `recommended: false` so a non-recommended provider + // carries no extra key — the shape stays exactly what callers assert on. + ...(RECOMMENDED_PROVIDERS.has(displayProvider) ? { recommended: true } : {}), }; for (const authType of authTypes) { @@ -78,7 +85,11 @@ export function toConnectableProviders( } const result = Array.from(byDisplayProvider.values()); - result.sort((a, b) => a.name.localeCompare(b.name)); + // Recommended providers first, then alphabetical. + result.sort((a, b) => { + if (a.recommended !== b.recommended) return a.recommended ? -1 : 1; + return a.name.localeCompare(b.name); + }); return result; } diff --git a/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/helpers.ts b/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/helpers.ts index ca55d61bea5a..3708bf1c997a 100644 --- a/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/helpers.ts +++ b/autogpt_platform/frontend/src/components/contextual/IntegrationsPanel/helpers.ts @@ -33,6 +33,7 @@ export function typeBadgeLabel(type: CredentialType): string { } const PROVIDER_DISPLAY_NAME_OVERRIDES: Record = { + aiml_api: "aimlapi.com", github: "GitHub", google: "Google", google_maps: "Google Maps", diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/CredentialField/helpers.ts b/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/CredentialField/helpers.ts index 08a3ca0dc0a6..802fd60d9a28 100644 --- a/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/CredentialField/helpers.ts +++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/CredentialField/helpers.ts @@ -62,7 +62,7 @@ export const filterCredentialsByProvider = ( export function toDisplayName(provider: string): string { // Special cases that need manual handling const specialCases: Record = { - aiml_api: "AI/ML", + aiml_api: "aimlapi.com", d_id: "D-ID", e2b: "E2B", llama_api: "Llama API", diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/LlmModelField.tsx b/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/LlmModelField.tsx index b58c471f3627..f1cb86a9168c 100644 --- a/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/LlmModelField.tsx +++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/LlmModelField.tsx @@ -23,8 +23,12 @@ export function LlmModelField(props: FieldProps) { return (schema as LlmModelSchema)?.llm_model_metadata ?? {}; }, [schema]); + // Aggregator build: only surface AIMLAPI-served models so every model in the + // picker runs on the single aimlapi.com key (native provider entries hidden). const models = useMemo(() => { - return Object.values(metadata); + return Object.values(metadata).filter( + (model) => model.provider === "aiml_api", + ); }, [metadata]); const selectedName = diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/components/LlmIcon.tsx b/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/components/LlmIcon.tsx index cad91a3dde80..39d13e273106 100644 --- a/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/components/LlmIcon.tsx +++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/components/LlmIcon.tsx @@ -25,6 +25,15 @@ const creatorIconMap: Record = { nousresearch: "/integrations/nousresearch.avif", perplexity: "/integrations/perplexity.webp", qwen: "/integrations/qwen.png", + // AIMLAPI aggregator provider tag (shown in the model's provider sub-menu). + aiml_api: "/integrations/aiml_api.png", + aimlapicom: "/integrations/aiml_api.png", + // Extra creator aliases so aggregator models group under a real logo instead + // of a letter chip (map to already-bundled assets). + deepseekai: "/integrations/deepseek.png", + moonshot: "/integrations/moonshot.png", + alibaba: "/integrations/qwen.png", + alibabacloud: "/integrations/qwen.png", }; type Props = { diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/components/LlmModelPicker.tsx b/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/components/LlmModelPicker.tsx index 7286d85c6386..a59622eec9ad 100644 --- a/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/components/LlmModelPicker.tsx +++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/components/LlmModelPicker.tsx @@ -20,11 +20,15 @@ import { LlmIcon } from "./LlmIcon"; import { LlmMenuHeader } from "./LlmMenuHeader"; import { LlmMenuItem } from "./LlmMenuItem"; import { LlmPriceTier } from "./LlmPriceTier"; -import { ArrowDown01Icon } from "@hugeicons/core-free-icons"; +import { ArrowDown01Icon, StarIcon } from "@hugeicons/core-free-icons"; import { Icon } from "@/components/atoms/Icon/Icon"; type MenuView = "creator" | "model" | "provider"; +// Synthetic first group holding every hottest model, so it reads like the +// creator groups below it instead of a flat list. +const RECOMMENDED_GROUP = "Recommended"; + type Props = { models: LlmModelMetadata[]; selectedName?: string; @@ -49,6 +53,11 @@ export function LlmModelPicker({ const modelsByCreator = useMemo(() => groupByCreator(models), [models]); + const hottestModels = useMemo( + () => models.filter((model) => model.is_hottest), + [models], + ); + const creators = useMemo(() => { return Array.from(modelsByCreator.keys()).sort((a, b) => a.localeCompare(b), @@ -79,8 +88,11 @@ export function LlmModelPicker({ const currentCreator = activeCreator ?? creators[0] ?? null; const currentModels = useMemo(() => { + if (currentCreator === RECOMMENDED_GROUP) { + return hottestModels; + } return currentCreator ? (modelsByCreator.get(currentCreator) ?? []) : []; - }, [currentCreator, modelsByCreator]); + }, [currentCreator, modelsByCreator, hottestModels]); const currentCreatorIcon = useMemo(() => { return currentModels[0]?.creator ?? currentCreator; @@ -160,16 +172,23 @@ export function LlmModelPicker({ > {view === "creator" && (
- {recommendedModel && ( - <> - } - onClick={() => handleSelectModel(recommendedModel.name)} - /> -
- + {hottestModels.length > 0 && ( + + } + showChevron={true} + isActive={selectedModel?.is_hottest ?? false} + onClick={() => { + setActiveCreator(RECOMMENDED_GROUP); + setView("model"); + }} + /> )} {creators.map((creator) => ( } + icon={ + + } rightSlot={} showChevron={entry.providerCount > 1} isActive={ diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/types.ts b/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/types.ts index 39389b25350e..11a1fad8e091 100644 --- a/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/types.ts +++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/custom/LlmModelField/types.ts @@ -6,6 +6,7 @@ export type LlmModelMetadata = { provider_name: string; name: string; price_tier?: number; + is_hottest?: boolean; }; export type LlmModelMetadataMap = Record; diff --git a/autogpt_platform/frontend/src/hooks/useAimlapiGetApiKey.ts b/autogpt_platform/frontend/src/hooks/useAimlapiGetApiKey.ts new file mode 100644 index 000000000000..8552bc7b4252 --- /dev/null +++ b/autogpt_platform/frontend/src/hooks/useAimlapiGetApiKey.ts @@ -0,0 +1,128 @@ +"use client"; + +import { useRef, useState } from "react"; + +export type AimlapiOAuthStatus = "idle" | "authorizing" | "success" | "error"; + +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +const AIMLAPI_BASE_URL = + process.env.NEXT_PUBLIC_AIMLAPI_API_URL?.replace(/\/$/, "") || + "https://api.aimlapi.com/v1"; + +// Attribution pair, required on EVERY aimlapi.com request — not just sign-up. +// The balance check below runs in the browser, so it cannot inherit the +// backend client's default headers and has to carry them itself. The partner +// id is valid on both staging and production, so it ships compiled in; the env +// override exists only for a staging-only test id. Mirrors +// backend/api/features/aimlapi/config.py. +const AIMLAPI_SOURCE = "agent/autogpt"; +const AIMLAPI_PARTNER_ID = + process.env.NEXT_PUBLIC_AIMLAPI_PARTNER_ID || "part_T70zDIEvQLKSMzMQ7asjdtKR"; + +function attributionHeaders(): Record { + return { + "X-AIMLAPI-Source": AIMLAPI_SOURCE, + "X-AIMLAPI-Partner-ID": AIMLAPI_PARTNER_ID, + }; +} + +// Verify a manually-entered AIMLAPI key by calling the balance endpoint with it +// (CORS-enabled, so the browser can hit it directly — no backend needed). A bad +// key returns 401; anything else means the key authenticates. Returns false +// only when the key is definitively invalid; on a network error we can't tell, +// so return true (fail open) rather than block a legitimate save. +export async function validateAimlapiApiKey(apiKey: string): Promise { + try { + const res = await fetch(`${AIMLAPI_BASE_URL}/billing/balance`, { + headers: { + Authorization: `Bearer ${apiKey}`, + ...attributionHeaders(), + }, + }); + return res.status !== 401; + } catch { + return true; + } +} + +// The AIMLAPI "Get API key" flow lives behind the frontend's server proxy, +// which forwards `/api/proxy/` to the backend and injects auth. +async function postProxy(path: string, body: unknown): Promise { + const res = await fetch(`/api/proxy/${path}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body ?? {}), + }); + if (!res.ok) throw new Error(`Request failed (${res.status})`); + return (await res.json()) as T; +} + +// Device-authorization ("Get API key") grant for AIMLAPI: open a consent tab, +// poll the backend, and hand the issued key to `onKey`. Shared by the settings +// Connect-service form and the in-builder "Add new API key" modal. +export function useAimlapiGetApiKey(onKey: (apiKey: string) => void) { + const [oauthStatus, setOauthStatus] = useState("idle"); + const [oauthMessage, setOauthMessage] = useState(null); + const authorizingRef = useRef(false); + + async function getApiKey() { + if (authorizingRef.current) return; + authorizingRef.current = true; + setOauthStatus("authorizing"); + setOauthMessage(null); + + // Open the consent tab synchronously so pop-up blockers allow it, then + // point it at the verification URL once the backend returns one. Do NOT + // pass `noopener` here: with it, window.open() returns null and we lose the + // handle needed to redirect the tab — leaving a blank about:blank page. + const consentWindow = window.open("about:blank", "_blank"); + + try { + const start = await postProxy<{ + request_id: string; + verification_uri: string; + interval: number; + expires_in: number; + }>("api/aimlapi/authorize/start", {}); + + if (consentWindow) consentWindow.location.href = start.verification_uri; + else window.open(start.verification_uri, "_blank"); + + const intervalMs = Math.max(1, start.interval) * 1000; + const deadline = Date.now() + Math.max(1, start.expires_in) * 1000; + + while (Date.now() < deadline) { + await sleep(intervalMs); + const poll = await postProxy<{ status: string; api_key: string | null }>( + "api/aimlapi/authorize/poll", + { request_id: start.request_id }, + ); + if (poll.status === "ready" && poll.api_key) { + onKey(poll.api_key); + setOauthStatus("success"); + setOauthMessage( + "Your key has already been generated and added above.", + ); + return; + } + if (poll.status !== "pending" && poll.status !== "authorizing") { + throw new Error("Sign-in failed. Please try again."); + } + } + throw new Error("Sign-in timed out. Please try again."); + } catch (error) { + if (consentWindow && !consentWindow.closed) consentWindow.close(); + setOauthStatus("error"); + setOauthMessage( + error instanceof Error + ? error.message + : "Sign-in failed. Please try again.", + ); + } finally { + authorizingRef.current = false; + } + } + + return { getApiKey, oauthStatus, oauthMessage }; +} diff --git a/autogpt_platform/frontend/src/providers/agent-credentials/helper.ts b/autogpt_platform/frontend/src/providers/agent-credentials/helper.ts index dfe430037c51..30ecb4089257 100644 --- a/autogpt_platform/frontend/src/providers/agent-credentials/helper.ts +++ b/autogpt_platform/frontend/src/providers/agent-credentials/helper.ts @@ -5,7 +5,7 @@ export function toDisplayName(provider: unknown): string { // Special cases that need manual handling const specialCases: Record = { - aiml_api: "AI/ML", + aiml_api: "aimlapi.com", d_id: "D-ID", e2b: "E2B", llama_api: "Llama API",