Tm start (#1200)
Co-authored-by: Max Tuzzolino <maxtuzz@Maxs-MacBook-Pro.local> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Max Tuzzolino <max.tuzsmith@gmail.com> Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
This commit is contained in:
155
src/ai-providers/custom-sdk/grok-cli/errors.js
Normal file
155
src/ai-providers/custom-sdk/grok-cli/errors.js
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* @fileoverview Error handling utilities for Grok CLI provider
|
||||
*/
|
||||
|
||||
import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider';
|
||||
|
||||
/**
|
||||
* @typedef {import('./types.js').GrokCliErrorMetadata} GrokCliErrorMetadata
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create an API call error with Grok CLI specific metadata
|
||||
* @param {Object} params - Error parameters
|
||||
* @param {string} params.message - Error message
|
||||
* @param {string} [params.code] - Error code
|
||||
* @param {number} [params.exitCode] - Process exit code
|
||||
* @param {string} [params.stderr] - Standard error output
|
||||
* @param {string} [params.stdout] - Standard output
|
||||
* @param {string} [params.promptExcerpt] - Excerpt of the prompt
|
||||
* @param {boolean} [params.isRetryable=false] - Whether the error is retryable
|
||||
* @returns {APICallError}
|
||||
*/
|
||||
export function createAPICallError({
|
||||
message,
|
||||
code,
|
||||
exitCode,
|
||||
stderr,
|
||||
stdout,
|
||||
promptExcerpt,
|
||||
isRetryable = false
|
||||
}) {
|
||||
/** @type {GrokCliErrorMetadata} */
|
||||
const metadata = {
|
||||
code,
|
||||
exitCode,
|
||||
stderr,
|
||||
stdout,
|
||||
promptExcerpt
|
||||
};
|
||||
|
||||
return new APICallError({
|
||||
message,
|
||||
isRetryable,
|
||||
url: 'grok-cli://command',
|
||||
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
|
||||
data: metadata
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an authentication error
|
||||
* @param {Object} params - Error parameters
|
||||
* @param {string} params.message - Error message
|
||||
* @returns {LoadAPIKeyError}
|
||||
*/
|
||||
export function createAuthenticationError({ message }) {
|
||||
return new LoadAPIKeyError({
|
||||
message:
|
||||
message ||
|
||||
'Authentication failed. Please ensure Grok CLI is properly configured with API key.'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a timeout error
|
||||
* @param {Object} params - Error parameters
|
||||
* @param {string} params.message - Error message
|
||||
* @param {string} [params.promptExcerpt] - Excerpt of the prompt
|
||||
* @param {number} params.timeoutMs - Timeout in milliseconds
|
||||
* @returns {APICallError}
|
||||
*/
|
||||
export function createTimeoutError({ message, promptExcerpt, timeoutMs }) {
|
||||
/** @type {GrokCliErrorMetadata & { timeoutMs: number }} */
|
||||
const metadata = {
|
||||
code: 'TIMEOUT',
|
||||
promptExcerpt,
|
||||
timeoutMs
|
||||
};
|
||||
|
||||
return new APICallError({
|
||||
message,
|
||||
isRetryable: true,
|
||||
url: 'grok-cli://command',
|
||||
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
|
||||
data: metadata
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CLI installation error
|
||||
* @param {Object} params - Error parameters
|
||||
* @param {string} [params.message] - Error message
|
||||
* @returns {APICallError}
|
||||
*/
|
||||
export function createInstallationError({ message }) {
|
||||
return new APICallError({
|
||||
message:
|
||||
message ||
|
||||
'Grok CLI is not installed or not found in PATH. Please install with: npm install -g @vibe-kit/grok-cli',
|
||||
isRetryable: false,
|
||||
url: 'grok-cli://installation'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is an authentication error
|
||||
* @param {unknown} error - Error to check
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAuthenticationError(error) {
|
||||
if (error instanceof LoadAPIKeyError) return true;
|
||||
if (
|
||||
error instanceof APICallError &&
|
||||
/** @type {GrokCliErrorMetadata} */ (error.data)?.exitCode === 401
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is a timeout error
|
||||
* @param {unknown} error - Error to check
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isTimeoutError(error) {
|
||||
if (
|
||||
error instanceof APICallError &&
|
||||
/** @type {GrokCliErrorMetadata} */ (error.data)?.code === 'TIMEOUT'
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is an installation error
|
||||
* @param {unknown} error - Error to check
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isInstallationError(error) {
|
||||
if (error instanceof APICallError && error.url === 'grok-cli://installation')
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error metadata from an error
|
||||
* @param {unknown} error - Error to extract metadata from
|
||||
* @returns {GrokCliErrorMetadata|undefined}
|
||||
*/
|
||||
export function getErrorMetadata(error) {
|
||||
if (error instanceof APICallError && error.data) {
|
||||
return /** @type {GrokCliErrorMetadata} */ (error.data);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
85
src/ai-providers/custom-sdk/grok-cli/index.js
Normal file
85
src/ai-providers/custom-sdk/grok-cli/index.js
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @fileoverview Grok CLI provider factory and exports
|
||||
*/
|
||||
|
||||
import { NoSuchModelError } from '@ai-sdk/provider';
|
||||
import { GrokCliLanguageModel } from './language-model.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('./types.js').GrokCliSettings} GrokCliSettings
|
||||
* @typedef {import('./types.js').GrokCliModelId} GrokCliModelId
|
||||
* @typedef {import('./types.js').GrokCliProvider} GrokCliProvider
|
||||
* @typedef {import('./types.js').GrokCliProviderSettings} GrokCliProviderSettings
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a Grok CLI provider
|
||||
* @param {GrokCliProviderSettings} [options={}] - Provider configuration options
|
||||
* @returns {GrokCliProvider} Grok CLI provider instance
|
||||
*/
|
||||
export function createGrokCli(options = {}) {
|
||||
/**
|
||||
* Create a language model instance
|
||||
* @param {GrokCliModelId} modelId - Model ID
|
||||
* @param {GrokCliSettings} [settings={}] - Model settings
|
||||
* @returns {GrokCliLanguageModel}
|
||||
*/
|
||||
const createModel = (modelId, settings = {}) => {
|
||||
return new GrokCliLanguageModel({
|
||||
id: modelId,
|
||||
settings: {
|
||||
...options.defaultSettings,
|
||||
...settings
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider function
|
||||
* @param {GrokCliModelId} modelId - Model ID
|
||||
* @param {GrokCliSettings} [settings] - Model settings
|
||||
* @returns {GrokCliLanguageModel}
|
||||
*/
|
||||
const provider = function (modelId, settings) {
|
||||
if (new.target) {
|
||||
throw new Error(
|
||||
'The Grok CLI model function cannot be called with the new keyword.'
|
||||
);
|
||||
}
|
||||
|
||||
return createModel(modelId, settings);
|
||||
};
|
||||
|
||||
provider.languageModel = createModel;
|
||||
provider.chat = createModel; // Alias for languageModel
|
||||
|
||||
// Add textEmbeddingModel method that throws NoSuchModelError
|
||||
provider.textEmbeddingModel = (modelId) => {
|
||||
throw new NoSuchModelError({
|
||||
modelId,
|
||||
modelType: 'textEmbeddingModel'
|
||||
});
|
||||
};
|
||||
|
||||
return /** @type {GrokCliProvider} */ (provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default Grok CLI provider instance
|
||||
*/
|
||||
export const grokCli = createGrokCli();
|
||||
|
||||
// Provider exports
|
||||
export { GrokCliLanguageModel } from './language-model.js';
|
||||
|
||||
// Error handling exports
|
||||
export {
|
||||
isAuthenticationError,
|
||||
isTimeoutError,
|
||||
isInstallationError,
|
||||
getErrorMetadata,
|
||||
createAPICallError,
|
||||
createAuthenticationError,
|
||||
createTimeoutError,
|
||||
createInstallationError
|
||||
} from './errors.js';
|
||||
59
src/ai-providers/custom-sdk/grok-cli/json-extractor.js
Normal file
59
src/ai-providers/custom-sdk/grok-cli/json-extractor.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @fileoverview Extract JSON from Grok's response, handling markdown blocks and other formatting
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extract JSON from Grok's response
|
||||
* @param {string} text - The text to extract JSON from
|
||||
* @returns {string} - The extracted JSON string
|
||||
*/
|
||||
export function extractJson(text) {
|
||||
// Remove markdown code blocks if present
|
||||
let jsonText = text.trim();
|
||||
|
||||
// Remove ```json blocks
|
||||
jsonText = jsonText.replace(/^```json\s*/gm, '');
|
||||
jsonText = jsonText.replace(/^```\s*/gm, '');
|
||||
jsonText = jsonText.replace(/```\s*$/gm, '');
|
||||
|
||||
// Remove common TypeScript/JavaScript patterns
|
||||
jsonText = jsonText.replace(/^const\s+\w+\s*=\s*/, ''); // Remove "const varName = "
|
||||
jsonText = jsonText.replace(/^let\s+\w+\s*=\s*/, ''); // Remove "let varName = "
|
||||
jsonText = jsonText.replace(/^var\s+\w+\s*=\s*/, ''); // Remove "var varName = "
|
||||
jsonText = jsonText.replace(/;?\s*$/, ''); // Remove trailing semicolons
|
||||
|
||||
// Try to extract JSON object or array
|
||||
const objectMatch = jsonText.match(/{[\s\S]*}/);
|
||||
const arrayMatch = jsonText.match(/\[[\s\S]*\]/);
|
||||
|
||||
if (objectMatch) {
|
||||
jsonText = objectMatch[0];
|
||||
} else if (arrayMatch) {
|
||||
jsonText = arrayMatch[0];
|
||||
}
|
||||
|
||||
// First try to parse as valid JSON
|
||||
try {
|
||||
JSON.parse(jsonText);
|
||||
return jsonText;
|
||||
} catch {
|
||||
// If it's not valid JSON, it might be a JavaScript object literal
|
||||
// Try to convert it to valid JSON
|
||||
try {
|
||||
// This is a simple conversion that handles basic cases
|
||||
// Replace unquoted keys with quoted keys
|
||||
const converted = jsonText
|
||||
.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":')
|
||||
// Replace single quotes with double quotes
|
||||
.replace(/'/g, '"');
|
||||
|
||||
// Validate the converted JSON
|
||||
JSON.parse(converted);
|
||||
return converted;
|
||||
} catch {
|
||||
// If all else fails, return the original text
|
||||
// The AI SDK will handle the error appropriately
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
407
src/ai-providers/custom-sdk/grok-cli/language-model.js
Normal file
407
src/ai-providers/custom-sdk/grok-cli/language-model.js
Normal file
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* @fileoverview Grok CLI Language Model implementation
|
||||
*/
|
||||
|
||||
import { NoSuchModelError } from '@ai-sdk/provider';
|
||||
import { generateId } from '@ai-sdk/provider-utils';
|
||||
import {
|
||||
createPromptFromMessages,
|
||||
convertFromGrokCliResponse,
|
||||
escapeShellArg
|
||||
} from './message-converter.js';
|
||||
import { extractJson } from './json-extractor.js';
|
||||
import {
|
||||
createAPICallError,
|
||||
createAuthenticationError,
|
||||
createInstallationError,
|
||||
createTimeoutError
|
||||
} from './errors.js';
|
||||
import { spawn } from 'child_process';
|
||||
import { promises as fs } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
/**
|
||||
* @typedef {import('./types.js').GrokCliSettings} GrokCliSettings
|
||||
* @typedef {import('./types.js').GrokCliModelId} GrokCliModelId
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} GrokCliLanguageModelOptions
|
||||
* @property {GrokCliModelId} id - Model ID
|
||||
* @property {GrokCliSettings} [settings] - Model settings
|
||||
*/
|
||||
|
||||
export class GrokCliLanguageModel {
|
||||
specificationVersion = 'v1';
|
||||
defaultObjectGenerationMode = 'json';
|
||||
supportsImageUrls = false;
|
||||
supportsStructuredOutputs = false;
|
||||
|
||||
/** @type {GrokCliModelId} */
|
||||
modelId;
|
||||
|
||||
/** @type {GrokCliSettings} */
|
||||
settings;
|
||||
|
||||
/**
|
||||
* @param {GrokCliLanguageModelOptions} options
|
||||
*/
|
||||
constructor(options) {
|
||||
this.modelId = options.id;
|
||||
this.settings = options.settings ?? {};
|
||||
|
||||
// Validate model ID format
|
||||
if (
|
||||
!this.modelId ||
|
||||
typeof this.modelId !== 'string' ||
|
||||
this.modelId.trim() === ''
|
||||
) {
|
||||
throw new NoSuchModelError({
|
||||
modelId: this.modelId,
|
||||
modelType: 'languageModel'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get provider() {
|
||||
return 'grok-cli';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Grok CLI is installed and available
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async checkGrokCliInstallation() {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn('grok', ['--version'], {
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
child.on('error', () => resolve(false));
|
||||
child.on('exit', (code) => resolve(code === 0));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key from settings or environment
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
async getApiKey() {
|
||||
// Check settings first
|
||||
if (this.settings.apiKey) {
|
||||
return this.settings.apiKey;
|
||||
}
|
||||
|
||||
// Check environment variable
|
||||
if (process.env.GROK_CLI_API_KEY) {
|
||||
return process.env.GROK_CLI_API_KEY;
|
||||
}
|
||||
|
||||
// Check grok-cli config file
|
||||
try {
|
||||
const configPath = join(homedir(), '.grok', 'user-settings.json');
|
||||
const configContent = await fs.readFile(configPath, 'utf8');
|
||||
const config = JSON.parse(configContent);
|
||||
return config.apiKey || null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Grok CLI command
|
||||
* @param {Array<string>} args - Command line arguments
|
||||
* @param {Object} options - Execution options
|
||||
* @returns {Promise<{stdout: string, stderr: string, exitCode: number}>}
|
||||
*/
|
||||
async executeGrokCli(args, options = {}) {
|
||||
const timeout = options.timeout || this.settings.timeout || 120000; // 2 minutes default
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('grok', args, {
|
||||
stdio: 'pipe',
|
||||
cwd: this.settings.workingDirectory || process.cwd()
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let timeoutId;
|
||||
|
||||
// Set up timeout
|
||||
if (timeout > 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
child.kill('SIGTERM');
|
||||
reject(
|
||||
createTimeoutError({
|
||||
message: `Grok CLI command timed out after ${timeout}ms`,
|
||||
timeoutMs: timeout,
|
||||
promptExcerpt: args.join(' ').substring(0, 200)
|
||||
})
|
||||
);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
|
||||
if (error.code === 'ENOENT') {
|
||||
reject(createInstallationError({}));
|
||||
} else {
|
||||
reject(
|
||||
createAPICallError({
|
||||
message: `Failed to execute Grok CLI: ${error.message}`,
|
||||
code: error.code,
|
||||
stderr: error.message,
|
||||
isRetryable: false
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('exit', (exitCode) => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
|
||||
resolve({
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
exitCode: exitCode || 0
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unsupported parameter warnings
|
||||
* @param {Object} options - Generation options
|
||||
* @returns {Array} Warnings array
|
||||
*/
|
||||
generateUnsupportedWarnings(options) {
|
||||
const warnings = [];
|
||||
const unsupportedParams = [];
|
||||
|
||||
// Grok CLI supports some parameters but not all AI SDK parameters
|
||||
if (options.topP !== undefined) unsupportedParams.push('topP');
|
||||
if (options.topK !== undefined) unsupportedParams.push('topK');
|
||||
if (options.presencePenalty !== undefined)
|
||||
unsupportedParams.push('presencePenalty');
|
||||
if (options.frequencyPenalty !== undefined)
|
||||
unsupportedParams.push('frequencyPenalty');
|
||||
if (options.stopSequences !== undefined && options.stopSequences.length > 0)
|
||||
unsupportedParams.push('stopSequences');
|
||||
if (options.seed !== undefined) unsupportedParams.push('seed');
|
||||
|
||||
if (unsupportedParams.length > 0) {
|
||||
for (const param of unsupportedParams) {
|
||||
warnings.push({
|
||||
type: 'unsupported-setting',
|
||||
setting: param,
|
||||
details: `Grok CLI does not support the ${param} parameter. It will be ignored.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate text using Grok CLI
|
||||
* @param {Object} options - Generation options
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async doGenerate(options) {
|
||||
// Check CLI installation
|
||||
const isInstalled = await this.checkGrokCliInstallation();
|
||||
if (!isInstalled) {
|
||||
throw createInstallationError({});
|
||||
}
|
||||
|
||||
// Get API key
|
||||
const apiKey = await this.getApiKey();
|
||||
if (!apiKey) {
|
||||
throw createAuthenticationError({
|
||||
message:
|
||||
'Grok CLI API key not found. Set GROK_CLI_API_KEY environment variable or configure grok-cli.'
|
||||
});
|
||||
}
|
||||
|
||||
const prompt = createPromptFromMessages(options.prompt);
|
||||
const warnings = this.generateUnsupportedWarnings(options);
|
||||
|
||||
// Build command arguments
|
||||
const args = ['--prompt', escapeShellArg(prompt)];
|
||||
|
||||
// Add model if specified
|
||||
if (this.modelId && this.modelId !== 'default') {
|
||||
args.push('--model', this.modelId);
|
||||
}
|
||||
|
||||
// Add API key if available
|
||||
if (apiKey) {
|
||||
args.push('--api-key', apiKey);
|
||||
}
|
||||
|
||||
// Add base URL if provided in settings
|
||||
if (this.settings.baseURL) {
|
||||
args.push('--base-url', this.settings.baseURL);
|
||||
}
|
||||
|
||||
// Add working directory if specified
|
||||
if (this.settings.workingDirectory) {
|
||||
args.push('--directory', this.settings.workingDirectory);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.executeGrokCli(args, {
|
||||
timeout: this.settings.timeout
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
// Handle authentication errors
|
||||
if (
|
||||
result.stderr.toLowerCase().includes('unauthorized') ||
|
||||
result.stderr.toLowerCase().includes('authentication')
|
||||
) {
|
||||
throw createAuthenticationError({
|
||||
message: `Grok CLI authentication failed: ${result.stderr}`
|
||||
});
|
||||
}
|
||||
|
||||
throw createAPICallError({
|
||||
message: `Grok CLI failed with exit code ${result.exitCode}: ${result.stderr || 'Unknown error'}`,
|
||||
exitCode: result.exitCode,
|
||||
stderr: result.stderr,
|
||||
stdout: result.stdout,
|
||||
promptExcerpt: prompt.substring(0, 200),
|
||||
isRetryable: false
|
||||
});
|
||||
}
|
||||
|
||||
// Parse response
|
||||
const response = convertFromGrokCliResponse(result.stdout);
|
||||
let text = response.text || '';
|
||||
|
||||
// Extract JSON if in object-json mode
|
||||
if (options.mode?.type === 'object-json' && text) {
|
||||
text = extractJson(text);
|
||||
}
|
||||
|
||||
return {
|
||||
text: text || undefined,
|
||||
usage: response.usage || { promptTokens: 0, completionTokens: 0 },
|
||||
finishReason: 'stop',
|
||||
rawCall: {
|
||||
rawPrompt: prompt,
|
||||
rawSettings: args
|
||||
},
|
||||
warnings: warnings.length > 0 ? warnings : undefined,
|
||||
response: {
|
||||
id: generateId(),
|
||||
timestamp: new Date(),
|
||||
modelId: this.modelId
|
||||
},
|
||||
request: {
|
||||
body: prompt
|
||||
},
|
||||
providerMetadata: {
|
||||
'grok-cli': {
|
||||
exitCode: result.exitCode,
|
||||
stderr: result.stderr || undefined
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Re-throw our custom errors
|
||||
if (error.name === 'APICallError' || error.name === 'LoadAPIKeyError') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Wrap other errors
|
||||
throw createAPICallError({
|
||||
message: `Grok CLI execution failed: ${error.message}`,
|
||||
code: error.code,
|
||||
promptExcerpt: prompt.substring(0, 200),
|
||||
isRetryable: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream text using Grok CLI
|
||||
* Note: Grok CLI doesn't natively support streaming, so this simulates streaming
|
||||
* by generating the full response and then streaming it in chunks
|
||||
* @param {Object} options - Stream options
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async doStream(options) {
|
||||
const warnings = this.generateUnsupportedWarnings(options);
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start: async (controller) => {
|
||||
try {
|
||||
// Generate the full response first
|
||||
const result = await this.doGenerate(options);
|
||||
|
||||
// Emit response metadata
|
||||
controller.enqueue({
|
||||
type: 'response-metadata',
|
||||
id: result.response.id,
|
||||
timestamp: result.response.timestamp,
|
||||
modelId: result.response.modelId
|
||||
});
|
||||
|
||||
// Simulate streaming by chunking the text
|
||||
const text = result.text || '';
|
||||
const chunkSize = 50; // Characters per chunk
|
||||
|
||||
for (let i = 0; i < text.length; i += chunkSize) {
|
||||
const chunk = text.slice(i, i + chunkSize);
|
||||
controller.enqueue({
|
||||
type: 'text-delta',
|
||||
textDelta: chunk
|
||||
});
|
||||
|
||||
// Add small delay to simulate streaming
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
}
|
||||
|
||||
// Emit finish event
|
||||
controller.enqueue({
|
||||
type: 'finish',
|
||||
finishReason: result.finishReason,
|
||||
usage: result.usage,
|
||||
providerMetadata: result.providerMetadata
|
||||
});
|
||||
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
controller.enqueue({
|
||||
type: 'error',
|
||||
error
|
||||
});
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
stream,
|
||||
rawCall: {
|
||||
rawPrompt: createPromptFromMessages(options.prompt),
|
||||
rawSettings: {}
|
||||
},
|
||||
warnings: warnings.length > 0 ? warnings : undefined,
|
||||
request: {
|
||||
body: createPromptFromMessages(options.prompt)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
135
src/ai-providers/custom-sdk/grok-cli/message-converter.js
Normal file
135
src/ai-providers/custom-sdk/grok-cli/message-converter.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* @fileoverview Message format conversion utilities for Grok CLI provider
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./types.js').GrokCliMessage} GrokCliMessage
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert AI SDK messages to Grok CLI compatible format
|
||||
* @param {Array<Object>} messages - AI SDK message array
|
||||
* @returns {Array<GrokCliMessage>} Grok CLI compatible messages
|
||||
*/
|
||||
export function convertToGrokCliMessages(messages) {
|
||||
return messages.map((message) => {
|
||||
// Handle different message content types
|
||||
let content = '';
|
||||
|
||||
if (typeof message.content === 'string') {
|
||||
content = message.content;
|
||||
} else if (Array.isArray(message.content)) {
|
||||
// Handle multi-part content (text and images)
|
||||
content = message.content
|
||||
.filter((part) => part.type === 'text')
|
||||
.map((part) => part.text)
|
||||
.join('\n');
|
||||
} else if (message.content && typeof message.content === 'object') {
|
||||
// Handle object content
|
||||
content = message.content.text || JSON.stringify(message.content);
|
||||
}
|
||||
|
||||
return {
|
||||
role: message.role,
|
||||
content: content.trim()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Grok CLI response to AI SDK format
|
||||
* @param {string} responseText - Raw response text from Grok CLI (JSONL format)
|
||||
* @returns {Object} AI SDK compatible response object
|
||||
*/
|
||||
export function convertFromGrokCliResponse(responseText) {
|
||||
try {
|
||||
// Grok CLI outputs JSONL format - each line is a separate JSON message
|
||||
const lines = responseText
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((line) => line.trim());
|
||||
|
||||
// Parse each line as JSON and find assistant messages
|
||||
const messages = [];
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const message = JSON.parse(line);
|
||||
messages.push(message);
|
||||
} catch (parseError) {
|
||||
// Skip invalid JSON lines
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the last assistant message
|
||||
const assistantMessage = messages
|
||||
.filter((msg) => msg.role === 'assistant')
|
||||
.pop();
|
||||
|
||||
if (assistantMessage && assistantMessage.content) {
|
||||
return {
|
||||
text: assistantMessage.content,
|
||||
usage: assistantMessage.usage
|
||||
? {
|
||||
promptTokens: assistantMessage.usage.prompt_tokens || 0,
|
||||
completionTokens: assistantMessage.usage.completion_tokens || 0,
|
||||
totalTokens: assistantMessage.usage.total_tokens || 0
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback: if no assistant message found, return the raw text
|
||||
return {
|
||||
text: responseText.trim(),
|
||||
usage: undefined
|
||||
};
|
||||
} catch (error) {
|
||||
// If parsing fails completely, treat as plain text response
|
||||
return {
|
||||
text: responseText.trim(),
|
||||
usage: undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a prompt string for Grok CLI from messages
|
||||
* @param {Array<Object>} messages - AI SDK message array
|
||||
* @returns {string} Formatted prompt string
|
||||
*/
|
||||
export function createPromptFromMessages(messages) {
|
||||
const grokMessages = convertToGrokCliMessages(messages);
|
||||
|
||||
// Create a conversation-style prompt
|
||||
const prompt = grokMessages
|
||||
.map((message) => {
|
||||
switch (message.role) {
|
||||
case 'system':
|
||||
return `System: ${message.content}`;
|
||||
case 'user':
|
||||
return `User: ${message.content}`;
|
||||
case 'assistant':
|
||||
return `Assistant: ${message.content}`;
|
||||
default:
|
||||
return `${message.role}: ${message.content}`;
|
||||
}
|
||||
})
|
||||
.join('\n\n');
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape shell arguments for safe CLI execution
|
||||
* @param {string} arg - Argument to escape
|
||||
* @returns {string} Shell-escaped argument
|
||||
*/
|
||||
export function escapeShellArg(arg) {
|
||||
if (typeof arg !== 'string') {
|
||||
arg = String(arg);
|
||||
}
|
||||
|
||||
// Replace single quotes with '\''
|
||||
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
||||
}
|
||||
56
src/ai-providers/custom-sdk/grok-cli/types.js
Normal file
56
src/ai-providers/custom-sdk/grok-cli/types.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @fileoverview Type definitions for Grok CLI provider
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} GrokCliSettings
|
||||
* @property {string} [apiKey] - API key for Grok CLI
|
||||
* @property {string} [baseURL] - Base URL for Grok API
|
||||
* @property {string} [model] - Default model to use
|
||||
* @property {number} [timeout] - Timeout in milliseconds
|
||||
* @property {string} [workingDirectory] - Working directory for CLI commands
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {string} GrokCliModelId
|
||||
* Model identifiers supported by Grok CLI
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} GrokCliErrorMetadata
|
||||
* @property {string} [code] - Error code
|
||||
* @property {number} [exitCode] - Process exit code
|
||||
* @property {string} [stderr] - Standard error output
|
||||
* @property {string} [stdout] - Standard output
|
||||
* @property {string} [promptExcerpt] - Excerpt of the prompt that caused the error
|
||||
* @property {number} [timeoutMs] - Timeout value in milliseconds
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Function} GrokCliProvider
|
||||
* @property {Function} languageModel - Create a language model
|
||||
* @property {Function} chat - Alias for languageModel
|
||||
* @property {Function} textEmbeddingModel - Text embedding model (throws error)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} GrokCliProviderSettings
|
||||
* @property {GrokCliSettings} [defaultSettings] - Default settings for all models
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} GrokCliMessage
|
||||
* @property {string} role - Message role (user, assistant, system)
|
||||
* @property {string} content - Message content
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} GrokCliResponse
|
||||
* @property {string} content - Response content
|
||||
* @property {Object} [usage] - Token usage information
|
||||
* @property {number} [usage.prompt_tokens] - Input tokens used
|
||||
* @property {number} [usage.completion_tokens] - Output tokens used
|
||||
* @property {number} [usage.total_tokens] - Total tokens used
|
||||
*/
|
||||
|
||||
export {};
|
||||
79
src/ai-providers/grok-cli.js
Normal file
79
src/ai-providers/grok-cli.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* grok-cli.js
|
||||
* AI provider implementation for Grok models using Grok CLI.
|
||||
*/
|
||||
|
||||
import { createGrokCli } from './custom-sdk/grok-cli/index.js';
|
||||
import { BaseAIProvider } from './base-provider.js';
|
||||
import { getGrokCliSettingsForCommand } from '../../scripts/modules/config-manager.js';
|
||||
|
||||
export class GrokCliProvider extends BaseAIProvider {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'Grok CLI';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the environment variable name required for this provider's API key.
|
||||
* @returns {string} The environment variable name for the Grok API key
|
||||
*/
|
||||
getRequiredApiKeyName() {
|
||||
return 'GROK_CLI_API_KEY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to indicate that API key is optional since Grok CLI can be configured separately
|
||||
* @returns {boolean} False since Grok CLI can use its own config
|
||||
*/
|
||||
isRequiredApiKey() {
|
||||
return false; // Grok CLI can use its own config file
|
||||
}
|
||||
|
||||
/**
|
||||
* Override validateAuth to be more flexible with API key validation
|
||||
* @param {object} params - Parameters to validate
|
||||
*/
|
||||
validateAuth(params) {
|
||||
// Grok CLI can work with:
|
||||
// 1. API key passed in params
|
||||
// 2. Environment variable GROK_CLI_API_KEY
|
||||
// 3. Grok CLI's own config file (~/.grok/user-settings.json)
|
||||
// So we don't enforce API key requirement here
|
||||
// Suppress unused parameter warning
|
||||
void params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a Grok CLI client instance.
|
||||
* @param {object} params - Parameters for client initialization
|
||||
* @param {string} [params.apiKey] - Grok CLI API key (optional if configured in CLI)
|
||||
* @param {string} [params.baseURL] - Optional custom API endpoint
|
||||
* @param {string} [params.workingDirectory] - Working directory for CLI commands
|
||||
* @param {number} [params.timeout] - Timeout for CLI commands in milliseconds
|
||||
* @param {string} [params.commandName] - Name of the command invoking the service
|
||||
* @returns {Function} Grok CLI client function
|
||||
* @throws {Error} If initialization fails
|
||||
*/
|
||||
getClient(params) {
|
||||
try {
|
||||
const { apiKey, baseURL, workingDirectory, timeout, commandName } =
|
||||
params;
|
||||
|
||||
// Get Grok CLI settings from config
|
||||
const grokCliSettings = getGrokCliSettingsForCommand(commandName);
|
||||
|
||||
return createGrokCli({
|
||||
defaultSettings: {
|
||||
apiKey,
|
||||
baseURL,
|
||||
workingDirectory:
|
||||
workingDirectory || grokCliSettings.workingDirectory,
|
||||
timeout: timeout || grokCliSettings.timeout,
|
||||
defaultModel: grokCliSettings.defaultModel
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleError('client initialization', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,3 +16,4 @@ export { AzureProvider } from './azure.js';
|
||||
export { VertexAIProvider } from './google-vertex.js';
|
||||
export { ClaudeCodeProvider } from './claude-code.js';
|
||||
export { GeminiCliProvider } from './gemini-cli.js';
|
||||
export { GrokCliProvider } from './grok-cli.js';
|
||||
|
||||
@@ -23,7 +23,8 @@ export const CUSTOM_PROVIDERS = {
|
||||
OLLAMA: 'ollama',
|
||||
CLAUDE_CODE: 'claude-code',
|
||||
MCP: 'mcp',
|
||||
GEMINI_CLI: 'gemini-cli'
|
||||
GEMINI_CLI: 'gemini-cli',
|
||||
GROK_CLI: 'grok-cli'
|
||||
};
|
||||
|
||||
// Custom providers array (for backward compatibility and iteration)
|
||||
|
||||
Reference in New Issue
Block a user