From 17b8d9ed87c3e3c2e4be3dfe22e8cc0e9f3e0918 Mon Sep 17 00:00:00 2001 From: 3byss Date: Fri, 20 Mar 2026 00:46:44 -0600 Subject: [PATCH 1/4] Finished the Grok LLM implementation --- .env.example | 4 ++-- README.md | 3 ++- crucix.config.mjs | 2 +- lib/llm/grok.mjs | 53 +++++++++++++++++++++++++++++++++++++++++++++++ lib/llm/index.mjs | 5 +++++ 5 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 lib/llm/grok.mjs diff --git a/.env.example b/.env.example index b44266f..d69966f 100644 --- a/.env.example +++ b/.env.example @@ -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 | minimax | mistral +# Provider options: anthropic | openai | gemini | codex | openrouter | minimax | mistral | grok 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 | minimax: MiniMax-M2.5 +# 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 | grok: grok-3 LLM_MODEL= # === Telegram Alerts (optional, requires LLM) === diff --git a/README.md b/README.md index 89b1477..dbc5716 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,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`, `openrouter`, `minimax`, `mistral` +Set `LLM_PROVIDER` to one of: `anthropic`, `openai`, `gemini`, `codex`, `openrouter`, `minimax`, `mistral`, `grok` | Provider | Key Required | Default Model | |----------|-------------|---------------| @@ -210,6 +210,7 @@ Set `LLM_PROVIDER` to one of: `anthropic`, `openai`, `gemini`, `codex`, `openrou | `codex` | None (uses `~/.codex/auth.json`) | gpt-5.3-codex | | `minimax` | `LLM_API_KEY` | MiniMax-M2.5 | | `mistral` | `LLM_API_KEY` | mistral-large-latest | +| `grok` | `LLM_API_KEY` | grok-3 | For Codex, run `npx @openai/codex login` to authenticate via your ChatGPT subscription. diff --git a/crucix.config.mjs b/crucix.config.mjs index c9e7235..aa35312 100644 --- a/crucix.config.mjs +++ b/crucix.config.mjs @@ -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 | minimax | mistral + provider: process.env.LLM_PROVIDER || null, // anthropic | openai | gemini | codex | openrouter | minimax | mistral | grok apiKey: process.env.LLM_API_KEY || null, model: process.env.LLM_MODEL || null, }, diff --git a/lib/llm/grok.mjs b/lib/llm/grok.mjs new file mode 100644 index 0000000..8fc4267 --- /dev/null +++ b/lib/llm/grok.mjs @@ -0,0 +1,53 @@ +// Grok Provider - raw fetch, no SDK + +import { LLMProvider } from './provider.mjs'; + +export class GrokProvider extends LLMProvider { + constructor(config) { + super(config); + this.name = 'grok'; + this.apiKey = config.apiKey; + this.model = config.model || 'grok-3'; + } + + getConfigured() { + return !!this.apiKey; + } + + async complete(systemPrompt, userMessage, opts = {}) { + const res = await fetch('https://api.x.ai/v1/chat/completions', { + method: POST, + headers: { + 'Content-type': 'application/json', + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userMessage }, + ], + model: this.model, + stream: false, + temperature: 0, + }), + signal: AbortSignal.timeout(opts.timeout || 60000), + }); + + if (!res.ok) { + const err = await res.text().catch(() => ''); + throw new Error(`Grok 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, + }; + } +} diff --git a/lib/llm/index.mjs b/lib/llm/index.mjs index b2d16ee..096279c 100644 --- a/lib/llm/index.mjs +++ b/lib/llm/index.mjs @@ -7,6 +7,7 @@ import { GeminiProvider } from './gemini.mjs'; import { CodexProvider } from './codex.mjs'; import { MiniMaxProvider } from './minimax.mjs'; import { MistralProvider } from './mistral.mjs'; +import { GrokProvider } from './grok.mjs'; export { LLMProvider } from './provider.mjs'; export { AnthropicProvider } from './anthropic.mjs'; @@ -16,6 +17,8 @@ export { GeminiProvider } from './gemini.mjs'; export { CodexProvider } from './codex.mjs'; export { MiniMaxProvider } from './minimax.mjs'; export { MistralProvider } from './mistral.mjs'; +export { GrokProvider } from './grok.mjs'; + /** * Create an LLM provider based on config. @@ -42,6 +45,8 @@ export function createLLMProvider(llmConfig) { return new MiniMaxProvider({ apiKey, model }); case 'mistral': return new MistralProvider({ apiKey, model }); + case 'grok': + return new GrokProvider({ apiKey, model }); default: console.warn(`[LLM] Unknown provider "${provider}". LLM features disabled.`); return null; From cb275759bb73f9d657fb07bafab8971d1395a6a9 Mon Sep 17 00:00:00 2001 From: 3byss Date: Fri, 20 Mar 2026 16:26:30 -0600 Subject: [PATCH 2/4] Fixed types and created tests --- lib/llm/grok.mjs | 6 ++-- test/llm-grok.test.mjs | 79 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 test/llm-grok.test.mjs diff --git a/lib/llm/grok.mjs b/lib/llm/grok.mjs index 8fc4267..8c8dc41 100644 --- a/lib/llm/grok.mjs +++ b/lib/llm/grok.mjs @@ -10,15 +10,15 @@ export class GrokProvider extends LLMProvider { this.model = config.model || 'grok-3'; } - getConfigured() { + get isConfigured() { return !!this.apiKey; } async complete(systemPrompt, userMessage, opts = {}) { const res = await fetch('https://api.x.ai/v1/chat/completions', { - method: POST, + method: 'POST', headers: { - 'Content-type': 'application/json', + 'Content-Type': 'application/json', Authorization: `Bearer ${this.apiKey}`, }, body: JSON.stringify({ diff --git a/test/llm-grok.test.mjs b/test/llm-grok.test.mjs new file mode 100644 index 0000000..59a648a --- /dev/null +++ b/test/llm-grok.test.mjs @@ -0,0 +1,79 @@ +// 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 assert from 'node:assert/strict'; +import { GrokProvider } from '../lib/llm/grok.mjs'; +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 accept custom model', () => { + const provider = new GrokProvider({ apiKey: 'sk-test', model: 'grok-2' }); + assert.equal(provider.model, 'grok-2'); + }); + + it('should report not configured without API key', () => { + const provider = new GrokProvider({}); + assert.equal(provider.isConfigured, false); + }); + + it('should throw on API error', async () => { + const provider = new GrokProvider({ 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, /Grok API 401/); + return true; + } + ); + } finally { + globalThis.fetch = originalFetch; + } + }); + + 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; + globalThis.fetch = mock.fn(() => + Promise.resolve({ ok: true, json: () => Promise.resolve(mockResponse) }) + ); + try { + const result = await provider.complete('system', 'user'); + assert.equal(result.text, 'Hello world'); + assert.equal(result.usage.inputTokens, 10); + assert.equal(result.usage.outputTokens, 5); + assert.equal(result.model, 'grok-3'); + } finally { + globalThis.fetch = originalFetch; + } + }); +}); + +// ─── Factory Tests ─── + +describe('createLLMProvider', () => { + it('should create Grok provider', () => { + const provider = createLLMProvider({ provider: 'grok', apiKey: 'sk-test' }); + assert.ok(provider instanceof GrokProvider); + assert.equal(provider.isConfigured, true); + }); +}); \ No newline at end of file From 457dba2800461742bb33f0cd1deabe520348986a Mon Sep 17 00:00:00 2001 From: 3byss Date: Fri, 27 Mar 2026 22:22:21 -0600 Subject: [PATCH 3/4] Fixed any issues with the PR --- .env.example | 2 +- README.md | 3 ++- lib/llm/grok.mjs | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index a547224..674e882 100644 --- a/.env.example +++ b/.env.example @@ -38,7 +38,7 @@ LLM_PROVIDER= # Not needed for codex (uses ~/.codex/auth.json) or ollama (local) 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 | minimax: MiniMax-M2.5 | ollama: llama3.1:8b | grok: grok-3 +# 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 | ollama: llama3.1:8b | grok: grok-4-latest LLM_MODEL= # Ollama base URL (only needed if not using default http://localhost:11434) OLLAMA_BASE_URL= diff --git a/README.md b/README.md index f0814a7..4583e78 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,7 @@ Set `LLM_PROVIDER` to one of: `anthropic`, `openai`, `gemini`, `codex`, `openrou | `codex` | None (uses `~/.codex/auth.json`) | gpt-5.3-codex | | `minimax` | `LLM_API_KEY` | MiniMax-M2.5 | | `mistral` | `LLM_API_KEY` | mistral-large-latest | -| `grok` | `LLM_API_KEY` | grok-3 | +| `grok` | `LLM_API_KEY` | grok-4-latest | For Codex, run `npx @openai/codex login` to authenticate via your ChatGPT subscription. @@ -308,6 +308,7 @@ crucix/ │ │ ├── anthropic.mjs # Claude │ │ ├── openai.mjs # GPT │ │ ├── gemini.mjs # Gemini +├── ├── grok.mjs # Grok │ │ ├── openrouter.mjs # OpenRouter (Unified API) │ │ ├── codex.mjs # Codex (ChatGPT subscription) │ │ ├── minimax.mjs # MiniMax (M2.5, 204K context) diff --git a/lib/llm/grok.mjs b/lib/llm/grok.mjs index 8c8dc41..621bb7f 100644 --- a/lib/llm/grok.mjs +++ b/lib/llm/grok.mjs @@ -7,7 +7,7 @@ export class GrokProvider extends LLMProvider { super(config); this.name = 'grok'; this.apiKey = config.apiKey; - this.model = config.model || 'grok-3'; + this.model = config.model || 'grok-4-latest'; } get isConfigured() { @@ -22,6 +22,7 @@ export class GrokProvider extends LLMProvider { Authorization: `Bearer ${this.apiKey}`, }, body: JSON.stringify({ + max_tokens: opts.max_tokens || 4096, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userMessage }, @@ -39,7 +40,7 @@ export class GrokProvider extends LLMProvider { } const data = await res.json(); - const text = data.choices?.[0].message?.content || ''; + const text = data.choices?.[0]?.message?.content || ''; return { text, From 1fb50939cef2df19ed90707d11a35a0378649fd6 Mon Sep 17 00:00:00 2001 From: calesthio Date: Sat, 28 Mar 2026 22:36:33 -0700 Subject: [PATCH 4/4] fix(grok): align token caps and docs --- README.md | 10 ++--- lib/llm/grok.mjs | 2 +- test/llm-grok.test.mjs | 86 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 76 insertions(+), 22 deletions(-) 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 +});