diff --git a/README.md b/README.md index 4583e78..a05f962 100644 --- a/README.md +++ b/README.md @@ -186,10 +186,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 6 LLM providers for enhanced analysis: +Connect any of 8 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), MiniMax, Mistral +- Providers: Anthropic Claude, OpenAI, Google Gemini, OpenRouter (Unified API), OpenAI Codex (ChatGPT subscription), MiniMax, Mistral, Grok - Graceful fallback — when LLM is unavailable, a rule-based engine takes over alert evaluation. LLM failures never crash the sweep cycle. --- @@ -303,12 +303,12 @@ crucix/ │ └── jarvis.html # Self-contained Jarvis HUD │ ├── lib/ -│ ├── llm/ # LLM abstraction (5 providers, raw fetch, no SDKs) +│ ├── llm/ # LLM abstraction (8 providers, raw fetch, no SDKs) │ │ ├── provider.mjs # Base class │ │ ├── anthropic.mjs # Claude │ │ ├── openai.mjs # GPT │ │ ├── gemini.mjs # Gemini -├── ├── grok.mjs # Grok +│ │ ├── grok.mjs # Grok │ │ ├── openrouter.mjs # OpenRouter (Unified API) │ │ ├── codex.mjs # Codex (ChatGPT subscription) │ │ ├── minimax.mjs # MiniMax (M2.5, 204K context) @@ -414,7 +414,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`, `openrouter`, `minimax`, or `mistral` | +| `LLM_PROVIDER` | disabled | `anthropic`, `openai`, `gemini`, `codex`, `openrouter`, `minimax`, `mistral`, or `grok` | | `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 | diff --git a/lib/llm/grok.mjs b/lib/llm/grok.mjs index 621bb7f..76fef66 100644 --- a/lib/llm/grok.mjs +++ b/lib/llm/grok.mjs @@ -22,7 +22,7 @@ export class GrokProvider extends LLMProvider { Authorization: `Bearer ${this.apiKey}`, }, body: JSON.stringify({ - max_tokens: opts.max_tokens || 4096, + max_tokens: opts.maxTokens || 4096, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userMessage }, diff --git a/test/llm-grok.test.mjs b/test/llm-grok.test.mjs index 59a648a..1d400fe 100644 --- a/test/llm-grok.test.mjs +++ b/test/llm-grok.test.mjs @@ -1,7 +1,7 @@ // Grok provider — unit tests // Uses Node.js built-in test runner (node:test) — no extra dependencies -import { describe, it, mock, beforeEach } from 'node:test'; +import { describe, it, mock } from 'node:test'; import assert from 'node:assert/strict'; import { GrokProvider } from '../lib/llm/grok.mjs'; import { createLLMProvider } from '../lib/llm/index.mjs'; @@ -9,12 +9,12 @@ import { createLLMProvider } from '../lib/llm/index.mjs'; // ─── Unit Tests ─── describe('GrokProvider', () => { - it('should set defaults correctly', () => { - const provider = new GrokProvider({ apiKey: 'sk-test' }); - assert.equal(provider.name, 'grok'); - assert.equal(provider.model, 'grok-3'); - assert.equal(provider.isConfigured, true); - }); + it('should set defaults correctly', () => { + const provider = new GrokProvider({ apiKey: 'sk-test' }); + assert.equal(provider.name, 'grok'); + assert.equal(provider.model, 'grok-4-latest'); + assert.equal(provider.isConfigured, true); + }); it('should accept custom model', () => { const provider = new GrokProvider({ apiKey: 'sk-test', model: 'grok-2' }); @@ -45,11 +45,11 @@ describe('GrokProvider', () => { } }); - it('should parse successful response', async () => { - const provider = new GrokProvider({ apiKey: 'sk-test' }); - const mockResponse = { - choices: [{ message: { content: 'Hello world' } }], - usage: { prompt_tokens: 10, completion_tokens: 5 }, + it('should parse successful response', async () => { + const provider = new GrokProvider({ apiKey: 'sk-test' }); + const mockResponse = { + choices: [{ message: { content: 'Hello world' } }], + usage: { prompt_tokens: 10, completion_tokens: 5 }, model: 'grok-3' }; const originalFetch = globalThis.fetch; @@ -63,9 +63,63 @@ describe('GrokProvider', () => { assert.equal(result.usage.outputTokens, 5); assert.equal(result.model, 'grok-3'); } finally { - globalThis.fetch = originalFetch; - } - }); + globalThis.fetch = originalFetch; + } + }); + + it('should send correct request format', async () => { + const provider = new GrokProvider({ apiKey: 'sk-test-key', model: 'grok-4-latest' }); + 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: 'grok-4-latest', + }), + }); + }); + try { + await provider.complete('system prompt', 'user message', { maxTokens: 2048 }); + assert.equal(capturedUrl, 'https://api.x.ai/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, 'grok-4-latest'); + 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 GrokProvider({ 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 ─── @@ -76,4 +130,4 @@ describe('createLLMProvider', () => { assert.ok(provider instanceof GrokProvider); assert.equal(provider.isConfigured, true); }); -}); \ No newline at end of file +});