|
| 1 | +import { xAI } from '@genkit-ai/compat-oai/xai'; |
| 2 | +import { GenkitPlugin, GenkitPluginV2 } from 'genkit/plugin'; |
| 3 | +import { RateLimiter } from 'limiter'; |
| 4 | +import fetch from 'node-fetch'; |
| 5 | +import { |
| 6 | + GenkitModelProvider, |
| 7 | + PromptDataForCounting, |
| 8 | + RateLimitConfig, |
| 9 | +} from '../model-provider.js'; |
| 10 | + |
| 11 | +export class GrokModelProvider extends GenkitModelProvider { |
| 12 | + readonly apiKeyVariableName = 'XAI_API_KEY'; |
| 13 | + |
| 14 | + protected readonly models = { |
| 15 | + 'grok-4': () => xAI.model('grok-4'), |
| 16 | + 'grok-code-fast-1': () => xAI.model('grok-code-fast-1'), |
| 17 | + }; |
| 18 | + |
| 19 | + private async countTokensWithXaiApi( |
| 20 | + prompt: PromptDataForCounting |
| 21 | + ): Promise<number | null> { |
| 22 | + const apiKey = this.getApiKey(); |
| 23 | + if (!apiKey) { |
| 24 | + return null; |
| 25 | + } |
| 26 | + |
| 27 | + try { |
| 28 | + // Use xAI's tokenize API for accurate token counting |
| 29 | + const messages = this.genkitPromptToXaiFormat(prompt); |
| 30 | + const text = messages.map((m) => `${m.role}: ${m.content}`).join('\n'); |
| 31 | + |
| 32 | + const response = await fetch('https://api.x.ai/v1/tokenize', { |
| 33 | + method: 'POST', |
| 34 | + headers: { |
| 35 | + 'Content-Type': 'application/json', |
| 36 | + Authorization: `Bearer ${apiKey}`, |
| 37 | + }, |
| 38 | + body: JSON.stringify({ text }), |
| 39 | + }); |
| 40 | + |
| 41 | + if (response.ok) { |
| 42 | + const data = (await response.json()) as { tokens: unknown[] }; |
| 43 | + return data.tokens?.length || 0; |
| 44 | + } |
| 45 | + return null; |
| 46 | + } catch (error) { |
| 47 | + console.warn('Failed to count tokens using xAI API', error); |
| 48 | + return null; |
| 49 | + } |
| 50 | + } |
| 51 | + |
| 52 | + private async countTokensForModel( |
| 53 | + _modelName: string, |
| 54 | + prompt: PromptDataForCounting |
| 55 | + ): Promise<number> { |
| 56 | + const xaiTokenCount = await this.countTokensWithXaiApi(prompt); |
| 57 | + if (xaiTokenCount !== null) { |
| 58 | + return xaiTokenCount; |
| 59 | + } |
| 60 | + return 0; |
| 61 | + } |
| 62 | + |
| 63 | + protected rateLimitConfig: Record<string, RateLimitConfig> = { |
| 64 | + // XAI Grok rate limits https://docs.x.ai/docs/models |
| 65 | + 'xai/grok-4': { |
| 66 | + requestPerMinute: new RateLimiter({ |
| 67 | + tokensPerInterval: 480, |
| 68 | + interval: 1000 * 60 * 1.5, // Refresh tokens after 1.5 minutes to be on the safe side |
| 69 | + }), |
| 70 | + tokensPerMinute: new RateLimiter({ |
| 71 | + tokensPerInterval: 2_000_000 * 0.75, |
| 72 | + interval: 1000 * 60 * 1.5, // Refresh tokens after 1.5 minutes to be on the safe side |
| 73 | + }), |
| 74 | + countTokens: (prompt) => this.countTokensForModel('grok-4', prompt), |
| 75 | + }, |
| 76 | + 'xai/grok-code-fast-1': { |
| 77 | + requestPerMinute: new RateLimiter({ |
| 78 | + tokensPerInterval: 480, |
| 79 | + interval: 1000 * 60 * 1.5, // Refresh tokens after 1.5 minutes to be on the safe side |
| 80 | + }), |
| 81 | + tokensPerMinute: new RateLimiter({ |
| 82 | + tokensPerInterval: 2_000_000 * 0.75, |
| 83 | + interval: 1000 * 60 * 1.5, // Refresh tokens after 1.5 minutes to be on the safe side |
| 84 | + }), |
| 85 | + countTokens: (prompt) => |
| 86 | + this.countTokensForModel('grok-code-fast-1', prompt), |
| 87 | + }, |
| 88 | + }; |
| 89 | + |
| 90 | + protected pluginFactory(apiKey: string): GenkitPlugin | GenkitPluginV2 { |
| 91 | + return xAI({ apiKey }); |
| 92 | + } |
| 93 | + |
| 94 | + getModelSpecificConfig(): object { |
| 95 | + // Grok doesn't require special configuration at this time |
| 96 | + return {}; |
| 97 | + } |
| 98 | + |
| 99 | + private genkitPromptToXaiFormat( |
| 100 | + prompt: PromptDataForCounting |
| 101 | + ): Array<{ role: string; content: string }> { |
| 102 | + const xaiPrompt: Array<{ role: string; content: string }> = []; |
| 103 | + for (const part of prompt.messages) { |
| 104 | + for (const c of part.content) { |
| 105 | + xaiPrompt.push({ |
| 106 | + role: part.role, |
| 107 | + content: 'media' in c ? c.media.url : c.text, |
| 108 | + }); |
| 109 | + } |
| 110 | + } |
| 111 | + return [...xaiPrompt, { role: 'user', content: prompt.prompt }]; |
| 112 | + } |
| 113 | +} |
0 commit comments