Merge master into feature/openrouter-support
This commit is contained in:
@@ -31,12 +31,12 @@ REFRESH_INTERVAL_MINUTES=15
|
||||
|
||||
# === LLM Layer (optional) ===
|
||||
# Enables AI-enhanced trade ideas and breaking news Telegram alerts.
|
||||
# Provider options: anthropic | openai | gemini | codex | openrouter
|
||||
# Provider options: anthropic | openai | gemini | codex | openrouter | minimax
|
||||
LLM_PROVIDER=
|
||||
# Not needed for codex (uses ~/.codex/auth.json)
|
||||
LLM_API_KEY=
|
||||
# Optional override. Each provider has a sensible default:
|
||||
# anthropic: claude-sonnet-4-6 | openai: gpt-5.4 | gemini: gemini-3.1-pro | codex: gpt-5.3-codex | openrouter: openrouter/auto
|
||||
# anthropic: claude-sonnet-4-6 | openai: gpt-5.4 | gemini: gemini-3.1-pro | codex: gpt-5.3-codex | openrouter: openrouter/auto | minimax: MiniMax-M2.5
|
||||
LLM_MODEL=
|
||||
|
||||
# === Telegram Alerts (optional, requires LLM) ===
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,3 +45,6 @@ npm-debug.log*
|
||||
|
||||
# Local maintainer notes
|
||||
MAINTAINER_DECISIONS.local.md
|
||||
|
||||
# Local deploy config
|
||||
dashboard/public/vercel.json
|
||||
|
||||
24
README.md
24
README.md
@@ -4,6 +4,11 @@
|
||||
|
||||
**Your own intelligence terminal. 27 sources. One command. Zero cloud.**
|
||||
|
||||
## [Visit The Live Site: crucix.live](https://www.crucix.live/)
|
||||
|
||||
[](https://www.crucix.live/)
|
||||
[](https://www.crucix.live/)
|
||||
|
||||
[](#quick-start)
|
||||
[](LICENSE)
|
||||
[-orange)](#architecture)
|
||||
@@ -27,10 +32,15 @@
|
||||
|
||||
</div>
|
||||
|
||||
> **Live website:** [https://www.crucix.live/](https://www.crucix.live/)
|
||||
> Explore the public demo first, then clone the repo to run Crucix locally.
|
||||
|
||||
Crucix pulls satellite fire detection, flight tracking, radiation monitoring, satellite constellation tracking, economic indicators, live market prices, conflict data, sanctions lists, and social sentiment from 27 open-source intelligence feeds — in parallel, every 15 minutes — and renders everything on a single self-contained Jarvis-style dashboard.
|
||||
|
||||
Hook it up to an LLM and it becomes a **two-way intelligence assistant** — pushing multi-tier alerts to Telegram and Discord when something meaningful changes, responding to commands like `/brief` and `/sweep` from your phone, and generating actionable trade ideas grounded in real cross-domain data. Your own analyst that watches the world while you sleep.
|
||||
|
||||
Try the live demo first at [https://www.crucix.live/](https://www.crucix.live/), then clone the repo when you want the full local stack.
|
||||
|
||||
No cloud. No telemetry. No subscriptions. Just `node server.mjs` and you're running.
|
||||
|
||||
---
|
||||
@@ -50,7 +60,7 @@ It was built for anyone who wants to understand what's actually happening in the
|
||||
```bash
|
||||
# 1. Clone the repo
|
||||
git clone https://github.com/calesthio/Crucix.git
|
||||
cd crucix
|
||||
cd Crucix
|
||||
|
||||
# 2. Install dependencies (just Express)
|
||||
npm install
|
||||
@@ -76,7 +86,7 @@ The dashboard opens automatically at `http://localhost:3117` and immediately beg
|
||||
|
||||
```bash
|
||||
git clone https://github.com/calesthio/Crucix.git
|
||||
cd crucix
|
||||
cd Crucix
|
||||
cp .env.example .env # add your API keys
|
||||
docker compose up -d
|
||||
```
|
||||
@@ -148,10 +158,10 @@ Alerts are delivered as rich embeds with color-coded sidebars: red for FLASH, ye
|
||||
**Optional dependency:** The full bot requires `discord.js`. Install it with `npm install discord.js`. If it's not installed, Crucix automatically falls back to webhook-only mode.
|
||||
|
||||
### Optional LLM Layer
|
||||
Connect any of 5 LLM providers for enhanced analysis:
|
||||
Connect any of 6 LLM providers for enhanced analysis:
|
||||
- **AI trade ideas** — quantitative analyst producing 5-8 actionable ideas citing specific data
|
||||
- **Smarter alert evaluation** — LLM classifies signals into FLASH/PRIORITY/ROUTINE tiers with cross-domain correlation and confidence scoring
|
||||
- Providers: Anthropic Claude, OpenAI, Google Gemini, OpenRouter (Unified API), OpenAI Codex (ChatGPT subscription)
|
||||
- Providers: Anthropic Claude, OpenAI, Google Gemini, OpenRouter (Unified API), OpenAI Codex (ChatGPT subscription), MiniMax
|
||||
- Graceful fallback — when LLM is unavailable, a rule-based engine takes over alert evaluation. LLM failures never crash the sweep cycle.
|
||||
|
||||
---
|
||||
@@ -184,7 +194,7 @@ These three unlock the most valuable economic and satellite data. Each takes abo
|
||||
|
||||
### LLM Provider (optional, for AI-enhanced ideas)
|
||||
|
||||
Set `LLM_PROVIDER` to one of: `anthropic`, `openai`, `gemini`, `codex`
|
||||
Set `LLM_PROVIDER` to one of: `anthropic`, `openai`, `gemini`, `codex`, `openrouter`, `minimax`
|
||||
|
||||
| Provider | Key Required | Default Model |
|
||||
|----------|-------------|---------------|
|
||||
@@ -193,6 +203,7 @@ Set `LLM_PROVIDER` to one of: `anthropic`, `openai`, `gemini`, `codex`
|
||||
| `gemini` | `LLM_API_KEY` | gemini-3.1-pro |
|
||||
| `openrouter` | `LLM_API_KEY` | openrouter/auto |
|
||||
| `codex` | None (uses `~/.codex/auth.json`) | gpt-5.3-codex |
|
||||
| `minimax` | `LLM_API_KEY` | MiniMax-M2.5 |
|
||||
|
||||
For Codex, run `npx @openai/codex login` to authenticate via your ChatGPT subscription.
|
||||
|
||||
@@ -269,6 +280,7 @@ crucix/
|
||||
│ │ ├── gemini.mjs # Gemini
|
||||
│ │ ├── openrouter.mjs # OpenRouter (Unified API)
|
||||
│ │ ├── codex.mjs # Codex (ChatGPT subscription)
|
||||
│ │ ├── minimax.mjs # MiniMax (M2.5, 204K context)
|
||||
│ │ ├── ideas.mjs # LLM-powered trade idea generation
|
||||
│ │ └── index.mjs # Factory: createLLMProvider()
|
||||
│ ├── delta/ # Change tracking between sweeps
|
||||
@@ -370,7 +382,7 @@ All settings are in `.env` with sensible defaults:
|
||||
|----------|---------|-------------|
|
||||
| `PORT` | `3117` | Dashboard server port |
|
||||
| `REFRESH_INTERVAL_MINUTES` | `15` | Auto-refresh interval |
|
||||
| `LLM_PROVIDER` | disabled | `anthropic`, `openai`, `gemini`, `codex`, or `openrouter` |
|
||||
| `LLM_PROVIDER` | disabled | `anthropic`, `openai`, `gemini`, `codex`, `openrouter`, or `minimax` |
|
||||
| `LLM_API_KEY` | — | API key (not needed for codex) |
|
||||
| `LLM_MODEL` | per-provider default | Override model selection |
|
||||
| `TELEGRAM_BOT_TOKEN` | disabled | For Telegram alerts + bot commands |
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
refreshIntervalMinutes: parseInt(process.env.REFRESH_INTERVAL_MINUTES) || 15,
|
||||
|
||||
llm: {
|
||||
provider: process.env.LLM_PROVIDER || null, // anthropic | openai | gemini | codex | openrouter
|
||||
provider: process.env.LLM_PROVIDER || null, // anthropic | openai | gemini | codex | openrouter | minimax
|
||||
apiKey: process.env.LLM_API_KEY || null,
|
||||
model: process.env.LLM_MODEL || null,
|
||||
},
|
||||
|
||||
@@ -9,6 +9,9 @@ import { readFileSync, writeFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { exec } from 'child_process';
|
||||
import config from '../crucix.config.mjs';
|
||||
import { createLLMProvider } from '../lib/llm/index.mjs';
|
||||
import { generateLLMIdeas } from '../lib/llm/ideas.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, '..');
|
||||
@@ -511,11 +514,42 @@ function buildNewsFeed(rssNews, gdeltData, tgUrgent, tgTop) {
|
||||
}
|
||||
|
||||
// === CLI Mode: inject into HTML file ===
|
||||
function getCliArg(flag) {
|
||||
const idx = process.argv.indexOf(flag);
|
||||
return idx >= 0 ? process.argv[idx + 1] : null;
|
||||
}
|
||||
|
||||
async function cliInject() {
|
||||
const data = JSON.parse(readFileSync(join(ROOT, 'runs/latest.json'), 'utf8'));
|
||||
const htmlOverride = getCliArg('--html');
|
||||
const shouldOpen = !process.argv.includes('--no-open');
|
||||
|
||||
console.log('Fetching RSS news feeds...');
|
||||
const V2 = await synthesize(data);
|
||||
const llmProvider = createLLMProvider(config.llm);
|
||||
|
||||
if (llmProvider?.isConfigured) {
|
||||
try {
|
||||
console.log(`[LLM] Generating ideas via ${llmProvider.name}...`);
|
||||
const llmIdeas = await generateLLMIdeas(llmProvider, V2, null, []);
|
||||
if (llmIdeas?.length) {
|
||||
V2.ideas = llmIdeas;
|
||||
V2.ideasSource = 'llm';
|
||||
console.log(`[LLM] Generated ${llmIdeas.length} ideas`);
|
||||
} else {
|
||||
V2.ideas = [];
|
||||
V2.ideasSource = 'llm-failed';
|
||||
console.log('[LLM] No ideas returned');
|
||||
}
|
||||
} catch (err) {
|
||||
V2.ideas = [];
|
||||
V2.ideasSource = 'llm-failed';
|
||||
console.log('[LLM] Idea generation failed:', err.message);
|
||||
}
|
||||
} else {
|
||||
V2.ideas = [];
|
||||
V2.ideasSource = 'disabled';
|
||||
}
|
||||
console.log(`Generated ${V2.ideas.length} leverageable ideas`);
|
||||
|
||||
const json = JSON.stringify(V2);
|
||||
@@ -523,12 +557,15 @@ async function cliInject() {
|
||||
console.log('Size:', json.length, 'bytes | Air:', V2.air.length, '| Thermal:', V2.thermal.length,
|
||||
'| News:', V2.news.length, '| Ideas:', V2.ideas.length, '| Sources:', V2.health.length);
|
||||
|
||||
const htmlPath = join(ROOT, 'dashboard/public/jarvis.html');
|
||||
const htmlPath = htmlOverride || join(ROOT, 'dashboard/public/jarvis.html');
|
||||
let html = readFileSync(htmlPath, 'utf8');
|
||||
html = html.replace(/^(let|const) D = .*;\s*$/m, 'let D = ' + json + ';');
|
||||
// Use a replacer function so JSON is inserted literally even if it contains `$`.
|
||||
html = html.replace(/^(let|const) D = .*;\s*$/m, () => 'let D = ' + json + ';');
|
||||
writeFileSync(htmlPath, html);
|
||||
console.log('Data injected into jarvis.html!');
|
||||
|
||||
if (!shouldOpen) return;
|
||||
|
||||
// Auto-open dashboard in default browser
|
||||
// NOTE: On Windows, `start` in PowerShell is an alias for Start-Service, not cmd's start.
|
||||
// We must use `cmd /c start ""` to ensure it works in both cmd.exe and PowerShell.
|
||||
@@ -542,7 +579,8 @@ async function cliInject() {
|
||||
}
|
||||
|
||||
// Run CLI if invoked directly
|
||||
const isMain = process.argv[1] && fileURLToPath(import.meta.url).includes(process.argv[1].replace(/\\/g, '/'));
|
||||
const isMain = process.argv[1]
|
||||
&& fileURLToPath(import.meta.url).replace(/\\/g, '/') === process.argv[1].replace(/\\/g, '/');
|
||||
if (isMain) {
|
||||
cliInject();
|
||||
await cliInject();
|
||||
}
|
||||
|
||||
@@ -1202,9 +1202,10 @@ function init(){
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const isServer = location.protocol !== 'file:';
|
||||
const hasInlineData = !!(D && D.meta);
|
||||
const canProbeApi = location.protocol !== 'file:';
|
||||
|
||||
if (isServer) {
|
||||
if (canProbeApi && !hasInlineData) {
|
||||
// Server mode: always fetch live data from API (ignore any stale inline D)
|
||||
fetch('/api/data')
|
||||
.then(r => r.json())
|
||||
@@ -1213,7 +1214,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Should not reach here — server routes to loading.html when no data
|
||||
if (D && D.meta) { init(); connectSSE(); }
|
||||
});
|
||||
} else if (D && D.meta) {
|
||||
} else if (hasInlineData) {
|
||||
// File mode: use inline data
|
||||
init();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
crucix:
|
||||
build: .
|
||||
|
||||
@@ -5,6 +5,7 @@ import { OpenAIProvider } from './openai.mjs';
|
||||
import { OpenRouterProvider } from './openrouter.mjs';
|
||||
import { GeminiProvider } from './gemini.mjs';
|
||||
import { CodexProvider } from './codex.mjs';
|
||||
import { MiniMaxProvider } from './minimax.mjs';
|
||||
|
||||
export { LLMProvider } from './provider.mjs';
|
||||
export { AnthropicProvider } from './anthropic.mjs';
|
||||
@@ -12,6 +13,7 @@ export { OpenAIProvider } from './openai.mjs';
|
||||
export { OpenRouterProvider } from './openrouter.mjs';
|
||||
export { GeminiProvider } from './gemini.mjs';
|
||||
export { CodexProvider } from './codex.mjs';
|
||||
export { MiniMaxProvider } from './minimax.mjs';
|
||||
|
||||
/**
|
||||
* Create an LLM provider based on config.
|
||||
@@ -34,6 +36,8 @@ export function createLLMProvider(llmConfig) {
|
||||
return new GeminiProvider({ apiKey, model });
|
||||
case 'codex':
|
||||
return new CodexProvider({ model });
|
||||
case 'minimax':
|
||||
return new MiniMaxProvider({ apiKey, model });
|
||||
default:
|
||||
console.warn(`[LLM] Unknown provider "${provider}". LLM features disabled.`);
|
||||
return null;
|
||||
|
||||
51
lib/llm/minimax.mjs
Normal file
51
lib/llm/minimax.mjs
Normal file
@@ -0,0 +1,51 @@
|
||||
// MiniMax Provider — raw fetch, no SDK
|
||||
// Uses MiniMax's OpenAI-compatible Chat Completions API
|
||||
|
||||
import { LLMProvider } from './provider.mjs';
|
||||
|
||||
export class MiniMaxProvider extends LLMProvider {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
this.name = 'minimax';
|
||||
this.apiKey = config.apiKey;
|
||||
this.model = config.model || 'MiniMax-M2.5';
|
||||
}
|
||||
|
||||
get isConfigured() { return !!this.apiKey; }
|
||||
|
||||
async complete(systemPrompt, userMessage, opts = {}) {
|
||||
const res = await fetch('https://api.minimax.io/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: this.model,
|
||||
max_tokens: opts.maxTokens || 4096,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userMessage },
|
||||
],
|
||||
}),
|
||||
signal: AbortSignal.timeout(opts.timeout || 60000),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text().catch(() => '');
|
||||
throw new Error(`MiniMax API ${res.status}: ${err.substring(0, 200)}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const text = data.choices?.[0]?.message?.content || '';
|
||||
|
||||
return {
|
||||
text,
|
||||
usage: {
|
||||
inputTokens: data.usage?.prompt_tokens || 0,
|
||||
outputTokens: data.usage?.completion_tokens || 0,
|
||||
},
|
||||
model: data.model || this.model,
|
||||
};
|
||||
}
|
||||
}
|
||||
332
package-lock.json
generated
332
package-lock.json
generated
@@ -7,12 +7,221 @@
|
||||
"": {
|
||||
"name": "crucix",
|
||||
"version": "2.0.0",
|
||||
"license": "ISC",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"express": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
"node": ">=22",
|
||||
"npm": ">=10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"discord.js": "^14.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/builders": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz",
|
||||
"integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@discordjs/formatters": "^0.6.2",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@sapphire/shapeshift": "^4.0.0",
|
||||
"discord-api-types": "^0.38.33",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"ts-mixer": "^6.0.4",
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/collection": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
|
||||
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/formatters": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
|
||||
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/rest": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz",
|
||||
"integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^2.1.1",
|
||||
"@discordjs/util": "^1.1.1",
|
||||
"@sapphire/async-queue": "^1.5.3",
|
||||
"@sapphire/snowflake": "^3.5.3",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"discord-api-types": "^0.38.16",
|
||||
"magic-bytes.js": "^1.10.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "6.21.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/util": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
|
||||
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/ws": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
|
||||
"integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^2.1.0",
|
||||
"@discordjs/rest": "^2.5.1",
|
||||
"@discordjs/util": "^1.1.0",
|
||||
"@sapphire/async-queue": "^1.5.2",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@vladfrangu/async_event_emitter": "^2.2.4",
|
||||
"discord-api-types": "^0.38.1",
|
||||
"tslib": "^2.6.2",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/async-queue": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
||||
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/shapeshift": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
|
||||
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v16"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/snowflake": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
|
||||
"integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
|
||||
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vladfrangu/async_event_emitter": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
|
||||
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
@@ -156,6 +365,44 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/discord-api-types": {
|
||||
"version": "0.38.42",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz",
|
||||
"integrity": "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"workspaces": [
|
||||
"scripts/actions/documentation"
|
||||
]
|
||||
},
|
||||
"node_modules/discord.js": {
|
||||
"version": "14.25.1",
|
||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz",
|
||||
"integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^1.13.0",
|
||||
"@discordjs/collection": "1.5.3",
|
||||
"@discordjs/formatters": "^0.6.2",
|
||||
"@discordjs/rest": "^2.6.0",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@discordjs/ws": "^1.2.3",
|
||||
"@sapphire/snowflake": "3.5.3",
|
||||
"discord-api-types": "^0.38.33",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"magic-bytes.js": "^1.10.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "6.21.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -273,6 +520,13 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
@@ -451,6 +705,27 @@
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/lodash.snakecase": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
||||
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/magic-bytes.js": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
|
||||
"integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -788,6 +1063,20 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-mixer": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
|
||||
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||
@@ -802,6 +1091,23 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.24.4",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz",
|
||||
"integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
@@ -825,6 +1131,28 @@
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
package.json
14
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "crucix",
|
||||
"version": "2.0.0",
|
||||
"description": "Local intelligence engine — 26 OSINT sources, live dashboard, auto-refresh, optional LLM layer.",
|
||||
"description": "Local intelligence engine - 27 OSINT sources, live dashboard, public demo at crucix.live, auto-refresh, optional LLM layer.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node server.mjs",
|
||||
@@ -14,7 +14,12 @@
|
||||
"clean": "node scripts/clean.mjs",
|
||||
"fresh-start": "npm run clean && npm start"
|
||||
},
|
||||
"keywords": ["osint", "intelligence", "dashboard", "geopolitical"],
|
||||
"keywords": [
|
||||
"osint",
|
||||
"intelligence",
|
||||
"dashboard",
|
||||
"geopolitical"
|
||||
],
|
||||
"author": "Crucix",
|
||||
"license": "AGPL-3.0-only",
|
||||
"engines": {
|
||||
@@ -25,6 +30,7 @@
|
||||
"express": "^5.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"discord.js": "^14.25.0"
|
||||
}
|
||||
"discord.js": "^14.25.1" },
|
||||
"overrides": {
|
||||
"undici": "^7.24.4" }
|
||||
}
|
||||
|
||||
30
test/llm-minimax-integration.test.mjs
Normal file
30
test/llm-minimax-integration.test.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
// MiniMax provider — integration test (calls real API)
|
||||
// Requires MINIMAX_API_KEY environment variable
|
||||
// Run: MINIMAX_API_KEY=sk-... node --test test/llm-minimax-integration.test.mjs
|
||||
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { MiniMaxProvider } from '../lib/llm/minimax.mjs';
|
||||
|
||||
const API_KEY = process.env.MINIMAX_API_KEY;
|
||||
|
||||
describe('MiniMax integration', { skip: !API_KEY && 'MINIMAX_API_KEY not set' }, () => {
|
||||
it('should complete a prompt with MiniMax-M2.5', async () => {
|
||||
const provider = new MiniMaxProvider({ apiKey: API_KEY, model: 'MiniMax-M2.5' });
|
||||
assert.equal(provider.isConfigured, true);
|
||||
|
||||
const result = await provider.complete(
|
||||
'You are a helpful assistant. Respond in exactly one sentence.',
|
||||
'What is 2+2?',
|
||||
{ maxTokens: 128, timeout: 30000 }
|
||||
);
|
||||
|
||||
assert.ok(result.text.length > 0, 'Response text should not be empty');
|
||||
assert.ok(result.usage.inputTokens > 0, 'Should report input tokens');
|
||||
assert.ok(result.usage.outputTokens > 0, 'Should report output tokens');
|
||||
assert.ok(result.model, 'Should report model name');
|
||||
console.log(` Response: ${result.text}`);
|
||||
console.log(` Tokens: ${result.usage.inputTokens} in / ${result.usage.outputTokens} out`);
|
||||
console.log(` Model: ${result.model}`);
|
||||
});
|
||||
});
|
||||
144
test/llm-minimax.test.mjs
Normal file
144
test/llm-minimax.test.mjs
Normal file
@@ -0,0 +1,144 @@
|
||||
// MiniMax provider — unit tests
|
||||
// Uses Node.js built-in test runner (node:test) — no extra dependencies
|
||||
|
||||
import { describe, it, mock, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { MiniMaxProvider } from '../lib/llm/minimax.mjs';
|
||||
import { createLLMProvider } from '../lib/llm/index.mjs';
|
||||
|
||||
// ─── Unit Tests ───
|
||||
|
||||
describe('MiniMaxProvider', () => {
|
||||
it('should set defaults correctly', () => {
|
||||
const provider = new MiniMaxProvider({ apiKey: 'sk-test' });
|
||||
assert.equal(provider.name, 'minimax');
|
||||
assert.equal(provider.model, 'MiniMax-M2.5');
|
||||
assert.equal(provider.isConfigured, true);
|
||||
});
|
||||
|
||||
it('should accept custom model', () => {
|
||||
const provider = new MiniMaxProvider({ apiKey: 'sk-test', model: 'MiniMax-M2.5-highspeed' });
|
||||
assert.equal(provider.model, 'MiniMax-M2.5-highspeed');
|
||||
});
|
||||
|
||||
it('should report not configured without API key', () => {
|
||||
const provider = new MiniMaxProvider({});
|
||||
assert.equal(provider.isConfigured, false);
|
||||
});
|
||||
|
||||
it('should throw on API error', async () => {
|
||||
const provider = new MiniMaxProvider({ apiKey: 'sk-test' });
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = mock.fn(() =>
|
||||
Promise.resolve({ ok: false, status: 401, text: () => Promise.resolve('Unauthorized') })
|
||||
);
|
||||
try {
|
||||
await assert.rejects(
|
||||
() => provider.complete('system', 'user'),
|
||||
(err) => {
|
||||
assert.match(err.message, /MiniMax API 401/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
it('should parse successful response', async () => {
|
||||
const provider = new MiniMaxProvider({ apiKey: 'sk-test' });
|
||||
const mockResponse = {
|
||||
choices: [{ message: { content: 'Hello from MiniMax' } }],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 5 },
|
||||
model: 'MiniMax-M2.5',
|
||||
};
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = mock.fn(() =>
|
||||
Promise.resolve({ ok: true, json: () => Promise.resolve(mockResponse) })
|
||||
);
|
||||
try {
|
||||
const result = await provider.complete('You are helpful.', 'Say hello');
|
||||
assert.equal(result.text, 'Hello from MiniMax');
|
||||
assert.equal(result.usage.inputTokens, 10);
|
||||
assert.equal(result.usage.outputTokens, 5);
|
||||
assert.equal(result.model, 'MiniMax-M2.5');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
it('should send correct request format', async () => {
|
||||
const provider = new MiniMaxProvider({ apiKey: 'sk-test-key', model: 'MiniMax-M2.5' });
|
||||
let capturedUrl, capturedOpts;
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = mock.fn((url, opts) => {
|
||||
capturedUrl = url;
|
||||
capturedOpts = opts;
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
choices: [{ message: { content: 'ok' } }],
|
||||
usage: { prompt_tokens: 1, completion_tokens: 1 },
|
||||
model: 'MiniMax-M2.5',
|
||||
}),
|
||||
});
|
||||
});
|
||||
try {
|
||||
await provider.complete('system prompt', 'user message', { maxTokens: 2048 });
|
||||
assert.equal(capturedUrl, 'https://api.minimax.io/v1/chat/completions');
|
||||
assert.equal(capturedOpts.method, 'POST');
|
||||
const headers = capturedOpts.headers;
|
||||
assert.equal(headers['Content-Type'], 'application/json');
|
||||
assert.equal(headers['Authorization'], 'Bearer sk-test-key');
|
||||
const body = JSON.parse(capturedOpts.body);
|
||||
assert.equal(body.model, 'MiniMax-M2.5');
|
||||
assert.equal(body.max_tokens, 2048);
|
||||
assert.equal(body.messages[0].role, 'system');
|
||||
assert.equal(body.messages[0].content, 'system prompt');
|
||||
assert.equal(body.messages[1].role, 'user');
|
||||
assert.equal(body.messages[1].content, 'user message');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle empty response gracefully', async () => {
|
||||
const provider = new MiniMaxProvider({ apiKey: 'sk-test' });
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = mock.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ choices: [], usage: {} }),
|
||||
})
|
||||
);
|
||||
try {
|
||||
const result = await provider.complete('sys', 'user');
|
||||
assert.equal(result.text, '');
|
||||
assert.equal(result.usage.inputTokens, 0);
|
||||
assert.equal(result.usage.outputTokens, 0);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Factory Tests ───
|
||||
|
||||
describe('createLLMProvider — minimax', () => {
|
||||
it('should create MiniMaxProvider for provider=minimax', () => {
|
||||
const provider = createLLMProvider({ provider: 'minimax', apiKey: 'sk-test', model: null });
|
||||
assert.ok(provider instanceof MiniMaxProvider);
|
||||
assert.equal(provider.name, 'minimax');
|
||||
assert.equal(provider.isConfigured, true);
|
||||
});
|
||||
|
||||
it('should be case-insensitive', () => {
|
||||
const provider = createLLMProvider({ provider: 'MiniMax', apiKey: 'sk-test', model: null });
|
||||
assert.ok(provider instanceof MiniMaxProvider);
|
||||
});
|
||||
|
||||
it('should return null for empty provider', () => {
|
||||
const provider = createLLMProvider({ provider: null, apiKey: 'sk-test', model: null });
|
||||
assert.equal(provider, null);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user