feat(xai): Grok Build as a first-class provider (grok-build-0.1) via xAI OpenAI-compatible API
This commit is contained in:
1 parent
eac30e4ee3
commit
0cb1642543
4 files changed
+63
-9
No files matched your search
@@ -134,3 +134,23 @@ describe('local-model (HTTP) backbone surfaces toolCalls — the keyless-path fi
|
|||||||
expect(sys.content).toContain('nmap_scan');
|
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<string, string> }) => ({
|
||||||
|
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<string, string> }).headers.Authorization).toContain('xai-test-key');
|
||||||
|
});
|
||||||
|
});
|
||||||
+40
-8
@@ -22,6 +22,7 @@ export interface TempestSettings {
|
|||||||
venice?: string;
|
venice?: string;
|
||||||
anthropic?: string;
|
anthropic?: string;
|
||||||
openai?: string;
|
openai?: string;
|
||||||
|
xai?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default LLM settings
|
// Default LLM settings
|
||||||
@@ -54,6 +55,12 @@ export interface TempestSettings {
|
|||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// xAI — Grok Build / Grok models (OpenAI-compatible API)
|
||||||
|
xai: {
|
||||||
|
baseUrl: string;
|
||||||
|
defaultModel: string;
|
||||||
|
};
|
||||||
|
|
||||||
// Codex CLI/account subscription backend
|
// Codex CLI/account subscription backend
|
||||||
codex: {
|
codex: {
|
||||||
command: string;
|
command: string;
|
||||||
@@ -112,6 +119,11 @@ const DEFAULT_SETTINGS: TempestSettings = {
|
|||||||
defaultModel: 'gpt-4-turbo-preview',
|
defaultModel: 'gpt-4-turbo-preview',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
xai: {
|
||||||
|
baseUrl: 'https://api.x.ai/v1',
|
||||||
|
defaultModel: 'grok-build-0.1',
|
||||||
|
},
|
||||||
|
|
||||||
codex: {
|
codex: {
|
||||||
command: 'codex',
|
command: 'codex',
|
||||||
defaultModel: 'codex-default',
|
defaultModel: 'codex-default',
|
||||||
@@ -389,6 +401,16 @@ export const AVAILABLE_MODELS: Record<LLMProvider, ModelInfo[]> = {
|
|||||||
capabilities: ['testing'],
|
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: [
|
local: [
|
||||||
{
|
{
|
||||||
id: 'local-model',
|
id: 'local-model',
|
||||||
@@ -486,6 +508,7 @@ class ConfigManager {
|
|||||||
// Environment variables (including values loaded from ~/.t3mp3st/.env above)
|
// Environment variables (including values loaded from ~/.t3mp3st/.env above)
|
||||||
// are intentionally process-local. getApiKey() already gives env vars highest
|
// 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.
|
// 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;
|
this.envLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,7 +536,7 @@ class ConfigManager {
|
|||||||
/**
|
/**
|
||||||
* Set an API key for a provider
|
* 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');
|
const apiKeys = this.config.get('apiKeys');
|
||||||
apiKeys[provider] = key;
|
apiKeys[provider] = key;
|
||||||
this.config.set('apiKeys', apiKeys);
|
this.config.set('apiKeys', apiKeys);
|
||||||
@@ -522,13 +545,14 @@ class ConfigManager {
|
|||||||
/**
|
/**
|
||||||
* Get an API key for a provider
|
* 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)
|
// First check environment variables (highest priority)
|
||||||
const envVarMap = {
|
const envVarMap = {
|
||||||
openrouter: 'OPENROUTER_API_KEY',
|
openrouter: 'OPENROUTER_API_KEY',
|
||||||
venice: 'VENICE_API_KEY',
|
venice: 'VENICE_API_KEY',
|
||||||
anthropic: 'ANTHROPIC_API_KEY',
|
anthropic: 'ANTHROPIC_API_KEY',
|
||||||
openai: 'OPENAI_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
|
// 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
|
* 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);
|
const key = this.getApiKey(provider);
|
||||||
return !!key && key.length > 10;
|
return !!key && key.length > 10;
|
||||||
}
|
}
|
||||||
@@ -555,7 +579,7 @@ class ConfigManager {
|
|||||||
/**
|
/**
|
||||||
* Remove an API key
|
* 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');
|
const apiKeys = this.config.get('apiKeys');
|
||||||
delete apiKeys[provider];
|
delete apiKeys[provider];
|
||||||
this.config.set('apiKeys', apiKeys);
|
this.config.set('apiKeys', apiKeys);
|
||||||
@@ -571,6 +595,7 @@ class ConfigManager {
|
|||||||
if (this.hasApiKey('venice')) providers.push('venice');
|
if (this.hasApiKey('venice')) providers.push('venice');
|
||||||
if (this.hasApiKey('anthropic')) providers.push('anthropic');
|
if (this.hasApiKey('anthropic')) providers.push('anthropic');
|
||||||
if (this.hasApiKey('openai')) providers.push('openai');
|
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.
|
// Codex uses the local Codex CLI/account auth instead of API-key storage.
|
||||||
providers.push('codex');
|
providers.push('codex');
|
||||||
@@ -612,6 +637,12 @@ class ConfigManager {
|
|||||||
baseUrl = this.config.get('openai').baseUrl;
|
baseUrl = this.config.get('openai').baseUrl;
|
||||||
actualModel = model || this.config.get('openai').defaultModel;
|
actualModel = model || this.config.get('openai').defaultModel;
|
||||||
break;
|
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':
|
case 'codex':
|
||||||
actualModel = model || this.config.get('codex').defaultModel;
|
actualModel = model || this.config.get('codex').defaultModel;
|
||||||
break;
|
break;
|
||||||
@@ -657,7 +688,7 @@ class ConfigManager {
|
|||||||
const flag = (process.env.TEMPEST_MODEL_FALLBACK || '').trim().toLowerCase();
|
const flag = (process.env.TEMPEST_MODEL_FALLBACK || '').trim().toLowerCase();
|
||||||
if (!flag || ['0', 'false', 'off', 'no'].includes(flag)) return [];
|
if (!flag || ['0', 'false', 'off', 'no'].includes(flag)) return [];
|
||||||
const chain: FallbackEntry[] = [];
|
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;
|
if (p === primary || !this.hasApiKey(p)) return;
|
||||||
chain.push({
|
chain.push({
|
||||||
provider: p,
|
provider: p,
|
||||||
@@ -670,6 +701,7 @@ class ConfigManager {
|
|||||||
add('venice');
|
add('venice');
|
||||||
add('anthropic');
|
add('anthropic');
|
||||||
add('openai');
|
add('openai');
|
||||||
|
add('xai');
|
||||||
return chain;
|
return chain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,8 +819,8 @@ OPENAI_API_KEY=
|
|||||||
export const config = new ConfigManager();
|
export const config = new ConfigManager();
|
||||||
|
|
||||||
// Helper functions for quick access
|
// Helper functions for quick access
|
||||||
export const getApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai') => config.getApiKey(provider);
|
export const getApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai') => config.getApiKey(provider);
|
||||||
export const setApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai', key: string) => config.setApiKey(provider, key);
|
export const setApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai', key: string) => config.setApiKey(provider, key);
|
||||||
export const hasApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai') => config.hasApiKey(provider);
|
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 getLLMConfig = (provider?: LLMProvider, model?: string) => config.getLLMConfig(provider, model);
|
||||||
export const getConfiguredProviders = () => config.getConfiguredProviders();
|
export const getConfiguredProviders = () => config.getConfiguredProviders();
|
||||||
@@ -1201,6 +1201,8 @@ export class LLMBackbone extends EventEmitter<LLMEvents> {
|
|||||||
return new AnthropicAdapter(config);
|
return new AnthropicAdapter(config);
|
||||||
case 'openai':
|
case 'openai':
|
||||||
return new OpenAIAdapter(config);
|
return new OpenAIAdapter(config);
|
||||||
|
case 'xai':
|
||||||
|
return new OpenAIAdapter(config); // xAI (Grok Build / grok-*) is OpenAI-compatible
|
||||||
case 'codex':
|
case 'codex':
|
||||||
return new CodexAdapter(config);
|
return new CodexAdapter(config);
|
||||||
case 'mock':
|
case 'mock':
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
// LLM CONFIGURATION
|
// 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 {
|
export interface LLMConfig {
|
||||||
provider: LLMProvider;
|
provider: LLMProvider;
|
||||||
|
|||||||
Reference in new issue
Block a user