// src/utils/grokClient.js import axios from 'axios'; import { logger } from './logger.js'; const GROK_API_URL = process.env.XAI_URL; const GROK_API_KEY = process.env.XAI_API_KEY; const GROK_MODEL_DEFAULT = process.env.XAI_MODEL; // Default system prompt for generic summarization / analysis callers. // A caller can override this via `options.system` (e.g. the ticket // classifier ships its own JSON-mode instructions). const DEFAULT_SYSTEM_PROMPT = 'You are a concise, professional IT support analyst. Be factual and clear.'; /** * Call Grok (xAI) with a prompt. * * Backward-compatible signature. Existing callers passing only * `{ temperature, max_tokens }` still get a string back. * * @param {string} prompt User prompt (message content). * @param {object} [options] * @param {number} [options.temperature=0.3] * @param {number} [options.max_tokens=400] * @param {string} [options.system] Override for the system prompt. * @param {string} [options.model] Override for XAI_MODEL (per-call). * @param {object} [options.response_format] OpenAI-compat response * format hint, e.g. `{ type: 'json_object' }` to force JSON output. * @param {boolean} [options.includeUsage=false] When true, return * `{ content, usage }` instead of just the content string. Preserves * the string return for every existing caller that didn't opt in. * @param {number} [options.timeout=15000] Per-request timeout in ms. * * @returns {Promise} * String by default. Object with `content` + `usage` when * `options.includeUsage` is true. `usage` follows the OpenAI shape * `{ prompt_tokens, completion_tokens, total_tokens }` — mirror what * xAI returns, undefined if the API omitted it. */ export async function callGrok(prompt, options = {}) { if (!GROK_API_KEY) { logger('grok:client', 'Missing XAI_API_KEY', 'error'); throw new Error('Grok API key not configured'); } const model = options.model || GROK_MODEL_DEFAULT; const payload = { model, messages: [ { role: 'system', content: options.system || DEFAULT_SYSTEM_PROMPT }, { role: 'user', content: prompt }, ], temperature: options.temperature ?? 0.3, max_tokens: options.max_tokens ?? 400, top_p: 0.95, }; if (options.response_format) { // OpenAI-compatible JSON mode. xAI supports this on Grok-3+; older // model choices silently ignore it (they'll still return text but // often wrapped in ```json fences that the caller has to strip). payload.response_format = options.response_format; } try { const response = await axios.post(GROK_API_URL, payload, { headers: { Authorization: `Bearer ${GROK_API_KEY}`, 'Content-Type': 'application/json', }, timeout: options.timeout ?? 15000, }); const content = response.data.choices?.[0]?.message?.content?.trim(); if (options.includeUsage) { return { content, usage: response.data.usage }; } return content; } catch (err) { logger('grok:client', `API error: ${err.response?.data || err.message}`, 'error'); throw err; } }