diff --git a/.env.example b/.env.example index 44a6bf02..3ea9bcf2 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,7 @@ HEXIS_BIND_ADDRESS=127.0.0.1 # ANTHROPIC_API_KEY=replace-with-your-key # GEMINI_API_KEY=replace-with-your-key # XAI_API_KEY=replace-with-your-key +# ATLASCLOUD_API_KEY=replace-with-your-key # A server that does not authenticate still needs a non-secret value because # the OpenAI client requires one, for example: OPENAI_API_KEY=not-needed diff --git a/apps/hexis_api.py b/apps/hexis_api.py index ab9fcabf..669c922e 100644 --- a/apps/hexis_api.py +++ b/apps/hexis_api.py @@ -3350,6 +3350,7 @@ def _resolve_fallback_api_key(provider: str, role: str) -> str | None: # Then provider-specific conventional env vars. mapping = { "openai": "OPENAI_API_KEY", + "atlascloud": "ATLASCLOUD_API_KEY", "anthropic": "ANTHROPIC_API_KEY", "grok": "XAI_API_KEY", "gemini": "GEMINI_API_KEY", @@ -3537,7 +3538,7 @@ async def init_consent_request(req: InitConsentRequest): # Fail early if we need a key (unless mocked). if ( not use_mock_consent - and provider in {"openai", "anthropic", "grok", "gemini"} + and provider in {"openai", "atlascloud", "anthropic", "grok", "gemini"} and not api_key ): return JSONResponse({"error": "Missing API key"}, status_code=400) diff --git a/apps/hexis_init.py b/apps/hexis_init.py index bb735a0b..76779eae 100644 --- a/apps/hexis_init.py +++ b/apps/hexis_init.py @@ -41,6 +41,7 @@ _PROVIDER_ENV_VARS: dict[str, str] = { "anthropic": "ANTHROPIC_API_KEY", + "atlascloud": "ATLASCLOUD_API_KEY", "openai": "OPENAI_API_KEY", "openai_compatible": "OPENAI_API_KEY", "openai-codex": "", @@ -133,6 +134,9 @@ def _resolve_noninteractive_endpoint( """Return the endpoint and its visible source label.""" endpoint = (args.endpoint or "").strip() source = "--endpoint" if endpoint else "" + if not endpoint and provider == "atlascloud": + endpoint = "https://api.atlascloud.ai/v1" + source = "provider default" if not endpoint and provider == "openai_compatible": endpoint = os.getenv("OPENAI_BASE_URL", "").strip() source = "OPENAI_BASE_URL" if endpoint else "" @@ -793,6 +797,7 @@ async def _configure_llm(conn: Any, *, dsn: str, wait_seconds: int) -> dict[str, ("OpenAI Codex — ChatGPT Plus/Pro OAuth (no API key)", "openai-codex"), ("OpenAI — API key", "openai"), ("Anthropic — API key", "anthropic"), + ("Atlas Cloud — API key", "atlascloud"), ("Grok (xAI) — API key", "grok"), ("Gemini — API key", "gemini"), ("Chutes — OAuth", "chutes"), @@ -880,7 +885,13 @@ async def _configure_llm(conn: Any, *, dsn: str, wait_seconds: int) -> dict[str, "Endpoint (blank for provider default)", # Only OpenAI-shaped providers should inherit OPENAI_BASE_URL — don't # bleed it into Grok/Gemini/Anthropic (Bar #5). - default=os.getenv("OPENAI_BASE_URL", "") if provider in {"openai", "openai_compatible"} else "", + default=( + "https://api.atlascloud.ai/v1" + if provider == "atlascloud" + else os.getenv("OPENAI_BASE_URL", "") + if provider in {"openai", "openai_compatible"} + else "" + ), ) api_key_env = _prompt( "API key env var name (e.g. OPENAI_API_KEY)", diff --git a/apps/tui/init_screens.py b/apps/tui/init_screens.py index b7054d9a..eb91c19b 100644 --- a/apps/tui/init_screens.py +++ b/apps/tui/init_screens.py @@ -49,6 +49,7 @@ def _plain(text: str, token: str = "text") -> Text: _DEFAULT_MODELS: dict[str, str] = { "anthropic": "claude-sonnet-4-20250514", + "atlascloud": "deepseek-ai/deepseek-v4-pro", "openai": "gpt-4o", "openai-codex": "gpt-5.2", "grok": "grok-3", @@ -63,6 +64,7 @@ def _plain(text: str, token: str = "text") -> Text: _PROVIDER_ENV_VARS: dict[str, str] = { "anthropic": "ANTHROPIC_API_KEY", + "atlascloud": "ATLASCLOUD_API_KEY", "openai": "OPENAI_API_KEY", "openai-codex": "", "grok": "XAI_API_KEY", @@ -79,6 +81,7 @@ def _plain(text: str, token: str = "text") -> Text: ("OpenAI Codex (ChatGPT OAuth)", "openai-codex"), ("OpenAI Platform (API key)", "openai"), ("Anthropic (API key)", "anthropic"), + ("Atlas Cloud (API key)", "atlascloud"), ("Grok (xAI)", "grok"), ("Gemini", "gemini"), ("Chutes (OAuth)", "chutes"), @@ -173,6 +176,8 @@ class LLMConfigScreen(Screen): @staticmethod def _default_endpoint(provider: str) -> str: + if provider == "atlascloud": + return "https://api.atlascloud.ai/v1" if provider == "openai": return os.getenv("OPENAI_BASE_URL", "") return "" diff --git a/apps/tui/model_catalog.py b/apps/tui/model_catalog.py index 9bd4d850..061fb01e 100644 --- a/apps/tui/model_catalog.py +++ b/apps/tui/model_catalog.py @@ -4,8 +4,8 @@ auto-updating source over a hand-maintained list, treat it as advisory, and always allow a free-typed id. - * cloud providers → the models.dev catalog (one no-auth JSON covering every - provider), cached on disk ~24h and sorted newest-first + * most cloud providers → the models.dev catalog, cached on disk ~24h + * Atlas Cloud → its public model catalog, filtered to visible LLMs * on any failure → a short curated fallback; the model field stays free-text """ from __future__ import annotations @@ -18,6 +18,7 @@ from core.integration_reliability import request_json MODELS_DEV_URL = "https://models.dev/api.json" +ATLAS_MODELS_URL = "https://api.atlascloud.ai/api/v1/models" _CACHE = Path.home() / ".cache" / "hexis" / "models_dev.json" _CACHE_TTL = 24 * 3600 _TIMEOUT = 12.0 @@ -46,6 +47,7 @@ # Minimal offline fallback if models.dev is unreachable. _FALLBACK: dict[str, list[str]] = { + "atlascloud": ["deepseek-ai/deepseek-v4-pro"], "openai": ["gpt-5.2", "gpt-4o", "gpt-4o-mini"], "openai-codex": ["gpt-5.2", "gpt-5.2-codex"], "anthropic": ["claude-opus-4-8", "claude-sonnet-5", "claude-haiku-4-5"], @@ -192,6 +194,29 @@ async def fetch_models( api_key: str | None = None, ) -> list[str]: """Best-effort current chat model ids for *provider* (newest first).""" + if provider == "atlascloud": + try: + data = await request_json( + "atlascloud_models", + "GET", + ATLAS_MODELS_URL, + timeout=_TIMEOUT, + attempts=2, + max_delay=2.0, + ) + rows = data.get("data", []) if isinstance(data, dict) else [] + models = [ + str(row["model"]) + for row in rows + if isinstance(row, dict) + and row.get("display_console") is True + and row.get("type") == "Text" + and "LLM" in (row.get("categories") or []) + and row.get("model") + ] + return models or _FALLBACK[provider] + except Exception: + return _FALLBACK[provider] if provider == "openai_compatible": if not endpoint: return [] diff --git a/core/cli_api.py b/core/cli_api.py index ab17a13a..75a0ddea 100644 --- a/core/cli_api.py +++ b/core/cli_api.py @@ -429,7 +429,7 @@ def _validate_llm(name: str) -> None: errors.append(f"{name}.api_key_env={api_key_env} is not set in environment") return - if provider in {"openai", "anthropic", "grok", "gemini"}: + if provider in {"openai", "atlascloud", "anthropic", "grok", "gemini"}: if api_key_env: if os.getenv(api_key_env) is None: errors.append( diff --git a/core/llm.py b/core/llm.py index 2a64c01f..be565884 100644 --- a/core/llm.py +++ b/core/llm.py @@ -33,6 +33,7 @@ OPENAI_COMPATIBLE = { "openai", + "atlascloud", "openai_compatible", "openai-chat-completions-endpoint", "grok", @@ -477,7 +478,11 @@ def _should_try_responses( # A generic compatible endpoint promises Chat Completions, not the # Responses event grammar. Some servers expose /responses nominally but # emit tool/reasoning items the official SDK cannot parse safely. - if provider in {"openai_compatible", "openai-chat-completions-endpoint"}: + if provider in { + "atlascloud", + "openai_compatible", + "openai-chat-completions-endpoint", + }: return False if not _HAS_RESPONSES_API: return False @@ -678,6 +683,7 @@ def normalize_endpoint(provider: str, endpoint: str | None) -> str | None: if endpoint: return endpoint.strip() or None _DEFAULTS: dict[str, str] = { + "atlascloud": "https://api.atlascloud.ai/v1", "grok": "https://api.x.ai/v1", "chutes": "https://api.chutes.ai/v1", "qwen-portal": "https://portal.qwen.ai/v1", @@ -718,6 +724,7 @@ def normalize_llm_config(config: dict[str, Any] | None, *, default_model: str = if not api_key: provider_env_map = { "openai": "OPENAI_API_KEY", + "atlascloud": "ATLASCLOUD_API_KEY", "anthropic": "ANTHROPIC_API_KEY", "grok": "XAI_API_KEY", "gemini": "GEMINI_API_KEY", diff --git a/docker-compose.yml b/docker-compose.yml index 3e4be19a..3cb8bf1c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -175,6 +175,7 @@ services: ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} XAI_API_KEY: ${XAI_API_KEY:-} + ATLASCLOUD_API_KEY: ${ATLASCLOUD_API_KEY:-} HEXIS_LLM_CONSCIOUS_API_KEY: ${HEXIS_LLM_CONSCIOUS_API_KEY:-} HEXIS_LLM_SUBCONSCIOUS_API_KEY: ${HEXIS_LLM_SUBCONSCIOUS_API_KEY:-} volumes: @@ -216,6 +217,7 @@ services: ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} XAI_API_KEY: ${XAI_API_KEY:-} + ATLASCLOUD_API_KEY: ${ATLASCLOUD_API_KEY:-} HEXIS_LLM_CONSCIOUS_API_KEY: ${HEXIS_LLM_CONSCIOUS_API_KEY:-} HEXIS_LLM_SUBCONSCIOUS_API_KEY: ${HEXIS_LLM_SUBCONSCIOUS_API_KEY:-} volumes: @@ -271,6 +273,7 @@ services: ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} XAI_API_KEY: ${XAI_API_KEY:-} + ATLASCLOUD_API_KEY: ${ATLASCLOUD_API_KEY:-} HEXIS_LLM_CONSCIOUS_API_KEY: ${HEXIS_LLM_CONSCIOUS_API_KEY:-} HEXIS_LLM_SUBCONSCIOUS_API_KEY: ${HEXIS_LLM_SUBCONSCIOUS_API_KEY:-} volumes: @@ -326,6 +329,7 @@ services: ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} XAI_API_KEY: ${XAI_API_KEY:-} + ATLASCLOUD_API_KEY: ${ATLASCLOUD_API_KEY:-} HEXIS_LLM_CONSCIOUS_API_KEY: ${HEXIS_LLM_CONSCIOUS_API_KEY:-} HEXIS_LLM_SUBCONSCIOUS_API_KEY: ${HEXIS_LLM_SUBCONSCIOUS_API_KEY:-} volumes: @@ -359,6 +363,7 @@ services: ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} XAI_API_KEY: ${XAI_API_KEY:-} + ATLASCLOUD_API_KEY: ${ATLASCLOUD_API_KEY:-} HEXIS_LLM_CONSCIOUS_API_KEY: ${HEXIS_LLM_CONSCIOUS_API_KEY:-} HEXIS_LLM_SUBCONSCIOUS_API_KEY: ${HEXIS_LLM_SUBCONSCIOUS_API_KEY:-} networks: diff --git a/docs/operations/environment-variables.md b/docs/operations/environment-variables.md index 3861f2d7..03f78084 100644 --- a/docs/operations/environment-variables.md +++ b/docs/operations/environment-variables.md @@ -47,6 +47,7 @@ All environment variables used by Hexis, configured via `.env`. | `ANTHROPIC_API_KEY` | Anthropic API key | | `GEMINI_API_KEY` | Google Gemini API key | | `XAI_API_KEY` | xAI Grok API key | +| `ATLASCLOUD_API_KEY` | Atlas Cloud API key | These are only needed for API-key providers. OAuth providers store credentials in the database. diff --git a/hexis-ui/app/api/init/models/route.test.ts b/hexis-ui/app/api/init/models/route.test.ts new file mode 100644 index 00000000..3794661d --- /dev/null +++ b/hexis-ui/app/api/init/models/route.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; + +import { atlasChatModels } from "./route"; + +describe("atlasChatModels", () => { + it("keeps only visible Atlas Cloud LLM models", () => { + expect( + atlasChatModels({ + data: [ + { + model: "chat/model", + type: "Text", + categories: ["LLM"], + display_console: true, + }, + { + model: "hidden/model", + type: "Text", + categories: ["LLM"], + display_console: false, + }, + { + model: "image/model", + type: "Image", + categories: ["TEXT-TO-IMAGE"], + display_console: true, + }, + ], + }) + ).toEqual(["chat/model"]); + }); +}); diff --git a/hexis-ui/app/api/init/models/route.ts b/hexis-ui/app/api/init/models/route.ts index eb3a1ee0..914b7515 100644 --- a/hexis-ui/app/api/init/models/route.ts +++ b/hexis-ui/app/api/init/models/route.ts @@ -2,8 +2,8 @@ // // Mirrors apps/tui/model_catalog.py: prefer a live, auto-updating source over a // hand-maintained list, treat it as advisory, and always allow a free-typed id. -// * cloud providers -> the models.dev catalog (one no-auth JSON), cached ~24h -// in-memory and sorted newest-first +// * most cloud providers -> the models.dev catalog, cached ~24h in-memory +// * Atlas Cloud -> its public model catalog, filtered to visible LLMs // * on any failure -> a short curated fallback; the model field stays free-text import { catalogDeclaredDefault } from "@/lib/init-llm"; @@ -12,6 +12,7 @@ import { hexisApiBaseUrl } from "@/lib/python-api"; export const runtime = "nodejs"; const MODELS_DEV_URL = "https://models.dev/api.json"; +const ATLAS_MODELS_URL = "https://api.atlascloud.ai/api/v1/models"; const CACHE_TTL_MS = 24 * 3600 * 1000; const TIMEOUT_MS = 12000; @@ -61,6 +62,7 @@ const DEFAULT_SKIP = [ // Minimal offline fallback if models.dev is unreachable. const FALLBACK: Record = { + atlascloud: ["deepseek-ai/deepseek-v4-pro"], openai: ["gpt-5.2", "gpt-4o", "gpt-4o-mini"], anthropic: ["claude-opus-4-8", "claude-sonnet-5", "claude-haiku-4-5"], grok: ["grok-4.3", "grok-3"], @@ -169,6 +171,21 @@ function chatModels(value: unknown): string[] { return ids; } +export function atlasChatModels(value: unknown): string[] { + const rows = asRecord(value).data; + if (!Array.isArray(rows)) return []; + return rows.flatMap((value) => { + const row = asRecord(value); + const categories = Array.isArray(row.categories) ? row.categories : []; + return row.display_console === true && + row.type === "Text" && + categories.includes("LLM") && + typeof row.model === "string" + ? [row.model] + : []; + }); +} + // Pick a sensible default from the live catalog (newest-first list). function recommendedDefault(_provider: string, models: string[]): string { if (models.length === 0) return ""; @@ -214,6 +231,26 @@ export async function GET(request: Request) { } } + if (provider === "atlascloud") { + try { + const response = await fetchWithTimeout(ATLAS_MODELS_URL); + if (!response.ok) throw new Error(`Atlas Cloud responded ${response.status}`); + const models = atlasChatModels(await response.json()); + const available = models.length > 0 ? models : FALLBACK[provider]; + return Response.json({ + models: available, + default: recommendedDefault(provider, available), + }); + } catch (err: unknown) { + const models = FALLBACK[provider]; + return Response.json({ + models, + default: recommendedDefault(provider, models), + error: errorMessage(err, "Unable to load Atlas Cloud model catalog."), + }); + } + } + const slug = PROVIDER_SLUG[provider]; if (!slug) { const models = FALLBACK[provider] || []; diff --git a/hexis-ui/app/init/page.tsx b/hexis-ui/app/init/page.tsx index 39c7c7b6..0bcdee2e 100644 --- a/hexis-ui/app/init/page.tsx +++ b/hexis-ui/app/init/page.tsx @@ -53,6 +53,7 @@ const stagePrompt: Record = { type LlmProvider = | "openai-codex" | "openai" + | "atlascloud" | "anthropic" | "grok" | "gemini" @@ -198,6 +199,13 @@ const providerMeta: Record = { apiKeyRequired: true, oauth: false, }, + atlascloud: { + label: "Atlas Cloud", + endpoint: "https://api.atlascloud.ai/v1", + apiKeyLabel: "Atlas Cloud API Key", + apiKeyRequired: true, + oauth: false, + }, anthropic: { label: "Anthropic", endpoint: "", diff --git a/hexis-ui/lib/init-llm.test.ts b/hexis-ui/lib/init-llm.test.ts index 20d410f8..37b92db9 100644 --- a/hexis-ui/lib/init-llm.test.ts +++ b/hexis-ui/lib/init-llm.test.ts @@ -19,6 +19,12 @@ describe("resolveInitLlmEndpoint", () => { ); }); + it("derives the Atlas Cloud API-key default", () => { + expect(resolveInitLlmEndpoint("atlascloud", "")).toBe( + "https://api.atlascloud.ai/v1" + ); + }); + it("derives the recommended model from live catalog metadata", () => { const block = { models: { diff --git a/hexis-ui/lib/init-llm.ts b/hexis-ui/lib/init-llm.ts index eefc1e03..e88f4e7d 100644 --- a/hexis-ui/lib/init-llm.ts +++ b/hexis-ui/lib/init-llm.ts @@ -1,4 +1,5 @@ const ENDPOINT_DEFAULTS: Record = { + atlascloud: "https://api.atlascloud.ai/v1", openai: "https://api.openai.com/v1", }; diff --git a/hexis-ui/lib/llm.ts b/hexis-ui/lib/llm.ts index 34cc7ebe..cc307f89 100644 --- a/hexis-ui/lib/llm.ts +++ b/hexis-ui/lib/llm.ts @@ -9,6 +9,7 @@ import os from "os"; export type LlmProviderName = | "openai" + | "atlascloud" | "openai-codex" | "anthropic" | "grok" @@ -173,6 +174,8 @@ async function callOpenAICompatible(params: { const baseURL = params.provider === "grok" ? "https://api.x.ai/v1" + : params.provider === "atlascloud" + ? params.endpoint || "https://api.atlascloud.ai/v1" : params.endpoint || undefined; const client = new OpenAI({ apiKey: params.apiKey || "local-key", diff --git a/ops/docker-compose.runtime.yml b/ops/docker-compose.runtime.yml index 754fcff3..3f84196f 100644 --- a/ops/docker-compose.runtime.yml +++ b/ops/docker-compose.runtime.yml @@ -126,6 +126,7 @@ services: ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} XAI_API_KEY: ${XAI_API_KEY:-} + ATLASCLOUD_API_KEY: ${ATLASCLOUD_API_KEY:-} HEXIS_LLM_CONSCIOUS_API_KEY: ${HEXIS_LLM_CONSCIOUS_API_KEY:-} HEXIS_LLM_SUBCONSCIOUS_API_KEY: ${HEXIS_LLM_SUBCONSCIOUS_API_KEY:-} volumes: @@ -159,6 +160,7 @@ services: ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} XAI_API_KEY: ${XAI_API_KEY:-} + ATLASCLOUD_API_KEY: ${ATLASCLOUD_API_KEY:-} HEXIS_LLM_CONSCIOUS_API_KEY: ${HEXIS_LLM_CONSCIOUS_API_KEY:-} HEXIS_LLM_SUBCONSCIOUS_API_KEY: ${HEXIS_LLM_SUBCONSCIOUS_API_KEY:-} volumes: @@ -204,6 +206,7 @@ services: ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} XAI_API_KEY: ${XAI_API_KEY:-} + ATLASCLOUD_API_KEY: ${ATLASCLOUD_API_KEY:-} HEXIS_LLM_CONSCIOUS_API_KEY: ${HEXIS_LLM_CONSCIOUS_API_KEY:-} HEXIS_LLM_SUBCONSCIOUS_API_KEY: ${HEXIS_LLM_SUBCONSCIOUS_API_KEY:-} volumes: @@ -251,6 +254,7 @@ services: ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} XAI_API_KEY: ${XAI_API_KEY:-} + ATLASCLOUD_API_KEY: ${ATLASCLOUD_API_KEY:-} HEXIS_LLM_CONSCIOUS_API_KEY: ${HEXIS_LLM_CONSCIOUS_API_KEY:-} HEXIS_LLM_SUBCONSCIOUS_API_KEY: ${HEXIS_LLM_SUBCONSCIOUS_API_KEY:-} volumes: @@ -278,6 +282,7 @@ services: ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} XAI_API_KEY: ${XAI_API_KEY:-} + ATLASCLOUD_API_KEY: ${ATLASCLOUD_API_KEY:-} HEXIS_LLM_CONSCIOUS_API_KEY: ${HEXIS_LLM_CONSCIOUS_API_KEY:-} HEXIS_LLM_SUBCONSCIOUS_API_KEY: ${HEXIS_LLM_SUBCONSCIOUS_API_KEY:-} networks: diff --git a/tests/cli/test_init_noninteractive.py b/tests/cli/test_init_noninteractive.py index d545fff8..ebd7ddff 100644 --- a/tests/cli/test_init_noninteractive.py +++ b/tests/cli/test_init_noninteractive.py @@ -135,7 +135,7 @@ def test_default_models_derive_from_live_catalog(): # Catalog-backed providers map to models.dev. A custom endpoint cannot: # its model list is defined by that server, so callers pass --model. - for provider in set(_PROVIDER_ENV_VARS) - {"openai_compatible"}: + for provider in set(_PROVIDER_ENV_VARS) - {"atlascloud", "openai_compatible"}: assert provider in model_catalog.PROVIDER_SLUG, f"No catalog slug for {provider}" # The flagship heuristic picks a sensible non-variant default. assert model_catalog.recommended_default( @@ -231,6 +231,21 @@ def test_standard_openai_does_not_inherit_custom_endpoint(monkeypatch): assert source == "" +def test_atlascloud_uses_provider_endpoint_and_key_env(monkeypatch): + monkeypatch.setenv("ATLASCLOUD_API_KEY", "atlas-test-key") + args = build_parser().parse_args([ + "--provider", "atlascloud", + "--model", "deepseek-ai/deepseek-v4-pro", + "--api-key-env", "ATLASCLOUD_API_KEY", + ]) + + endpoint, source = _resolve_noninteractive_endpoint(args, "atlascloud") + + assert endpoint == "https://api.atlascloud.ai/v1" + assert source == "provider default" + assert _resolve_noninteractive_api_key_env(args, "atlascloud") == "ATLASCLOUD_API_KEY" + + # --------------------------------------------------------------------------- # CLI smoke tests (subprocess) # --------------------------------------------------------------------------- diff --git a/tests/core/test_llm.py b/tests/core/test_llm.py index d3ff4687..4a706ffa 100644 --- a/tests/core/test_llm.py +++ b/tests/core/test_llm.py @@ -56,6 +56,10 @@ def test_normalize_endpoint_defaults(): def test_normalize_endpoint_new_providers(): + assert ( + llm.normalize_endpoint("atlascloud", None) + == "https://api.atlascloud.ai/v1" + ) assert llm.normalize_endpoint("chutes", None) == "https://api.chutes.ai/v1" assert llm.normalize_endpoint("qwen-portal", None) == "https://portal.qwen.ai/v1" assert llm.normalize_endpoint("google-gemini-cli", None) == "https://cloudcode-pa.googleapis.com" @@ -67,6 +71,25 @@ def test_normalize_endpoint_new_providers(): ) +def test_atlascloud_uses_chat_completions_only(): + assert "atlascloud" in llm.OPENAI_COMPATIBLE + assert not llm._should_try_responses( + "https://api.atlascloud.ai/v1", provider="atlascloud" + ) + + +def test_atlascloud_resolves_conventional_api_key(monkeypatch): + monkeypatch.setenv("ATLASCLOUD_API_KEY", "atlas-test-key") + normalized = llm.normalize_llm_config( + { + "provider": "atlascloud", + "model": "deepseek-ai/deepseek-v4-pro", + } + ) + assert normalized["api_key"] == "atlas-test-key" + assert normalized["endpoint"] == "https://api.atlascloud.ai/v1" + + def test_normalize_llm_config_preserves_auth_mode(): cfg = llm.normalize_llm_config({ "provider": "anthropic", diff --git a/tests/core/test_tui_widgets.py b/tests/core/test_tui_widgets.py index d04fbb88..3df3abcd 100644 --- a/tests/core/test_tui_widgets.py +++ b/tests/core/test_tui_widgets.py @@ -77,6 +77,37 @@ def test_model_catalog_provider_mapping(): assert slugs["anthropic"] == "anthropic" +@pytest.mark.asyncio +async def test_atlascloud_catalog_filters_visible_llms(monkeypatch): + async def fake_request_json(*_args, **_kwargs): + return { + "data": [ + { + "model": "chat/model", + "type": "Text", + "categories": ["LLM"], + "display_console": True, + }, + { + "model": "hidden/model", + "type": "Text", + "categories": ["LLM"], + "display_console": False, + }, + { + "model": "image/model", + "type": "Image", + "categories": ["TEXT-TO-IMAGE"], + "display_console": True, + }, + ] + } + + monkeypatch.setattr(model_catalog, "request_json", fake_request_json) + + assert await model_catalog.fetch_models("atlascloud") == ["chat/model"] + + def test_model_menu_filter_and_suppress(): menu = ModelMenu(id="model-menu") menu.populate(["gpt-5.2", "gpt-4o", "claude-sonnet-5"])