diff --git a/.env.example b/.env.example index e857292..a4210ec 100755 --- a/.env.example +++ b/.env.example @@ -20,7 +20,7 @@ db_mode=json ######################################## # LLM Core Settings ######################################## -LLM_PROVIDER=gemini # gemini | openai | claude | grok | ollama | openrouter | requesty | cheaperinference | minimax | apiroute +LLM_PROVIDER=gemini # gemini | openai | claude | grok | ollama | openrouter | requesty | cheaperinference | atlascloud | minimax | apiroute EMB_PROVIDER=openai # openai | gemini | ollama etc. LLM_TEMP=1 LLM_MAXTOK=16384 @@ -74,6 +74,16 @@ CHEAPER_INFERENCE_API_KEY= CHEAPER_INFERENCE_MODEL=gpt-5.4-mini CHEAPER_INFERENCE_BASE_URL="https://api.cheaperinference.com/v1" +######################################## +# Atlas Cloud (https://atlascloud.ai) +######################################## +# Get a key at https://atlascloud.ai -- the model catalog at +# https://api.atlascloud.ai/v1/models is public and needs no key. +# Embeddings use the OpenAI settings above +ATLASCLOUD_API_KEY= +ATLASCLOUD_MODEL=deepseek-ai/deepseek-v4-flash +ATLASCLOUD_BASE_URL="https://api.atlascloud.ai/v1" + ######################################## # Grok (xAI) ######################################## diff --git a/README.md b/README.md index 11c83da..ccfb1cd 100755 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ The platform provides a modern interface for students, educators, and researcher ### Supported AI Models -- Google Gemini • OpenAI GPT • Anthropic Claude • xAI Grok • [MiniMax](https://www.minimax.io/) • [API Route](https://www.api-route.com) • Ollama (local) • OpenRouter • [Requesty](https://requesty.ai) • [Cheaper Inference](https://cheaperinference.com) +- Google Gemini • OpenAI GPT • Anthropic Claude • xAI Grok • [MiniMax](https://www.minimax.io/) • [API Route](https://www.api-route.com) • Ollama (local) • OpenRouter • [Requesty](https://requesty.ai) • [Cheaper Inference](https://cheaperinference.com) • [Atlas Cloud](https://atlascloud.ai) ### Embedding Providers diff --git a/backend/src/config/env.ts b/backend/src/config/env.ts index bfd8542..925687a 100755 --- a/backend/src/config/env.ts +++ b/backend/src/config/env.ts @@ -17,6 +17,9 @@ export const config = { cheaperinference: process.env.CHEAPER_INFERENCE_API_KEY || '', cheaperinference_model: process.env.CHEAPER_INFERENCE_MODEL || 'gpt-5.4-mini', cheaperinference_base: process.env.CHEAPER_INFERENCE_BASE_URL || 'https://api.cheaperinference.com/v1', + atlascloud: process.env.ATLASCLOUD_API_KEY || '', + atlascloud_model: process.env.ATLASCLOUD_MODEL || 'deepseek-ai/deepseek-v4-flash', + atlascloud_base: process.env.ATLASCLOUD_BASE_URL || 'https://api.atlascloud.ai/v1', gemini: process.env.gemini || process.env.GOOGLE_API_KEY || '', gemini_model: process.env.gemini_model || 'gemini-1.5-pro', gemini_embed_model: process.env.gemini_embed_model || 'text-embedding-004', diff --git a/backend/src/utils/llm/models/__tests__/atlascloud.test.ts b/backend/src/utils/llm/models/__tests__/atlascloud.test.ts new file mode 100644 index 0000000..a5ee858 --- /dev/null +++ b/backend/src/utils/llm/models/__tests__/atlascloud.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +vi.mock('@langchain/openai', () => { + const MockChatOpenAI = vi.fn(function (this: any, opts: any) { + this.invoke = vi.fn().mockResolvedValue({ content: 'mock response' }) + this._opts = opts + }) + const MockOpenAIEmbeddings = vi.fn(function (this: any, opts: any) { + this.embedDocuments = vi.fn().mockResolvedValue([[0.1, 0.2]]) + this.embedQuery = vi.fn().mockResolvedValue([0.1, 0.2]) + this._opts = opts + }) + return { ChatOpenAI: MockChatOpenAI, OpenAIEmbeddings: MockOpenAIEmbeddings } +}) + +import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai' +import { makeLLM, makeEmbeddings } from '../atlascloud' + +describe('Atlas Cloud LLM provider', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should create a ChatOpenAI instance with Atlas Cloud defaults', () => { + const llm = makeLLM({ atlascloud: 'test-key' }) + + expect(ChatOpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'deepseek-ai/deepseek-v4-flash', + apiKey: 'test-key', + configuration: { baseURL: 'https://api.atlascloud.ai/v1' }, + }), + ) + expect(typeof llm.invoke).toBe('function') + expect(typeof llm.call).toBe('function') + }) + + it('should use configured model and base URL', () => { + makeLLM({ + atlascloud: 'key', + atlascloud_model: 'zai-org/glm-5.3-flash', + atlascloud_base: 'https://gateway.example.test/v1', + }) + + expect(ChatOpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'zai-org/glm-5.3-flash', + configuration: { baseURL: 'https://gateway.example.test/v1' }, + }), + ) + }) + + it('should pass temperature and max tokens through', () => { + makeLLM({ atlascloud: 'key', temp: 0.2, max_tokens: 1024 }) + + expect(ChatOpenAI).toHaveBeenCalledWith( + expect.objectContaining({ temperature: 0.2, maxTokens: 1024 }), + ) + }) + + it('should create embeddings with the OpenAI settings', () => { + makeEmbeddings({ openai: 'openai-key' }) + + expect(OpenAIEmbeddings).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'text-embedding-3-large', + apiKey: 'openai-key', + }), + ) + }) +}) diff --git a/backend/src/utils/llm/models/atlascloud.ts b/backend/src/utils/llm/models/atlascloud.ts new file mode 100644 index 0000000..ff702f5 --- /dev/null +++ b/backend/src/utils/llm/models/atlascloud.ts @@ -0,0 +1,24 @@ +import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai' +import { wrapChat } from './util' +import type { MkLLM, MkEmb, EmbeddingsLike } from './types' + +export const makeLLM: MkLLM = (cfg: any) => { + const m = new ChatOpenAI({ + model: cfg.atlascloud_model || 'deepseek-ai/deepseek-v4-flash', + apiKey: cfg.atlascloud || '', + configuration: { baseURL: cfg.atlascloud_base || 'https://api.atlascloud.ai/v1' }, + temperature: cfg.temp ?? 0.7, + maxTokens: cfg.max_tokens, + }) + return wrapChat(m) +} + +// Atlas Cloud serves chat completions only -- it has no /v1/embeddings +// endpoint -- so embeddings fall back to the OpenAI settings, the same way +// the other chat-only gateways here do. +export const makeEmbeddings: MkEmb = (cfg: any): EmbeddingsLike => { + return new OpenAIEmbeddings({ + model: cfg.openai_embed_model || 'text-embedding-3-large', + apiKey: cfg.openai || process.env.OPENAI_API_KEY, + }) +} diff --git a/backend/src/utils/llm/models/index.ts b/backend/src/utils/llm/models/index.ts index fdd18de..1511e3d 100755 --- a/backend/src/utils/llm/models/index.ts +++ b/backend/src/utils/llm/models/index.ts @@ -6,6 +6,7 @@ import * as claude from './claude' import * as openrouter from './openrouter' import * as requesty from './requesty' import * as cheaperinference from './cheaperinference' +import * as atlascloud from './atlascloud' import * as minimax from './minimax' import * as apiroute from './apiroute' import { config } from '../../../config/env' @@ -23,6 +24,7 @@ function pick(p: string) { case 'openrouter': return openrouter case 'requesty': return requesty case 'cheaperinference': return cheaperinference + case 'atlascloud': return atlascloud case 'minimax': return minimax case 'apiroute': case 'api_route':