diff --git a/CITATION.cff b/CITATION.cff index ec2e9bbc261..e7b23ca950b 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,7 +1,7 @@ cff-version: 1.2.0 message: "If you use AgentOS in your work, please cite it using this metadata." title: AgentOS -abstract: "An Apache-2.0 TypeScript AI agent runtime: persistent cognitive memory, optional HEXACO personality, multi-agent orchestration, runtime tool forging, multimodal RAG, and one dispatch interface across 11 LLM providers." +abstract: "An Apache-2.0 TypeScript AI agent runtime: persistent cognitive memory, optional HEXACO personality, multi-agent orchestration, runtime tool forging, multimodal RAG, and one dispatch interface across 12 LLM providers." type: software authors: - given-names: Johnny diff --git a/README.md b/README.md index 095cd219a2e..9722a704fa8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ # **AgentOS** · TypeScript AI Agent Framework -**Agents that remember, forge their own tools, and survive long-running sessions.** Persistent cognitive memory, optional HEXACO personality, multi-agent orchestration, and one dispatch interface across 11 LLM providers. Apache-2.0. +**Agents that remember, forge their own tools, and survive long-running sessions.** Persistent cognitive memory, optional HEXACO personality, multi-agent orchestration, and one dispatch interface across 12 LLM providers. Apache-2.0. [![npm](https://img.shields.io/npm/v/@framers/agentos?style=flat-square&logo=npm&color=cb3837)](https://www.npmjs.com/package/@framers/agentos) [![CI](https://img.shields.io/github/actions/workflow/status/framerslab/agentos/ci.yml?branch=master&style=flat-square&logo=github&label=CI)](https://github.com/framerslab/agentos/actions/workflows/ci.yml) @@ -33,7 +33,7 @@ AgentOS is an open-source TypeScript framework for AI agents that **remember, ad - **Top open-source memory benchmarks:** [85.6% on LongMemEval-S](https://github.com/framerslab/agentos-bench/blob/master/results/LEADERBOARD.md) at $0.0090/correct (gpt-4o), and 70.2% on LongMemEval-M, the only open-source library above 65% on M with reproducible methodology. - **Runtime tool forging.** An agent writes a TypeScript function with a Zod schema, an LLM judge approves it, and it runs in a hardened `node:vm` sandbox before joining the catalog for the rest of the session. - **Persistent [cognitive memory](https://docs.agentos.sh/features/cognitive-memory)** with 8 neuroscience-backed mechanisms: Ebbinghaus decay, retrieval-induced forgetting, reconsolidation, source-confidence decay. -- **Optional [HEXACO personality](https://docs.agentos.sh/features/hexaco-personality)**, [6 orchestration strategies](https://docs.agentos.sh/features/agency-collaboration), [guardrails](https://docs.agentos.sh/features/guardrails-architecture), and [voice](https://docs.agentos.sh/features/voice-pipeline) across **11 LLM providers**; 100+ extensions and 88 skills auto-load at startup. +- **Optional [HEXACO personality](https://docs.agentos.sh/features/hexaco-personality)**, [6 orchestration strategies](https://docs.agentos.sh/features/agency-collaboration), [guardrails](https://docs.agentos.sh/features/guardrails-architecture), and [voice](https://docs.agentos.sh/features/voice-pipeline) across **12 LLM providers**; 100+ extensions and 88 skills auto-load at startup. --- @@ -178,7 +178,7 @@ Strategies: `sequential`, `parallel`, `debate`, `review-loop`, `hierarchical`, ` | Package | Role | |---|---| -| [`@framers/agentos`](https://www.npmjs.com/package/@framers/agentos) | Core runtime: agents, cognitive memory, orchestration, guardrails, voice, 11 LLM providers. Apache-2.0. | +| [`@framers/agentos`](https://www.npmjs.com/package/@framers/agentos) | Core runtime: agents, cognitive memory, orchestration, guardrails, voice, 12 LLM providers. Apache-2.0. | | [`@framers/agentos-extensions`](https://www.npmjs.com/package/@framers/agentos-extensions) | 100+ first-party extensions: channel adapters, tool packs, integrations, guardrail packs. | | [`@framers/agentos-extensions-registry`](https://www.npmjs.com/package/@framers/agentos-extensions-registry) | Discovery + auto-loader for the extensions catalog. | | [`@framers/agentos-skills`](https://www.npmjs.com/package/@framers/agentos-skills) | 88 curated `SKILL.md` skills. | diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index a354f672fe8..640ac9c6402 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -14,7 +14,7 @@ This page is the system map. For the *what* of each subsystem — components, li For specific subsystem deep-dives, see: - [Provenance & Immutability](../features/provenance-immutability.md) -![AgentOS layered architecture: seven cooperating layers from caller-facing API (generateText, streamText, agent, agency, mission) through cognitive substrate (GMI coordinator, PersonaOverlayManager, SentimentTracker, MetapromptExecutor), memory and RAG (4-tier memory, 8 cognitive mechanisms, HyDE, GraphRAG, 7 vector backends), tools and capabilities (100+ extension packs, 88 SKILL.md modules, runtime tool forging), guardrails and HITL (PII redaction, ML classifiers, NLI grounding, 5 approval triggers), orchestration (workflow, mission, AgentGraph, checkpointing), down to I/O and providers (voice pipeline, channels, media generation, 11 LLM providers, OpenRouter fanout).](/img/diagrams/system-architecture.svg) +![AgentOS layered architecture: seven cooperating layers from caller-facing API (generateText, streamText, agent, agency, mission) through cognitive substrate (GMI coordinator, PersonaOverlayManager, SentimentTracker, MetapromptExecutor), memory and RAG (4-tier memory, 8 cognitive mechanisms, HyDE, GraphRAG, 7 vector backends), tools and capabilities (100+ extension packs, 88 SKILL.md modules, runtime tool forging), guardrails and HITL (PII redaction, ML classifiers, NLI grounding, 5 approval triggers), orchestration (workflow, mission, AgentGraph, checkpointing), down to I/O and providers (voice pipeline, channels, media generation, 12 LLM providers, OpenRouter fanout).](/img/diagrams/system-architecture.svg) Each layer above corresponds to a section below. The mapping is one-to-one: layer 1 → [API Surface Contract](#api-surface-contract), layer 2 → [GMI](#gmi-generalized-mind-instance), layer 3 → [Memory System](#memory-system), layer 4 → [Tools, Skills, Extensions](#tools-skills--extensions), layer 5 → [Safety & Guardrails](#safety--guardrails), layer 6 → [Orchestration](#orchestration), layer 7 → [Perception & Channels](#perception--channels). The component pills inside each layer in the diagram are the same class and function names you'll see in the subsystem write-ups. diff --git a/docs/features/LLM_PROVIDERS.md b/docs/features/LLM_PROVIDERS.md index 1e009edf084..ab430721cae 100644 --- a/docs/features/LLM_PROVIDERS.md +++ b/docs/features/LLM_PROVIDERS.md @@ -1,6 +1,6 @@ # LLM Providers — multi-provider configuration & routing -AgentOS abstracts every LLM behind a single [`IProvider`](https://github.com/framerslab/agentos/blob/master/src/core/llm/providers/IProvider.ts) interface. Eleven providers are wired in directly — nine via API key, two via local CLI bridges that ride an existing Claude Max or Google account subscription. OpenRouter, included in the eleven, fans out to 200+ additional models from the same set of vendors. Every provider speaks the same streaming protocol, supports the same tool-call shape (with the documented exceptions below), and participates in the same cost ledger. The fallback chain is auto-built from whichever keys are set in the environment and is overridable per agent. +AgentOS abstracts every LLM behind a single [`IProvider`](https://github.com/framerslab/agentos/blob/master/src/core/llm/providers/IProvider.ts) interface. Twelve providers are wired in directly — nine via API key, Ollama locally, and two via local CLI bridges that ride an existing Claude Max or Google account subscription. OpenRouter, included in the twelve, fans out to 200+ additional models from the same set of vendors. Every provider speaks the same streaming protocol, supports the same tool-call shape (with the documented exceptions below), and participates in the same cost ledger. The fallback chain is auto-built from whichever keys are set in the environment and is overridable per agent. --- @@ -22,6 +22,7 @@ AgentOS abstracts every LLM behind a single [`IProvider`](https://github.com/fra - [Mistral AI](#mistral-ai) - [xAI (Grok)](#xai-grok) - [OpenRouter](#openrouter) + - [Atlas Cloud](#atlas-cloud) - [Ollama](#ollama) 9. [Programmatic Configuration](#programmatic-configuration) 10. [Adding a Custom Provider](#adding-a-custom-provider) @@ -36,7 +37,7 @@ AgentOS abstracts LLM access behind a unified [`IProvider`](https://github.com/f **Key features:** -- **11 providers** supported out of the box (9 API-key + 2 CLI-based) +- **12 providers** supported out of the box (9 API-key + Ollama + 2 CLI-based) - **CLI providers**: Use your Claude Max or Google account subscription via local CLI — no API key needed - **Auto-detection**: Set an API key or install a CLI and the provider is available - **Fallback**: Automatic retry with alternate providers on failure (`fallbackProviders`) @@ -58,6 +59,7 @@ AgentOS abstracts LLM access behind a unified [`IProvider`](https://github.com/f | **Mistral** | `MISTRAL_API_KEY` | `mistral-large-latest` | Yes | Yes | No | Yes | $$ | | **xAI** | `XAI_API_KEY` | `grok-2` | Yes | Yes | Yes | No | $$ | | **OpenRouter** | `OPENROUTER_API_KEY` | `openai/gpt-4o` | Yes | Yes | Yes* | Yes* | Varies | +| **Atlas Cloud** | `ATLASCLOUD_API_KEY` | `deepseek-ai/deepseek-v4-pro` | Yes | Model-dep. | No | No | Varies | | **Ollama** | `OLLAMA_BASE_URL` | `llama3.2` | Yes | Partial | Model-dep. | Yes | Free | | **Claude Code CLI** | _(PATH detection)_ | `claude-sonnet-4-5-20250929` | Yes | Yes | Yes | No | Free* | | **Gemini CLI** | _(PATH detection)_ | `gemini-2.5-flash` | Yes | Partial** | Yes | No | Free* | @@ -111,15 +113,20 @@ order and uses the first one found: 1. `OPENROUTER_API_KEY` → OpenRouter 2. `OPENAI_API_KEY` → OpenAI -3. `ANTHROPIC_API_KEY` → Anthropic -4. `GEMINI_API_KEY` → Google Gemini -5. `GROQ_API_KEY` → Groq -6. `TOGETHER_API_KEY` → Together AI -7. `MISTRAL_API_KEY` → Mistral -8. `XAI_API_KEY` → xAI -9. `which claude` → Claude Code CLI (PATH detection — no API key, uses Max subscription) -10. `which gemini` → Gemini CLI (PATH detection — no API key, uses Google account) -11. `OLLAMA_BASE_URL` → Ollama +3. `ATLASCLOUD_API_KEY` → Atlas Cloud +4. `ANTHROPIC_API_KEY` → Anthropic +5. `GEMINI_API_KEY` → Google Gemini +6. `GROQ_API_KEY` → Groq +7. `TOGETHER_API_KEY` → Together AI +8. `MISTRAL_API_KEY` → Mistral +9. `XAI_API_KEY` → xAI +10. `which claude` → Claude Code CLI (PATH detection — no API key, uses Max subscription) +11. `which gemini` → Gemini CLI (PATH detection — no API key, uses Google account) +12. `OLLAMA_BASE_URL` → Ollama + +This list describes the public runtime's `autoDetectProvider()` order. The +QueryRouter defaults prefer direct provider keys before `OPENROUTER_API_KEY` so +their selected provider and credential resolver remain aligned. You can override auto-detection in four ways, highest priority first: @@ -161,6 +168,9 @@ OPENAI_API_KEY=sk-... # Fallback provider OPENROUTER_API_KEY=sk-or-... +# OpenAI-compatible provider +ATLASCLOUD_API_KEY=... + # Local provider (no API key needed) OLLAMA_BASE_URL=http://localhost:11434 ``` @@ -240,7 +250,8 @@ AgentOS tracks token usage and cost across all providers: | Tier | Providers | Approximate Cost (1M tokens) | |------|-----------|------------------------------| | **$** (Budget) | Groq, Together, Ollama (free) | $0.00–$0.60 | -| **$$** (Standard) | Gemini, Mistral, xAI, OpenRouter (varies) | $0.50–$3.00 | +| **$$** (Standard) | Gemini, Mistral, xAI | $0.50–$3.00 | +| **Varies** | OpenRouter, Atlas Cloud | Model-dependent | | **$$$** (Premium) | OpenAI, Anthropic | $3.00–$15.00 | ### Cost-Aware Caps @@ -385,6 +396,23 @@ Popular OpenRouter models: - `google/gemini-2.5-flash` - `meta-llama/llama-3.3-70b-instruct` +### Atlas Cloud + +```bash +export ATLASCLOUD_API_KEY=... +# Optional endpoint override for proxies or private deployments: +export ATLASCLOUD_BASE_URL=https://api.atlascloud.ai/v1 +``` + +Atlas Cloud uses an OpenAI-compatible `/v1/chat/completions` endpoint, so it +shares AgentOS's standard OpenAI-compatible transport, streaming, tool-call, and +structured-output handling. + +| Model | Context | Vision | Tool Calling | Notes | +|-------|---------|--------|-------------|-------| +| `deepseek-ai/deepseek-v4-pro` | 1M | No | Yes | Default long-context reasoning model | +| `qwen/qwen3.5-flash` | 1M | No | Partial | Fast chat model | + ### Ollama ```bash @@ -491,6 +519,7 @@ exercised paths. | Mistral | Yes | No | `auto/none/any` | Good support | | xAI | Yes | No | `auto/none` | Basic tool use | | OpenRouter | Model-dependent | Model-dependent | Model-dependent | Pass-through | +| Atlas Cloud | Model-dependent | Model-dependent | Model-dependent | Depends on the selected Atlas Cloud model | | Ollama | Partial | No | `auto/none` | Model-dependent | ### Embedding Support diff --git a/package.json b/package.json index edfba827a60..31fdb5aafa6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@framers/agentos", "version": "0.10.11", - "description": "AgentOS: open-source TypeScript framework for autonomous AI agents. Unified graph orchestration, cognitive memory, runtime tool forging, multi-tier guardrails, voice pipeline, and 11 LLM providers.", + "description": "AgentOS: open-source TypeScript framework for autonomous AI agents. Unified graph orchestration, cognitive memory, runtime tool forging, multi-tier guardrails, voice pipeline, and 12 LLM providers.", "homepage": "https://agentos.sh", "repository": { "type": "git", diff --git a/scripts/build-knowledge-corpus.mjs b/scripts/build-knowledge-corpus.mjs index f7837f93f39..44dc16b4bbc 100644 --- a/scripts/build-knowledge-corpus.mjs +++ b/scripts/build-knowledge-corpus.mjs @@ -210,7 +210,7 @@ function getFaqEntries(skillCount = 0) { id: 'faq:supported-models', heading: 'What LLM models are supported?', content: - 'AgentOS supports 11 LLM providers: OpenAI (gpt-4o, gpt-4o-mini), Anthropic (claude-sonnet, claude-haiku, claude-opus), Google Gemini (gemini-2.5-pro, gemini-2.5-flash), Groq, Together AI, Mistral, xAI, OpenRouter (fans out to 200+ models), and Ollama (any local model), plus CLI bridges for Claude Code and Gemini CLI.', + 'AgentOS supports 12 LLM providers: OpenAI (gpt-4o, gpt-4o-mini), Anthropic (claude-sonnet, claude-haiku, claude-opus), Google Gemini (gemini-2.5-pro, gemini-2.5-flash), Groq, Together AI, Mistral, xAI, Atlas Cloud, OpenRouter (fans out to 200+ models), and Ollama (any local model), plus CLI bridges for Claude Code and Gemini CLI.', category: 'faq', }, { @@ -489,7 +489,7 @@ function getApiReferenceEntries() { id: 'api:generateText', heading: 'generateText() API', content: - "import { generateText } from '@framers/agentos'. generateText({ model: 'openai:gpt-4o', prompt: 'Hello', system?: '...', tools?: [...], maxSteps?: 5, temperature?: 0.7 }) -> Promise<{ text, provider, model, usage, toolCalls }>. The core text generation function supporting all 11 LLM providers.", + "import { generateText } from '@framers/agentos'. generateText({ model: 'openai:gpt-4o', prompt: 'Hello', system?: '...', tools?: [...], maxSteps?: 5, temperature?: 0.7 }) -> Promise<{ text, provider, model, usage, toolCalls }>. The core text generation function supporting all 12 LLM providers.", category: 'api', }, { diff --git a/src/api/model.ts b/src/api/model.ts index dee2edc6bb7..4e5667f9233 100644 --- a/src/api/model.ts +++ b/src/api/model.ts @@ -39,6 +39,7 @@ export interface ResolvedProvider { const ENV_KEY_MAP: Record = { openai: 'OPENAI_API_KEY', + atlascloud: 'ATLASCLOUD_API_KEY', anthropic: 'ANTHROPIC_API_KEY', openrouter: 'OPENROUTER_API_KEY', gemini: 'GEMINI_API_KEY', @@ -52,6 +53,7 @@ const ENV_KEY_MAP: Record = { const ENV_URL_MAP: Record = { openai: 'OPENAI_BASE_URL', + atlascloud: 'ATLASCLOUD_BASE_URL', openrouter: 'OPENROUTER_BASE_URL', stability: 'STABILITY_BASE_URL', replicate: 'REPLICATE_BASE_URL', diff --git a/src/api/runtime/__tests__/provider-defaults.test.ts b/src/api/runtime/__tests__/provider-defaults.test.ts index 39449d1ccdb..8b40c43a39b 100644 --- a/src/api/runtime/__tests__/provider-defaults.test.ts +++ b/src/api/runtime/__tests__/provider-defaults.test.ts @@ -13,7 +13,7 @@ import { resolveModelOption, resolveProvider } from '../model.js'; describe('PROVIDER_DEFAULTS', () => { it('has text model for all major providers', () => { - for (const id of ['openai', 'anthropic', 'ollama', 'openrouter', 'gemini']) { + for (const id of ['openai', 'anthropic', 'ollama', 'openrouter', 'atlascloud', 'gemini']) { expect(PROVIDER_DEFAULTS[id]?.text).toBeDefined(); } }); @@ -40,6 +40,7 @@ describe('autoDetectProvider', () => { // Restore env after each test for (const key of [ 'OPENROUTER_API_KEY', + 'ATLASCLOUD_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', @@ -62,6 +63,7 @@ describe('autoDetectProvider', () => { it('detects openai from OPENAI_API_KEY', () => { delete process.env.ANTHROPIC_API_KEY; delete process.env.OPENROUTER_API_KEY; + delete process.env.ATLASCLOUD_API_KEY; process.env.OPENAI_API_KEY = 'test'; expect(autoDetectProvider()).toBe('openai'); }); @@ -75,6 +77,7 @@ describe('autoDetectProvider', () => { it('detects anthropic from ANTHROPIC_API_KEY', () => { delete process.env.OPENAI_API_KEY; delete process.env.OPENROUTER_API_KEY; + delete process.env.ATLASCLOUD_API_KEY; process.env.ANTHROPIC_API_KEY = 'test'; expect(autoDetectProvider()).toBe('anthropic'); }); @@ -82,6 +85,7 @@ describe('autoDetectProvider', () => { it('skips providers without image defaults when detecting for image tasks', () => { delete process.env.OPENAI_API_KEY; delete process.env.OPENROUTER_API_KEY; + delete process.env.ATLASCLOUD_API_KEY; process.env.ANTHROPIC_API_KEY = 'anthropic-test'; process.env.STABILITY_API_KEY = 'stability-test'; @@ -92,6 +96,7 @@ describe('autoDetectProvider', () => { delete process.env.OPENAI_API_KEY; delete process.env.ANTHROPIC_API_KEY; delete process.env.OPENROUTER_API_KEY; + delete process.env.ATLASCLOUD_API_KEY; delete process.env.GEMINI_API_KEY; delete process.env.GROQ_API_KEY; delete process.env.TOGETHER_API_KEY; @@ -107,6 +112,7 @@ describe('autoDetectProvider', () => { delete process.env.OPENAI_API_KEY; delete process.env.ANTHROPIC_API_KEY; delete process.env.OPENROUTER_API_KEY; + delete process.env.ATLASCLOUD_API_KEY; delete process.env.GEMINI_API_KEY; delete process.env.GROQ_API_KEY; delete process.env.TOGETHER_API_KEY; @@ -120,6 +126,13 @@ describe('autoDetectProvider', () => { expect(autoDetectProvider()).toBe('claude-code-cli'); }); + + it('detects atlascloud from ATLASCLOUD_API_KEY', () => { + delete process.env.OPENROUTER_API_KEY; + delete process.env.OPENAI_API_KEY; + process.env.ATLASCLOUD_API_KEY = 'atlas-test'; + expect(autoDetectProvider()).toBe('atlascloud'); + }); }); describe('resolveModelOption', () => { @@ -128,6 +141,11 @@ describe('resolveModelOption', () => { expect(result).toEqual({ providerId: 'openai', modelId: 'gpt-4o' }); }); + it('resolves atlascloud provider-only to default text model', () => { + const result = resolveModelOption({ provider: 'atlascloud' }, 'text'); + expect(result).toEqual({ providerId: 'atlascloud', modelId: 'deepseek-ai/deepseek-v4-pro' }); + }); + it('resolves provider + explicit model override', () => { const result = resolveModelOption({ provider: 'openai', model: 'gpt-4o-mini' }, 'text'); expect(result).toEqual({ providerId: 'openai', modelId: 'gpt-4o-mini' }); diff --git a/src/api/runtime/__tests__/responseFormatForProvider.test.ts b/src/api/runtime/__tests__/responseFormatForProvider.test.ts index 98a4d204edf..57e50bed3ee 100644 --- a/src/api/runtime/__tests__/responseFormatForProvider.test.ts +++ b/src/api/runtime/__tests__/responseFormatForProvider.test.ts @@ -81,6 +81,15 @@ describe('buildResponseFormatForProvider', () => { ).toEqual({ type: 'json_object' }); }); + it('atlascloud + plain schema -> strict json_schema; record -> json_object', () => { + expect( + buildResponseFormatForProvider(inputs('atlascloud', 'deepseek-ai/deepseek-v4-pro', plainSchema)), + ).toMatchObject({ type: 'json_schema' }); + expect( + buildResponseFormatForProvider(inputs('atlascloud', 'deepseek-ai/deepseek-v4-pro', recordSchema)), + ).toEqual({ type: 'json_object' }); + }); + it('unknown provider -> undefined (schema rides the system prompt)', () => { expect( buildResponseFormatForProvider(inputs('ollama', 'llama3', plainSchema)), diff --git a/src/api/runtime/provider-defaults.ts b/src/api/runtime/provider-defaults.ts index a97356542ad..9b697ab3754 100644 --- a/src/api/runtime/provider-defaults.ts +++ b/src/api/runtime/provider-defaults.ts @@ -55,6 +55,10 @@ export const PROVIDER_DEFAULTS: Record = { text: 'openai/gpt-4o', cheap: 'openai/gpt-4o-mini', }, + atlascloud: { + text: 'deepseek-ai/deepseek-v4-pro', + cheap: 'qwen/qwen3.5-flash', + }, gemini: { text: 'gemini-2.5-flash', cheap: 'gemini-2.0-flash', @@ -125,6 +129,7 @@ const AUTO_DETECT_ORDER: AutoDetectProbe[] = [ // are set". { envKey: 'OPENROUTER_API_KEY', provider: 'openrouter' }, { envKey: 'OPENAI_API_KEY', provider: 'openai' }, + { envKey: 'ATLASCLOUD_API_KEY', provider: 'atlascloud' }, { envKey: 'ANTHROPIC_API_KEY', provider: 'anthropic' }, { envKey: 'GEMINI_API_KEY', provider: 'gemini' }, { envKey: 'GROQ_API_KEY', provider: 'groq' }, diff --git a/src/api/runtime/responseFormatForProvider.ts b/src/api/runtime/responseFormatForProvider.ts index b85d04e8156..229f352edea 100644 --- a/src/api/runtime/responseFormatForProvider.ts +++ b/src/api/runtime/responseFormatForProvider.ts @@ -23,7 +23,7 @@ import { modelSupportsForcedToolChoice } from '../../core/llm/providers/model-fo * as a loose JSON mode. (Moved from generateObject.ts so the provider-branch * logic lives exactly once.) */ -export const JSON_MODE_PROVIDERS = new Set(['openai', 'openrouter']); +export const JSON_MODE_PROVIDERS = new Set(['openai', 'openrouter', 'atlascloud']); /** Inputs for {@link buildResponseFormatForProvider}. */ export interface BuildResponseFormatForProviderInputs { @@ -61,7 +61,7 @@ export interface BuildResponseFormatForProviderInputs { * `options.responseFormat` (bridge options + text return only), so * emitting the Gemini marker there is dead weight; the leg is prompt-only * in practice and this builder says so honestly. - * - `openrouter` — OpenAI-shaped strict `json_schema` (forwarded upstream + * - `openrouter` / `atlascloud` — OpenAI-shaped strict `json_schema` (forwarded upstream * with `require_parameters` routing) when strict-compatible, else * `json_object`. * - any other {@link JSON_MODE_PROVIDERS} member — `json_object`. @@ -101,7 +101,7 @@ export function buildResponseFormatForProvider( // The leg is prompt-only in practice; say so honestly. return undefined; } - if (providerId === 'openrouter') { + if (providerId === 'openrouter' || providerId === 'atlascloud') { return canUseStrictJsonSchema(jsonSchema) ? buildOpenAIJsonSchemaResponseFormat(jsonSchema, schemaName) : { type: 'json_object' as const }; diff --git a/src/core/llm/providers/AIModelProviderManager.ts b/src/core/llm/providers/AIModelProviderManager.ts index c3e7aa1b1e2..9362a981ecb 100644 --- a/src/core/llm/providers/AIModelProviderManager.ts +++ b/src/core/llm/providers/AIModelProviderManager.ts @@ -27,6 +27,7 @@ import { GroqProvider, GroqProviderConfig } from './implementations/GroqProvider import { TogetherProvider, TogetherProviderConfig } from './implementations/TogetherProvider'; import { MistralProvider, MistralProviderConfig } from './implementations/MistralProvider'; import { XAIProvider, XAIProviderConfig } from './implementations/XAIProvider'; +import { AtlasCloudProvider, AtlasCloudProviderConfig } from './implementations/AtlasCloudProvider'; import { GeminiProvider, GeminiProviderConfig } from './implementations/GeminiProvider'; import { ClaudeCodeProvider, ClaudeCodeProviderConfig } from './implementations/ClaudeCodeProvider'; import { GeminiCLIProvider, GeminiCLIProviderConfig } from './implementations/GeminiCLIProvider'; @@ -39,7 +40,7 @@ import { GMIError, GMIErrorCode, createGMIErrorFromError } from '../../utils/err export interface ProviderConfigEntry { providerId: string; enabled: boolean; - config: Partial>; + config: Partial>; isDefault?: boolean; } @@ -144,6 +145,9 @@ export class AIModelProviderManager { case 'xai': providerInstance = new XAIProvider(); break; + case 'atlascloud': + providerInstance = new AtlasCloudProvider(); + break; case 'gemini': providerInstance = new GeminiProvider(); break; @@ -376,4 +380,3 @@ export class AIModelProviderManager { console.log("AIModelProviderManager: Shutdown complete. All providers processed."); } } - diff --git a/src/core/llm/providers/__tests__/OpenAICompatProviders.test.ts b/src/core/llm/providers/__tests__/OpenAICompatProviders.test.ts index ee7732a1862..c438432a0c8 100644 --- a/src/core/llm/providers/__tests__/OpenAICompatProviders.test.ts +++ b/src/core/llm/providers/__tests__/OpenAICompatProviders.test.ts @@ -1,7 +1,7 @@ /** * @fileoverview Unit tests for the OpenAI-compatible provider wrappers. * - * Validates that Groq, Together, Mistral, and xAI providers: + * Validates that Groq, Together, Mistral, xAI, and Atlas Cloud providers: * - Initialize correctly with proper base URLs * - Delegate to the underlying OpenAI provider * - Have correct model defaults in their catalogs @@ -41,6 +41,7 @@ import { GroqProvider } from '../implementations/GroqProvider'; import { TogetherProvider } from '../implementations/TogetherProvider'; import { MistralProvider } from '../implementations/MistralProvider'; import { XAIProvider } from '../implementations/XAIProvider'; +import { AtlasCloudProvider } from '../implementations/AtlasCloudProvider'; import { PROVIDER_DEFAULTS, autoDetectProvider } from '../../../../api/runtime/provider-defaults'; // --------------------------------------------------------------------------- @@ -247,6 +248,63 @@ describe('OpenAI-compatible provider wrappers', () => { }); }); + // ========================================================================= + // AtlasCloudProvider + // ========================================================================= + + describe('AtlasCloudProvider', () => { + it('has providerId "atlascloud"', () => { + const provider = new AtlasCloudProvider(); + expect(provider.providerId).toBe('atlascloud'); + }); + + it('initializes with Atlas Cloud base URL', async () => { + const provider = new AtlasCloudProvider(); + await provider.initialize({ apiKey: 'atlas-test-key' }); + + expect(initializeMock).toHaveBeenCalledTimes(1); + const config = initializeMock.mock.calls[0][0]; + expect(config.apiKey).toBe('atlas-test-key'); + expect(config.baseURL).toBe('https://api.atlascloud.ai/v1'); + }); + + it('throws when API key is missing', async () => { + const provider = new AtlasCloudProvider(); + await expect(provider.initialize({ apiKey: '' })).rejects.toThrow('API key is required'); + }); + + it('defaults to deepseek-ai/deepseek-v4-pro model', async () => { + const provider = new AtlasCloudProvider(); + await provider.initialize({ apiKey: 'atlas-test' }); + expect(provider.defaultModelId).toBe('deepseek-ai/deepseek-v4-pro'); + }); + + it('lists known Atlas Cloud models', async () => { + const provider = new AtlasCloudProvider(); + await provider.initialize({ apiKey: 'atlas-test' }); + const models = await provider.listAvailableModels(); + const ids = models.map(m => m.modelId); + expect(ids).toContain('deepseek-ai/deepseek-v4-pro'); + expect(ids).toContain('qwen/qwen3.5-flash'); + for (const m of models) { + expect(m.providerId).toBe('atlascloud'); + } + }); + + it('delegates generateCompletion to OpenAI provider', async () => { + const provider = new AtlasCloudProvider(); + await provider.initialize({ apiKey: 'atlas-test' }); + await provider.generateCompletion('test-model', [], {}); + expect(generateCompletionMock).toHaveBeenCalledTimes(1); + }); + + it('rejects embeddings (not supported)', async () => { + const provider = new AtlasCloudProvider(); + await provider.initialize({ apiKey: 'atlas-test' }); + await expect(provider.generateEmbeddings('model', ['text'])).rejects.toThrow('embeddings'); + }); + }); + // ========================================================================= // PROVIDER_DEFAULTS registration // ========================================================================= @@ -276,6 +334,12 @@ describe('OpenAI-compatible provider wrappers', () => { expect(PROVIDER_DEFAULTS.xai.cheap).toBe('grok-2-mini'); }); + it('includes atlascloud with correct defaults', () => { + expect(PROVIDER_DEFAULTS.atlascloud).toBeDefined(); + expect(PROVIDER_DEFAULTS.atlascloud.text).toBe('deepseek-ai/deepseek-v4-pro'); + expect(PROVIDER_DEFAULTS.atlascloud.cheap).toBe('qwen/qwen3.5-flash'); + }); + it('still has anthropic defaults', () => { expect(PROVIDER_DEFAULTS.anthropic).toBeDefined(); expect(PROVIDER_DEFAULTS.anthropic.text).toBe('claude-sonnet-4-6'); @@ -294,6 +358,7 @@ describe('OpenAI-compatible provider wrappers', () => { // Clear all provider env vars so auto-detect starts clean process.env = { ...originalEnv }; delete process.env.OPENAI_API_KEY; + delete process.env.ATLASCLOUD_API_KEY; delete process.env.ANTHROPIC_API_KEY; delete process.env.OPENROUTER_API_KEY; delete process.env.GEMINI_API_KEY; @@ -331,6 +396,11 @@ describe('OpenAI-compatible provider wrappers', () => { expect(autoDetectProvider()).toBe('xai'); }); + it('detects atlascloud when ATLASCLOUD_API_KEY is set', () => { + process.env.ATLASCLOUD_API_KEY = 'atlas-test'; + expect(autoDetectProvider()).toBe('atlascloud'); + }); + it('prefers openai over groq when both are set', () => { process.env.OPENAI_API_KEY = 'sk-test'; process.env.GROQ_API_KEY = 'gsk-test'; diff --git a/src/core/llm/providers/implementations/AtlasCloudProvider.ts b/src/core/llm/providers/implementations/AtlasCloudProvider.ts new file mode 100644 index 00000000000..537e96bc795 --- /dev/null +++ b/src/core/llm/providers/implementations/AtlasCloudProvider.ts @@ -0,0 +1,149 @@ +// File: backend/agentos/core/llm/providers/implementations/AtlasCloudProvider.ts + +/** + * @fileoverview Implements the IProvider interface for Atlas Cloud's + * OpenAI-compatible LLM endpoint. + * + * Atlas Cloud exposes `/v1/chat/completions` with OpenAI-shaped request and + * response payloads, so this provider reuses the OpenAIProvider transport while + * keeping Atlas Cloud credentials, endpoint defaults, and model catalog separate + * from OpenAI. + * + * @module backend/agentos/core/llm/providers/implementations/AtlasCloudProvider + * @implements {IProvider} + */ + +import { + IProvider, + ChatMessage, + ModelCompletionOptions, + ModelCompletionResponse, + ModelInfo, + ProviderEmbeddingOptions, + ProviderEmbeddingResponse, +} from '../IProvider'; +import { OpenAIProvider } from './OpenAIProvider'; + +export interface AtlasCloudProviderConfig { + /** Atlas Cloud API key. Sourced from `ATLASCLOUD_API_KEY`. */ + apiKey: string; + /** + * Base URL override. + * @default "https://api.atlascloud.ai/v1" + */ + baseURL?: string; + /** + * Default model to use when none is specified. + * @default "deepseek-ai/deepseek-v4-pro" + */ + defaultModelId?: string; + /** Request timeout in milliseconds. @default 60000 */ + requestTimeout?: number; +} + +const ATLAS_CLOUD_MODELS: ModelInfo[] = [ + { + modelId: 'deepseek-ai/deepseek-v4-pro', + providerId: 'atlascloud', + displayName: 'DeepSeek V4 Pro', + description: 'Long-context reasoning model served through Atlas Cloud.', + capabilities: ['chat', 'tool_use', 'json_mode', 'structured_outputs'], + contextWindowSize: 1048576, + supportsStreaming: true, + status: 'active', + }, + { + modelId: 'qwen/qwen3.5-flash', + providerId: 'atlascloud', + displayName: 'Qwen3.5 Flash', + description: 'Fast long-context chat model served through Atlas Cloud.', + capabilities: ['chat'], + contextWindowSize: 1000000, + supportsStreaming: true, + status: 'active', + }, +]; + +/** + * Thin wrapper around {@link OpenAIProvider} for Atlas Cloud's + * OpenAI-compatible LLM API. + */ +export class AtlasCloudProvider implements IProvider { + /** @inheritdoc */ + public readonly providerId: string = 'atlascloud'; + /** @inheritdoc */ + public isInitialized: boolean = false; + /** @inheritdoc */ + public defaultModelId?: string; + + private delegate = new OpenAIProvider(); + + public async initialize(config: AtlasCloudProviderConfig): Promise { + if (!config.apiKey) { + throw new Error('API key is required for AtlasCloudProvider. Set ATLASCLOUD_API_KEY.'); + } + + this.defaultModelId = config.defaultModelId ?? 'deepseek-ai/deepseek-v4-pro'; + + await this.delegate.initialize({ + apiKey: config.apiKey, + baseURL: config.baseURL ?? 'https://api.atlascloud.ai/v1', + defaultModelId: this.defaultModelId, + requestTimeout: config.requestTimeout ?? 60000, + }); + + this.isInitialized = true; + } + + /** @inheritdoc */ + public async generateCompletion( + modelId: string, + messages: ChatMessage[], + options: ModelCompletionOptions, + ): Promise { + return this.delegate.generateCompletion(modelId, messages, options); + } + + /** @inheritdoc */ + public async *generateCompletionStream( + modelId: string, + messages: ChatMessage[], + options: ModelCompletionOptions, + ): AsyncGenerator { + yield* this.delegate.generateCompletionStream(modelId, messages, options); + } + + public async generateEmbeddings( + _modelId: string, + _texts: string[], + _options?: ProviderEmbeddingOptions, + ): Promise { + throw new Error( + 'Atlas Cloud does not currently expose embeddings in AgentOS. Use a dedicated embedding provider.', + ); + } + + public async listAvailableModels(filter?: { capability?: string }): Promise { + if (filter?.capability) { + const { capability } = filter; + return ATLAS_CLOUD_MODELS.filter(m => m.capabilities.includes(capability)); + } + return [...ATLAS_CLOUD_MODELS]; + } + + /** @inheritdoc */ + public async getModelInfo(modelId: string): Promise { + return ATLAS_CLOUD_MODELS.find(m => m.modelId === modelId); + } + + /** @inheritdoc */ + public async checkHealth(): Promise<{ isHealthy: boolean; details?: unknown }> { + return this.delegate.checkHealth(); + } + + /** @inheritdoc */ + public async shutdown(): Promise { + await this.delegate.shutdown(); + this.isInitialized = false; + } +} diff --git a/src/orchestration/__tests__/ProviderAssignmentEngine.test.ts b/src/orchestration/__tests__/ProviderAssignmentEngine.test.ts index b2675be08d0..0472c1d254a 100644 --- a/src/orchestration/__tests__/ProviderAssignmentEngine.test.ts +++ b/src/orchestration/__tests__/ProviderAssignmentEngine.test.ts @@ -73,6 +73,23 @@ describe('ProviderAssignmentEngine', () => { expect(assignments.find((a) => a.nodeId === 'border_low')!.model).toBe('gpt-4o-mini'); expect(assignments.find((a) => a.nodeId === 'border_high')!.model).toBe('gpt-4o'); }); + + it('uses Atlas Cloud when it is the only available provider', () => { + const engine = new ProviderAssignmentEngine(['atlascloud']); + const nodes = [makeGmiNode('easy', 0.1), makeGmiNode('hard', 0.9)]; + const assignments = engine.assign(nodes, { strategy: 'balanced' }); + + expect(assignments).toEqual([ + expect.objectContaining({ + provider: 'atlascloud', + model: 'qwen/qwen3.5-flash', + }), + expect.objectContaining({ + provider: 'atlascloud', + model: 'deepseek-ai/deepseek-v4-pro', + }), + ]); + }); }); describe('explicit strategy', () => { diff --git a/src/orchestration/pipeline/query/QueryRouter.ts b/src/orchestration/pipeline/query/QueryRouter.ts index 4baf4aa5265..ef2c3538b1b 100644 --- a/src/orchestration/pipeline/query/QueryRouter.ts +++ b/src/orchestration/pipeline/query/QueryRouter.ts @@ -1561,12 +1561,14 @@ export class QueryRouter { /** * Resolve API key for embedding calls. - * Falls back through embedding-specific → global → env var scan. + * Falls back through embedding-specific → global → embedding-capable env vars. */ private getEmbeddingApiKey(): string { if (this.config.embeddingApiKey) return this.config.embeddingApiKey; - // Fall through to general LLM key resolver - return this.getLlmApiKey(); + if (this.config.apiKey) return this.config.apiKey; + if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY; + if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY; + return ''; } private getEmbeddingBaseUrl(): string | undefined { @@ -1613,6 +1615,7 @@ export class QueryRouter { // both are configured. const envKeys = [ 'OPENAI_API_KEY', + 'ATLASCLOUD_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY', @@ -1650,6 +1653,7 @@ export class QueryRouter { // to OpenRouter when no direct key is present. const directKeys = [ 'OPENAI_API_KEY', + 'ATLASCLOUD_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY', diff --git a/src/orchestration/pipeline/query/__tests__/QueryRouter.test.ts b/src/orchestration/pipeline/query/__tests__/QueryRouter.test.ts index ba698b3e3e3..f3666daf187 100644 --- a/src/orchestration/pipeline/query/__tests__/QueryRouter.test.ts +++ b/src/orchestration/pipeline/query/__tests__/QueryRouter.test.ts @@ -173,6 +173,7 @@ describe('QueryRouter', () => { mockGenerateText.mockReset(); delete process.env.OPENAI_API_KEY; delete process.env.OPENROUTER_API_KEY; + delete process.env.ATLASCLOUD_API_KEY; mockExistsSync.mockReturnValue(true); mockReaddirSync.mockReturnValue([ { name: 'pricing.md', isDirectory: () => false, isFile: () => true }, @@ -240,6 +241,59 @@ describe('QueryRouter', () => { expect((router as any).getEmbeddingBaseUrl()).toBeUndefined(); }); + it('resolves Atlas Cloud credentials without overriding its provider endpoint', () => { + process.env.ATLASCLOUD_API_KEY = 'test-atlascloud-key'; + + const router = createRouter({ + classifierProvider: 'atlascloud', + generationProvider: 'atlascloud', + }); + + expect((router as any).getLlmApiKey()).toBe('test-atlascloud-key'); + expect((router as any).getLlmBaseUrl()).toBeUndefined(); + expect((router as any).getEmbeddingApiKey()).toBe(''); + expect((router as any).getEmbeddingBaseUrl()).toBeUndefined(); + }); + + it('uses Atlas Cloud models in QueryRouter defaults when it is the only configured provider', async () => { + const providerEnvKeys = [ + 'OPENAI_API_KEY', + 'ATLASCLOUD_API_KEY', + 'ANTHROPIC_API_KEY', + 'GEMINI_API_KEY', + 'GROQ_API_KEY', + 'TOGETHER_API_KEY', + 'MISTRAL_API_KEY', + 'XAI_API_KEY', + 'OPENROUTER_API_KEY', + ]; + const originalEnv = new Map(providerEnvKeys.map((key) => [key, process.env[key]])); + + for (const key of providerEnvKeys) delete process.env[key]; + process.env.ATLASCLOUD_API_KEY = 'test-atlascloud-key'; + vi.resetModules(); + + try { + const { DEFAULT_QUERY_ROUTER_CONFIG } = await import('../types.js'); + + expect(DEFAULT_QUERY_ROUTER_CONFIG).toMatchObject({ + classifierProvider: 'atlascloud', + classifierModel: 'qwen/qwen3.5-flash', + embeddingProvider: 'openai', + generationProvider: 'atlascloud', + generationModel: 'qwen/qwen3.5-flash', + generationModelDeep: 'deepseek-ai/deepseek-v4-pro', + }); + } finally { + for (const key of providerEnvKeys) { + const value = originalEnv.get(key); + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + vi.resetModules(); + } + }); + // ------------------------------------------------------------------------- // Test 1: classify() returns a ClassificationResult // ------------------------------------------------------------------------- diff --git a/src/orchestration/pipeline/query/__tests__/platform-knowledge.test.ts b/src/orchestration/pipeline/query/__tests__/platform-knowledge.test.ts index 242195b188d..c68aed60e7d 100644 --- a/src/orchestration/pipeline/query/__tests__/platform-knowledge.test.ts +++ b/src/orchestration/pipeline/query/__tests__/platform-knowledge.test.ts @@ -34,7 +34,7 @@ const PLATFORM_CORPUS_FIXTURE = JSON.stringify([ id: 'api:generateText', heading: 'generateText() API', content: - "import { generateText } from '@framers/agentos'. Core text generation function supporting all 11 LLM providers.", + "import { generateText } from '@framers/agentos'. Core text generation function supporting all 12 LLM providers.", category: 'api', }, { diff --git a/src/orchestration/pipeline/query/types.ts b/src/orchestration/pipeline/query/types.ts index 5c0bb7eaa48..648ba79184a 100644 --- a/src/orchestration/pipeline/query/types.ts +++ b/src/orchestration/pipeline/query/types.ts @@ -788,6 +788,7 @@ function resolveDefaultProvider(): string { try { const envMap: ReadonlyArray = [ ['OPENAI_API_KEY', 'openai'], + ['ATLASCLOUD_API_KEY', 'atlascloud'], ['ANTHROPIC_API_KEY', 'anthropic'], ['GEMINI_API_KEY', 'gemini'], ['GROQ_API_KEY', 'groq'], @@ -805,11 +806,27 @@ function resolveDefaultProvider(): string { return 'openai'; } +/** + * Resolve embeddings independently because not every text provider supports + * the OpenAI embeddings API. Atlas-only configurations intentionally fall + * back to `openai` without a key so QueryRouter uses keyword retrieval. + */ +function resolveDefaultEmbeddingProvider(): string { + try { + if (process.env.OPENAI_API_KEY) return 'openai'; + if (process.env.OPENROUTER_API_KEY) return 'openrouter'; + } catch { + // Browser or restricted env — fall back + } + return 'openai'; +} + /** Provider → cheap model mapping for classifier/T0-T1 generation. */ const CHEAP_MODELS: Record = { openai: 'gpt-4o-mini', anthropic: 'claude-haiku-4-5-20251001', openrouter: 'openai/gpt-4o-mini', + atlascloud: 'qwen/qwen3.5-flash', gemini: 'gemini-2.0-flash', groq: 'gemma2-9b-it', together: 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo', @@ -825,6 +842,7 @@ const STRONG_MODELS: Record = { openai: 'gpt-4o', anthropic: 'claude-sonnet-4-6', openrouter: 'openai/gpt-4o', + atlascloud: 'deepseek-ai/deepseek-v4-pro', gemini: 'gemini-2.5-flash', groq: 'llama-3.3-70b-versatile', together: 'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo', @@ -840,7 +858,7 @@ export const DEFAULT_QUERY_ROUTER_CONFIG = { classifierModel: CHEAP_MODELS[resolveDefaultProvider()] ?? 'gpt-4o-mini', classifierProvider: resolveDefaultProvider(), maxTier: 3 as QueryTier, - embeddingProvider: resolveDefaultProvider(), + embeddingProvider: resolveDefaultEmbeddingProvider(), embeddingModel: 'text-embedding-3-small', generationModel: CHEAP_MODELS[resolveDefaultProvider()] ?? 'gpt-4o-mini', generationModelDeep: STRONG_MODELS[resolveDefaultProvider()] ?? 'gpt-4o', diff --git a/src/orchestration/planning/ProviderAssignmentEngine.ts b/src/orchestration/planning/ProviderAssignmentEngine.ts index e242b496bed..416131b4f79 100644 --- a/src/orchestration/planning/ProviderAssignmentEngine.ts +++ b/src/orchestration/planning/ProviderAssignmentEngine.ts @@ -23,6 +23,7 @@ const DEFAULTS: Record = { gemini: { text: 'gemini-2.5-flash', cheap: 'gemini-2.0-flash' }, ollama: { text: 'llama3.2', cheap: 'llama3.2' }, openrouter: { text: 'openai/gpt-4o', cheap: 'openai/gpt-4o-mini' }, + atlascloud: { text: 'deepseek-ai/deepseek-v4-pro', cheap: 'qwen/qwen3.5-flash' }, groq: { text: 'llama-3.3-70b-versatile', cheap: 'gemma2-9b-it' }, together: { text: 'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo', @@ -90,7 +91,7 @@ export class ProviderAssignmentEngine { private assignBest(node: AnnotatedNode): NodeProviderAssignment { const provider = this.pickProvider( 'best', - ['anthropic', 'openai', 'openrouter', 'gemini', 'groq', 'xai', 'mistral', 'together', 'ollama'], + ['anthropic', 'openai', 'openrouter', 'atlascloud', 'gemini', 'groq', 'xai', 'mistral', 'together', 'ollama'], ); const defaults = DEFAULTS[provider]; return { @@ -105,7 +106,7 @@ export class ProviderAssignmentEngine { private assignCheapest(node: AnnotatedNode): NodeProviderAssignment { const provider = this.pickProvider( 'cheapest', - ['groq', 'gemini', 'openai', 'openrouter', 'together', 'mistral', 'xai', 'anthropic', 'ollama'], + ['groq', 'gemini', 'atlascloud', 'openai', 'openrouter', 'together', 'mistral', 'xai', 'anthropic', 'ollama'], ); const defaults = DEFAULTS[provider]; return { @@ -129,16 +130,16 @@ export class ProviderAssignmentEngine { tier === 'cheap' ? this.pickProvider( 'balanced:cheap', - ['groq', 'gemini', 'openai', 'openrouter', 'together', 'mistral', 'xai', 'anthropic', 'ollama'], + ['groq', 'gemini', 'atlascloud', 'openai', 'openrouter', 'together', 'mistral', 'xai', 'anthropic', 'ollama'], ) : tier === 'strong' ? this.pickProvider( 'balanced:strong', - ['anthropic', 'openai', 'openrouter', 'gemini', 'groq', 'xai', 'mistral', 'together', 'ollama'], + ['anthropic', 'openai', 'openrouter', 'atlascloud', 'gemini', 'groq', 'xai', 'mistral', 'together', 'ollama'], ) : this.pickProvider( 'balanced:standard', - ['openai', 'openrouter', 'anthropic', 'gemini', 'groq', 'mistral', 'xai', 'together', 'ollama'], + ['openai', 'openrouter', 'atlascloud', 'anthropic', 'gemini', 'groq', 'mistral', 'xai', 'together', 'ollama'], ); const defaults = DEFAULTS[provider]; diff --git a/tests/api/model.spec.ts b/tests/api/model.spec.ts index f69c6d5b65c..b36effb9c8a 100644 --- a/tests/api/model.spec.ts +++ b/tests/api/model.spec.ts @@ -57,6 +57,16 @@ describe('resolveProvider', () => { expect(config.baseUrl).toBe('http://localhost:11434'); }); + it('resolves atlascloud from ATLASCLOUD_API_KEY and ATLASCLOUD_BASE_URL', () => { + process.env.ATLASCLOUD_API_KEY = 'atlas-test'; + process.env.ATLASCLOUD_BASE_URL = 'https://proxy.example.com/v1'; + const config = resolveProvider('atlascloud', 'deepseek-ai/deepseek-v4-pro'); + expect(config.providerId).toBe('atlascloud'); + expect(config.apiKey).toBe('atlas-test'); + expect(config.baseUrl).toBe('https://proxy.example.com/v1'); + expect(config.modelId).toBe('deepseek-ai/deepseek-v4-pro'); + }); + it('throws when no API key found', () => { delete process.env.OPENAI_API_KEY; expect(() => resolveProvider('openai', 'gpt-4o')).toThrow('No API key'); diff --git a/tests/e2e/platform-knowledge.e2e.spec.ts b/tests/e2e/platform-knowledge.e2e.spec.ts index 969c79d7289..0f91bcd4b71 100644 --- a/tests/e2e/platform-knowledge.e2e.spec.ts +++ b/tests/e2e/platform-knowledge.e2e.spec.ts @@ -182,7 +182,7 @@ describe('Platform Knowledge — e2e', () => { // Generator mock mockGenerateText.mockResolvedValueOnce({ - text: 'AgentOS supports 11 LLM providers including OpenAI, Anthropic, Google, and Ollama.', + text: 'AgentOS supports 12 LLM providers including OpenAI, Anthropic, Google, Atlas Cloud, and Ollama.', provider: 'openai', model: 'gpt-4o-mini', usage: { promptTokens: 150, completionTokens: 40, totalTokens: 190 },