diff --git a/src/__tests__/local-agent-tool-calling.test.ts b/src/__tests__/local-agent-tool-calling.test.ts index e3353ab..a5791d2 100644 --- a/src/__tests__/local-agent-tool-calling.test.ts +++ b/src/__tests__/local-agent-tool-calling.test.ts @@ -134,3 +134,23 @@ describe('local-model (HTTP) backbone surfaces toolCalls — the keyless-path fi expect(sys.content).toContain('nmap_scan'); }); }); + +describe('xai provider (Grok Build) — routes to xAI OpenAI-compatible API with the key', () => { + const origFetch = global.fetch; + afterEach(() => { global.fetch = origFetch; }); + it('hits api.x.ai with the bearer key and surfaces native tool_calls', async () => { + const spy = vi.fn(async (_url: string, _init: { headers: Record }) => ({ + ok: true, + json: async () => ({ + model: 'grok-build-0.1', + choices: [{ message: { content: '', tool_calls: [{ id: 'c1', function: { name: 'nmap_scan', arguments: '{"target":"x"}' } }] }, finish_reason: 'tool_calls' }], + }), + } as unknown as Response)); + global.fetch = spy as unknown as typeof fetch; + const be = new LLMBackbone({ provider: 'xai', model: 'grok-build-0.1', apiKey: 'xai-test-key-1234567890', baseUrl: 'https://api.x.ai/v1' } as never); + const res = await be.chatWithTools([{ role: 'user', content: 'scan' }], TOOLS as never); + expect(res.toolCalls?.[0]?.name).toBe('nmap_scan'); + expect(spy.mock.calls[0][0]).toContain('api.x.ai'); + expect((spy.mock.calls[0][1] as { headers: Record }).headers.Authorization).toContain('xai-test-key'); + }); +}); diff --git a/src/config/index.ts b/src/config/index.ts index 9189860..94f6d35 100755 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -22,6 +22,7 @@ export interface TempestSettings { venice?: string; anthropic?: string; openai?: string; + xai?: string; }; // Default LLM settings @@ -54,6 +55,12 @@ export interface TempestSettings { defaultModel: string; }; + // xAI — Grok Build / Grok models (OpenAI-compatible API) + xai: { + baseUrl: string; + defaultModel: string; + }; + // Codex CLI/account subscription backend codex: { command: string; @@ -112,6 +119,11 @@ const DEFAULT_SETTINGS: TempestSettings = { defaultModel: 'gpt-4-turbo-preview', }, + xai: { + baseUrl: 'https://api.x.ai/v1', + defaultModel: 'grok-build-0.1', + }, + codex: { command: 'codex', defaultModel: 'codex-default', @@ -389,6 +401,16 @@ export const AVAILABLE_MODELS: Record = { capabilities: ['testing'], }, ], + xai: [ + { + id: 'grok-build-0.1', + name: 'Grok Build (grok-build-0.1, 256K)', + provider: 'xAI', + contextWindow: 256000, + maxOutput: 8192, + capabilities: ['reasoning', 'code', 'analysis', 'agents', 'tools'], + }, + ], local: [ { id: 'local-model', @@ -486,6 +508,7 @@ class ConfigManager { // Environment variables (including values loaded from ~/.t3mp3st/.env above) // are intentionally process-local. getApiKey() already gives env vars highest // priority, so do not persist them into Conf's apiKeys store as a side effect. + // (XAI_API_KEY / Grok Build follows the same safe path — see the envVarMap in getApiKey.) this.envLoaded = true; } @@ -513,7 +536,7 @@ class ConfigManager { /** * Set an API key for a provider */ - setApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai', key: string): void { + setApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai', key: string): void { const apiKeys = this.config.get('apiKeys'); apiKeys[provider] = key; this.config.set('apiKeys', apiKeys); @@ -522,13 +545,14 @@ class ConfigManager { /** * Get an API key for a provider */ - getApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai'): string | undefined { + getApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai'): string | undefined { // First check environment variables (highest priority) const envVarMap = { openrouter: 'OPENROUTER_API_KEY', venice: 'VENICE_API_KEY', anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', + xai: 'XAI_API_KEY', }; // Force a fully UNCONFIGURED server (no key from env OR the saved store) — used by @@ -547,7 +571,7 @@ class ConfigManager { /** * Check if a provider has a valid API key configured */ - hasApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai'): boolean { + hasApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai'): boolean { const key = this.getApiKey(provider); return !!key && key.length > 10; } @@ -555,7 +579,7 @@ class ConfigManager { /** * Remove an API key */ - removeApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai'): void { + removeApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai'): void { const apiKeys = this.config.get('apiKeys'); delete apiKeys[provider]; this.config.set('apiKeys', apiKeys); @@ -571,6 +595,7 @@ class ConfigManager { if (this.hasApiKey('venice')) providers.push('venice'); if (this.hasApiKey('anthropic')) providers.push('anthropic'); if (this.hasApiKey('openai')) providers.push('openai'); + if (this.hasApiKey('xai')) providers.push('xai'); // Codex uses the local Codex CLI/account auth instead of API-key storage. providers.push('codex'); @@ -612,6 +637,12 @@ class ConfigManager { baseUrl = this.config.get('openai').baseUrl; actualModel = model || this.config.get('openai').defaultModel; break; + case 'xai': + // Grok Build / Grok — xAI's OpenAI-compatible API (native tool-calling). + apiKey = this.getApiKey('xai'); + baseUrl = this.config.get('xai').baseUrl; + actualModel = model || this.config.get('xai').defaultModel; + break; case 'codex': actualModel = model || this.config.get('codex').defaultModel; break; @@ -657,7 +688,7 @@ class ConfigManager { const flag = (process.env.TEMPEST_MODEL_FALLBACK || '').trim().toLowerCase(); if (!flag || ['0', 'false', 'off', 'no'].includes(flag)) return []; const chain: FallbackEntry[] = []; - const add = (p: 'openrouter' | 'venice' | 'anthropic' | 'openai') => { + const add = (p: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai') => { if (p === primary || !this.hasApiKey(p)) return; chain.push({ provider: p, @@ -670,6 +701,7 @@ class ConfigManager { add('venice'); add('anthropic'); add('openai'); + add('xai'); return chain; } @@ -787,8 +819,8 @@ OPENAI_API_KEY= export const config = new ConfigManager(); // Helper functions for quick access -export const getApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai') => config.getApiKey(provider); -export const setApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai', key: string) => config.setApiKey(provider, key); -export const hasApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai') => config.hasApiKey(provider); +export const getApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai') => config.getApiKey(provider); +export const setApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai', key: string) => config.setApiKey(provider, key); +export const hasApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai') => config.hasApiKey(provider); export const getLLMConfig = (provider?: LLMProvider, model?: string) => config.getLLMConfig(provider, model); export const getConfiguredProviders = () => config.getConfiguredProviders(); diff --git a/src/llm/index.ts b/src/llm/index.ts index 3404255..3e781fc 100755 --- a/src/llm/index.ts +++ b/src/llm/index.ts @@ -1201,6 +1201,8 @@ export class LLMBackbone extends EventEmitter { return new AnthropicAdapter(config); case 'openai': return new OpenAIAdapter(config); + case 'xai': + return new OpenAIAdapter(config); // xAI (Grok Build / grok-*) is OpenAI-compatible case 'codex': return new CodexAdapter(config); case 'mock': diff --git a/src/types/index.ts b/src/types/index.ts index 2e6d638..e8cfdea 100755 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,7 +6,7 @@ // LLM CONFIGURATION // ============================================================================= -export type LLMProvider = 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'codex' | 'mock' | 'local' | 'local-agent'; +export type LLMProvider = 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'codex' | 'mock' | 'local' | 'local-agent'; export interface LLMConfig { provider: LLMProvider;