From 10074c9b7d220e1993c90e6352497b4b3e0986c9 Mon Sep 17 00:00:00 2001 From: Marcelo Ceccon <32395187+marceloceccon@users.noreply.github.com> Date: Sat, 4 Apr 2026 07:20:55 -0300 Subject: [PATCH] feat(llm): add first-class Grok (xAI) support with dedicated GrokAdapter (#44) feat(llm): add first-class Grok (xAI) support with dedicated GrokAdapter --- README.md | 17 +++- examples/12-grok.ts | 154 +++++++++++++++++++++++++++++++++++++ src/llm/adapter.ts | 7 +- src/llm/grok.ts | 29 +++++++ src/llm/openai.ts | 2 +- src/types.ts | 4 +- tests/grok-adapter.test.ts | 74 ++++++++++++++++++ 7 files changed, 282 insertions(+), 5 deletions(-) create mode 100644 examples/12-grok.ts create mode 100644 src/llm/grok.ts create mode 100644 tests/grok-adapter.test.ts diff --git a/README.md b/README.md index d9b5d39..412a4fd 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ npx tsx examples/01-single-agent.ts | [09 — Structured Output](examples/09-structured-output.ts) | `outputSchema` (Zod) on AgentConfig — validated JSON via `result.structured` | | [10 — Task Retry](examples/10-task-retry.ts) | `maxRetries` / `retryDelayMs` / `retryBackoff` with `task_retry` progress events | | [11 — Trace Observability](examples/11-trace-observability.ts) | `onTrace` callback — structured spans for LLM calls, tools, tasks, and agents | +| [12 — Grok](examples/12-grok.ts) | Same as example 02 (`runTeam()` collaboration) with Grok (`XAI_API_KEY`) | ## Architecture @@ -180,12 +181,26 @@ npx tsx examples/01-single-agent.ts |----------|--------|---------|--------| | Anthropic (Claude) | `provider: 'anthropic'` | `ANTHROPIC_API_KEY` | Verified | | OpenAI (GPT) | `provider: 'openai'` | `OPENAI_API_KEY` | Verified | +| Grok (xAI) | `provider: 'grok'` | `XAI_API_KEY` | Verified | | GitHub Copilot | `provider: 'copilot'` | `GITHUB_TOKEN` | Verified | | Ollama / vLLM / LM Studio | `provider: 'openai'` + `baseURL` | — | Verified | Verified local models with tool-calling: **Gemma 4** (see [example 08](examples/08-gemma4-local.ts)). -Any OpenAI-compatible API should work via `provider: 'openai'` + `baseURL` (DeepSeek, Groq, Mistral, Qwen, MiniMax, etc.). These providers have not been fully verified yet — contributions welcome via [#25](https://github.com/JackChen-me/open-multi-agent/issues/25). +Any OpenAI-compatible API should work via `provider: 'openai'` + `baseURL` (DeepSeek, Groq, Mistral, Qwen, MiniMax, etc.). **Grok now has first-class support** via `provider: 'grok'`. + +### LLM Configuration Examples + +```typescript +const grokAgent: AgentConfig = { + name: 'grok-agent', + provider: 'grok', + model: 'grok-4', + systemPrompt: 'You are a helpful assistant.', +} +``` + +(Set your `XAI_API_KEY` environment variable — no `baseURL` needed anymore.) ## Contributing diff --git a/examples/12-grok.ts b/examples/12-grok.ts new file mode 100644 index 0000000..d4ed08b --- /dev/null +++ b/examples/12-grok.ts @@ -0,0 +1,154 @@ +/** + * Example 12 — Multi-Agent Team Collaboration with Grok (xAI) + * + * Three specialized agents (architect, developer, reviewer) collaborate via `runTeam()` + * to build a minimal Express.js REST API. Every agent uses Grok's coding-optimized model. + * + * Run: + * npx tsx examples/12-grok.ts + * + * Prerequisites: + * XAI_API_KEY environment variable must be set. + */ + +import { OpenMultiAgent } from '../src/index.js' +import type { AgentConfig, OrchestratorEvent } from '../src/types.js' + +// --------------------------------------------------------------------------- +// Agent definitions (all using grok-code-fast-1) +// --------------------------------------------------------------------------- +const architect: AgentConfig = { + name: 'architect', + model: 'grok-code-fast-1', + provider: 'grok', + systemPrompt: `You are a software architect with deep experience in Node.js and REST API design. +Your job is to design clear, production-quality API contracts and file/directory structures. +Output concise plans in markdown — no unnecessary prose.`, + tools: ['bash', 'file_write'], + maxTurns: 5, + temperature: 0.2, +} + +const developer: AgentConfig = { + name: 'developer', + model: 'grok-code-fast-1', + provider: 'grok', + systemPrompt: `You are a TypeScript/Node.js developer. You implement what the architect specifies. +Write clean, runnable code with proper error handling. Use the tools to write files and run tests.`, + tools: ['bash', 'file_read', 'file_write', 'file_edit'], + maxTurns: 12, + temperature: 0.1, +} + +const reviewer: AgentConfig = { + name: 'reviewer', + model: 'grok-code-fast-1', + provider: 'grok', + systemPrompt: `You are a senior code reviewer. Review code for correctness, security, and clarity. +Provide a structured review with: LGTM items, suggestions, and any blocking issues. +Read files using the tools before reviewing.`, + tools: ['bash', 'file_read', 'grep'], + maxTurns: 5, + temperature: 0.3, +} + +// --------------------------------------------------------------------------- +// Progress tracking +// --------------------------------------------------------------------------- +const startTimes = new Map() + +function handleProgress(event: OrchestratorEvent): void { + const ts = new Date().toISOString().slice(11, 23) // HH:MM:SS.mmm + switch (event.type) { + case 'agent_start': + startTimes.set(event.agent ?? '', Date.now()) + console.log(`[${ts}] AGENT START → ${event.agent}`) + break + case 'agent_complete': { + const elapsed = Date.now() - (startTimes.get(event.agent ?? '') ?? Date.now()) + console.log(`[${ts}] AGENT DONE ← ${event.agent} (${elapsed}ms)`) + break + } + case 'task_start': + console.log(`[${ts}] TASK START ↓ ${event.task}`) + break + case 'task_complete': + console.log(`[${ts}] TASK DONE ↑ ${event.task}`) + break + case 'message': + console.log(`[${ts}] MESSAGE • ${event.agent} → (team)`) + break + case 'error': + console.error(`[${ts}] ERROR ✗ agent=${event.agent} task=${event.task}`) + if (event.data instanceof Error) console.error(` ${event.data.message}`) + break + } +} + +// --------------------------------------------------------------------------- +// Orchestrate +// --------------------------------------------------------------------------- +const orchestrator = new OpenMultiAgent({ + defaultModel: 'grok-code-fast-1', + defaultProvider: 'grok', + maxConcurrency: 1, // sequential for readable output + onProgress: handleProgress, +}) + +const team = orchestrator.createTeam('api-team', { + name: 'api-team', + agents: [architect, developer, reviewer], + sharedMemory: true, + maxConcurrency: 1, +}) + +console.log(`Team "${team.name}" created with agents: ${team.getAgents().map(a => a.name).join(', ')}`) +console.log('\nStarting team run...\n') +console.log('='.repeat(60)) + +const goal = `Create a minimal Express.js REST API in /tmp/express-api/ with: +- GET /health → { status: "ok" } +- GET /users → returns a hardcoded array of 2 user objects +- POST /users → accepts { name, email } body, logs it, returns 201 +- Proper error handling middleware +- The server should listen on port 3001 +- Include a package.json with the required dependencies` + +const result = await orchestrator.runTeam(team, goal) + +console.log('\n' + '='.repeat(60)) + +// --------------------------------------------------------------------------- +// Results +// --------------------------------------------------------------------------- +console.log('\nTeam run complete.') +console.log(`Success: ${result.success}`) +console.log(`Total tokens — input: ${result.totalTokenUsage.input_tokens}, output: ${result.totalTokenUsage.output_tokens}`) + +console.log('\nPer-agent results:') +for (const [agentName, agentResult] of result.agentResults) { + const status = agentResult.success ? 'OK' : 'FAILED' + const tools = agentResult.toolCalls.length + console.log(` ${agentName.padEnd(12)} [${status}] tool_calls=${tools}`) + if (!agentResult.success) { + console.log(` Error: ${agentResult.output.slice(0, 120)}`) + } +} + +// Sample outputs +const developerResult = result.agentResults.get('developer') +if (developerResult?.success) { + console.log('\nDeveloper output (last 600 chars):') + console.log('─'.repeat(60)) + const out = developerResult.output + console.log(out.length > 600 ? '...' + out.slice(-600) : out) + console.log('─'.repeat(60)) +} + +const reviewerResult = result.agentResults.get('reviewer') +if (reviewerResult?.success) { + console.log('\nReviewer output:') + console.log('─'.repeat(60)) + console.log(reviewerResult.output) + console.log('─'.repeat(60)) +} \ No newline at end of file diff --git a/src/llm/adapter.ts b/src/llm/adapter.ts index cbe5b4f..98d4907 100644 --- a/src/llm/adapter.ts +++ b/src/llm/adapter.ts @@ -37,7 +37,7 @@ import type { LLMAdapter } from '../types.js' * Additional providers can be integrated by implementing {@link LLMAdapter} * directly and bypassing this factory. */ -export type SupportedProvider = 'anthropic' | 'copilot' | 'openai' +export type SupportedProvider = 'anthropic' | 'copilot' | 'grok' | 'openai' /** * Instantiate the appropriate {@link LLMAdapter} for the given provider. @@ -46,6 +46,7 @@ export type SupportedProvider = 'anthropic' | 'copilot' | 'openai' * explicitly: * - `anthropic` → `ANTHROPIC_API_KEY` * - `openai` → `OPENAI_API_KEY` + * - `grok` → `XAI_API_KEY` * - `copilot` → `GITHUB_COPILOT_TOKEN` / `GITHUB_TOKEN`, or interactive * OAuth2 device flow if neither is set * @@ -78,6 +79,10 @@ export async function createAdapter( const { OpenAIAdapter } = await import('./openai.js') return new OpenAIAdapter(apiKey, baseURL) } + case 'grok': { + const { GrokAdapter } = await import('./grok.js') + return new GrokAdapter(apiKey, baseURL) + } default: { // The `never` cast here makes TypeScript enforce exhaustiveness. const _exhaustive: never = provider diff --git a/src/llm/grok.ts b/src/llm/grok.ts new file mode 100644 index 0000000..31ef49c --- /dev/null +++ b/src/llm/grok.ts @@ -0,0 +1,29 @@ +/** + * @fileoverview Grok (xAI) adapter. + * + * Thin wrapper around OpenAIAdapter that hard-codes the official xAI endpoint + * and XAI_API_KEY environment variable fallback. + */ + +import { OpenAIAdapter } from './openai.js' + +/** + * LLM adapter for Grok models (grok-4 series and future models). + * + * Thread-safe. Can be shared across agents. + * + * Usage: + * provider: 'grok' + * model: 'grok-4' (or any current Grok model name) + */ +export class GrokAdapter extends OpenAIAdapter { + readonly name = 'grok' + + constructor(apiKey?: string, baseURL?: string) { + // Allow override of baseURL (for proxies or future changes) but default to official xAI endpoint. + super( + apiKey ?? process.env['XAI_API_KEY'], + baseURL ?? 'https://api.x.ai/v1' + ) + } +} diff --git a/src/llm/openai.ts b/src/llm/openai.ts index 568f94e..e3f166f 100644 --- a/src/llm/openai.ts +++ b/src/llm/openai.ts @@ -65,7 +65,7 @@ import { * Thread-safe — a single instance may be shared across concurrent agent runs. */ export class OpenAIAdapter implements LLMAdapter { - readonly name = 'openai' + readonly name: string = 'openai' readonly #client: OpenAI diff --git a/src/types.ts b/src/types.ts index 418d54e..0989df4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -186,7 +186,7 @@ export interface ToolDefinition> { export interface AgentConfig { readonly name: string readonly model: string - readonly provider?: 'anthropic' | 'copilot' | 'openai' + readonly provider?: 'anthropic' | 'copilot' | 'grok' | 'openai' /** * Custom base URL for OpenAI-compatible APIs (Ollama, vLLM, LM Studio, etc.). * Note: local servers that don't require auth still need `apiKey` set to a @@ -312,7 +312,7 @@ export interface OrchestratorEvent { export interface OrchestratorConfig { readonly maxConcurrency?: number readonly defaultModel?: string - readonly defaultProvider?: 'anthropic' | 'copilot' | 'openai' + readonly defaultProvider?: 'anthropic' | 'copilot' | 'grok' | 'openai' readonly defaultBaseURL?: string readonly defaultApiKey?: string readonly onProgress?: (event: OrchestratorEvent) => void diff --git a/tests/grok-adapter.test.ts b/tests/grok-adapter.test.ts new file mode 100644 index 0000000..bb459b2 --- /dev/null +++ b/tests/grok-adapter.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +// --------------------------------------------------------------------------- +// Mock OpenAI constructor (must be hoisted for Vitest) +// --------------------------------------------------------------------------- +const OpenAIMock = vi.hoisted(() => vi.fn()) + +vi.mock('openai', () => ({ + default: OpenAIMock, +})) + +import { GrokAdapter } from '../src/llm/grok.js' +import { createAdapter } from '../src/llm/adapter.js' + +// --------------------------------------------------------------------------- +// GrokAdapter tests +// --------------------------------------------------------------------------- + +describe('GrokAdapter', () => { + beforeEach(() => { + OpenAIMock.mockClear() + }) + + it('has name "grok"', () => { + const adapter = new GrokAdapter() + expect(adapter.name).toBe('grok') + }) + + it('uses XAI_API_KEY by default', () => { + const original = process.env['XAI_API_KEY'] + process.env['XAI_API_KEY'] = 'xai-test-key-123' + + try { + new GrokAdapter() + expect(OpenAIMock).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: 'xai-test-key-123', + baseURL: 'https://api.x.ai/v1', + }) + ) + } finally { + if (original === undefined) { + delete process.env['XAI_API_KEY'] + } else { + process.env['XAI_API_KEY'] = original + } + } + }) + + it('uses official xAI baseURL by default', () => { + new GrokAdapter('some-key') + expect(OpenAIMock).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: 'some-key', + baseURL: 'https://api.x.ai/v1', + }) + ) + }) + + it('allows overriding apiKey and baseURL', () => { + new GrokAdapter('custom-key', 'https://custom.endpoint/v1') + expect(OpenAIMock).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: 'custom-key', + baseURL: 'https://custom.endpoint/v1', + }) + ) + }) + + it('createAdapter("grok") returns GrokAdapter instance', async () => { + const adapter = await createAdapter('grok') + expect(adapter).toBeInstanceOf(GrokAdapter) + }) +}) \ No newline at end of file