feat(llm): add first-class Grok (xAI) support with dedicated GrokAdapter (#44)

feat(llm): add first-class Grok (xAI) support with dedicated GrokAdapter
This commit is contained in:
Marcelo Ceccon 2026-04-04 07:20:55 -03:00 committed by GitHub
parent 071d5dce61
commit 10074c9b7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 282 additions and 5 deletions

View File

@ -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` | | [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 | | [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 | | [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 ## Architecture
@ -180,12 +181,26 @@ npx tsx examples/01-single-agent.ts
|----------|--------|---------|--------| |----------|--------|---------|--------|
| Anthropic (Claude) | `provider: 'anthropic'` | `ANTHROPIC_API_KEY` | Verified | | Anthropic (Claude) | `provider: 'anthropic'` | `ANTHROPIC_API_KEY` | Verified |
| OpenAI (GPT) | `provider: 'openai'` | `OPENAI_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 | | GitHub Copilot | `provider: 'copilot'` | `GITHUB_TOKEN` | Verified |
| Ollama / vLLM / LM Studio | `provider: 'openai'` + `baseURL` | — | 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)). 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 ## Contributing

154
examples/12-grok.ts Normal file
View File

@ -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<string, number>()
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))
}

View File

@ -37,7 +37,7 @@ import type { LLMAdapter } from '../types.js'
* Additional providers can be integrated by implementing {@link LLMAdapter} * Additional providers can be integrated by implementing {@link LLMAdapter}
* directly and bypassing this factory. * 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. * Instantiate the appropriate {@link LLMAdapter} for the given provider.
@ -46,6 +46,7 @@ export type SupportedProvider = 'anthropic' | 'copilot' | 'openai'
* explicitly: * explicitly:
* - `anthropic` `ANTHROPIC_API_KEY` * - `anthropic` `ANTHROPIC_API_KEY`
* - `openai` `OPENAI_API_KEY` * - `openai` `OPENAI_API_KEY`
* - `grok` `XAI_API_KEY`
* - `copilot` `GITHUB_COPILOT_TOKEN` / `GITHUB_TOKEN`, or interactive * - `copilot` `GITHUB_COPILOT_TOKEN` / `GITHUB_TOKEN`, or interactive
* OAuth2 device flow if neither is set * OAuth2 device flow if neither is set
* *
@ -78,6 +79,10 @@ export async function createAdapter(
const { OpenAIAdapter } = await import('./openai.js') const { OpenAIAdapter } = await import('./openai.js')
return new OpenAIAdapter(apiKey, baseURL) return new OpenAIAdapter(apiKey, baseURL)
} }
case 'grok': {
const { GrokAdapter } = await import('./grok.js')
return new GrokAdapter(apiKey, baseURL)
}
default: { default: {
// The `never` cast here makes TypeScript enforce exhaustiveness. // The `never` cast here makes TypeScript enforce exhaustiveness.
const _exhaustive: never = provider const _exhaustive: never = provider

29
src/llm/grok.ts Normal file
View File

@ -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'
)
}
}

View File

@ -65,7 +65,7 @@ import {
* Thread-safe a single instance may be shared across concurrent agent runs. * Thread-safe a single instance may be shared across concurrent agent runs.
*/ */
export class OpenAIAdapter implements LLMAdapter { export class OpenAIAdapter implements LLMAdapter {
readonly name = 'openai' readonly name: string = 'openai'
readonly #client: OpenAI readonly #client: OpenAI

View File

@ -186,7 +186,7 @@ export interface ToolDefinition<TInput = Record<string, unknown>> {
export interface AgentConfig { export interface AgentConfig {
readonly name: string readonly name: string
readonly model: 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.). * 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 * Note: local servers that don't require auth still need `apiKey` set to a
@ -312,7 +312,7 @@ export interface OrchestratorEvent {
export interface OrchestratorConfig { export interface OrchestratorConfig {
readonly maxConcurrency?: number readonly maxConcurrency?: number
readonly defaultModel?: string readonly defaultModel?: string
readonly defaultProvider?: 'anthropic' | 'copilot' | 'openai' readonly defaultProvider?: 'anthropic' | 'copilot' | 'grok' | 'openai'
readonly defaultBaseURL?: string readonly defaultBaseURL?: string
readonly defaultApiKey?: string readonly defaultApiKey?: string
readonly onProgress?: (event: OrchestratorEvent) => void readonly onProgress?: (event: OrchestratorEvent) => void

View File

@ -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)
})
})