refactor: enhance error handling and normalization across AI engines

This update introduces a centralized error handling mechanism for various AI engines, improving the consistency and clarity of error messages. The new `normalizeEngineError` function standardizes error responses, allowing for better user feedback and recovery suggestions. Additionally, specific error classes for insufficient credits, rate limits, and service availability have been implemented, along with user-friendly formatting for error messages. This refactor aims to enhance the overall user experience when interacting with the AI services.
This commit is contained in:
di-sukharev
2026-01-17 23:34:49 +03:00
parent 8b0ee25923
commit 5b241ed2d0
16 changed files with 1081 additions and 412 deletions

View File

@@ -1,5 +1,6 @@
import OpenAI from 'openai';
import axios, { AxiosInstance } from 'axios';
import { normalizeEngineError } from '../utils/engineErrorHandler';
import { AiEngine, AiEngineConfig } from './Engine';
interface AimlApiConfig extends AiEngineConfig {}
@@ -32,16 +33,7 @@ export class AimlApiEngine implements AiEngine {
const message = response.data.choices?.[0]?.message;
return message?.content ?? null;
} catch (error) {
const err = error as Error;
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const apiError = error.response.data.error;
if (apiError) throw new Error(apiError.message);
}
throw err;
throw normalizeEngineError(error, 'aimlapi', this.config.model);
}
};
}

View File

@@ -3,12 +3,9 @@ import {
MessageCreateParamsNonStreaming,
MessageParam
} from '@anthropic-ai/sdk/resources/messages.mjs';
import { outro } from '@clack/prompts';
import axios from 'axios';
import chalk from 'chalk';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { ModelNotFoundError } from '../utils/errors';
import { normalizeEngineError } from '../utils/engineErrorHandler';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine';
@@ -59,41 +56,7 @@ export class AnthropicEngine implements AiEngine {
let content = message;
return removeContentTags(content, 'think');
} catch (error) {
const err = error as Error;
// Check for model not found errors
if (err.message?.toLowerCase().includes('model') &&
(err.message?.toLowerCase().includes('not found') ||
err.message?.toLowerCase().includes('does not exist') ||
err.message?.toLowerCase().includes('invalid'))) {
throw new ModelNotFoundError(this.config.model, 'anthropic', 404);
}
// Check for 404 errors
if ('status' in (error as any) && (error as any).status === 404) {
throw new ModelNotFoundError(this.config.model, 'anthropic', 404);
}
outro(`${chalk.red('✖')} ${err?.message || err}`);
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const anthropicAiError = error.response.data.error;
if (anthropicAiError?.message) outro(anthropicAiError.message);
outro(
'For help look into README https://github.com/di-sukharev/opencommit#setup'
);
}
// Check axios 404 errors
if (axios.isAxiosError(error) && error.response?.status === 404) {
throw new ModelNotFoundError(this.config.model, 'anthropic', 404);
}
throw err;
throw normalizeEngineError(error, 'anthropic', this.config.model);
}
};
}

View File

@@ -2,11 +2,9 @@ import {
AzureKeyCredential,
OpenAIClient as AzureOpenAIClient
} from '@azure/openai';
import { outro } from '@clack/prompts';
import axios from 'axios';
import chalk from 'chalk';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { normalizeEngineError } from '../utils/engineErrorHandler';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine';
@@ -57,24 +55,7 @@ export class AzureEngine implements AiEngine {
let content = message?.content;
return removeContentTags(content, 'think');
} catch (error) {
outro(`${chalk.red('✖')} ${this.config.model}`);
const err = error as Error;
outro(`${chalk.red('✖')} ${JSON.stringify(error)}`);
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const openAiError = error.response.data.error;
if (openAiError?.message) outro(openAiError.message);
outro(
'For help look into README https://github.com/di-sukharev/opencommit#setup'
);
}
throw err;
throw normalizeEngineError(error, 'azure', this.config.model);
}
};
}

View File

@@ -1,6 +1,6 @@
import axios from 'axios';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { normalizeEngineError } from '../utils/engineErrorHandler';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount';
import { OpenAiEngine, OpenAiConfig } from './openAi';
@@ -45,17 +45,7 @@ export class DeepseekEngine extends OpenAiEngine {
let content = message?.content;
return removeContentTags(content, 'think');
} catch (error) {
const err = error as Error;
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const openAiError = error.response.data.error;
if (openAiError) throw new Error(openAiError.message);
}
throw err;
throw normalizeEngineError(error, 'deepseek', this.config.model);
}
};
}

View File

@@ -1,5 +1,6 @@
import axios, { AxiosInstance } from 'axios';
import { OpenAI } from 'openai';
import { normalizeEngineError } from '../utils/engineErrorHandler';
import { removeContentTags } from '../utils/removeContentTags';
import { AiEngine, AiEngineConfig } from './Engine';
@@ -39,9 +40,8 @@ export class FlowiseEngine implements AiEngine {
const message = response.data;
let content = message?.text;
return removeContentTags(content, 'think');
} catch (err: any) {
const message = err.response?.data?.error ?? err.message;
throw new Error('local model issues. details: ' + message);
} catch (error) {
throw normalizeEngineError(error, 'flowise', this.config.model);
}
}
}

View File

@@ -5,9 +5,8 @@ import {
HarmCategory,
Part
} from '@google/generative-ai';
import axios from 'axios';
import { OpenAI } from 'openai';
import { ModelNotFoundError } from '../utils/errors';
import { normalizeEngineError } from '../utils/engineErrorHandler';
import { removeContentTags } from '../utils/removeContentTags';
import { AiEngine, AiEngineConfig } from './Engine';
@@ -76,30 +75,7 @@ export class GeminiEngine implements AiEngine {
const content = result.response.text();
return removeContentTags(content, 'think');
} catch (error) {
const err = error as Error;
// Check for model not found errors
if (err.message?.toLowerCase().includes('model') &&
(err.message?.toLowerCase().includes('not found') ||
err.message?.toLowerCase().includes('does not exist') ||
err.message?.toLowerCase().includes('invalid'))) {
throw new ModelNotFoundError(this.config.model, 'gemini', 404);
}
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const geminiError = error.response.data.error;
if (geminiError) throw new Error(geminiError?.message);
}
// Check axios 404 errors
if (axios.isAxiosError(error) && error.response?.status === 404) {
throw new ModelNotFoundError(this.config.model, 'gemini', 404);
}
throw err;
throw normalizeEngineError(error, 'gemini', this.config.model);
}
}
}

View File

@@ -1,6 +1,6 @@
import axios from 'axios';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { normalizeEngineError } from '../utils/engineErrorHandler';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine';
@@ -63,17 +63,7 @@ export class MistralAiEngine implements AiEngine {
let content = message.content as string;
return removeContentTags(content, 'think');
} catch (error) {
const err = error as Error;
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const mistralError = error.response.data.error;
if (mistralError) throw new Error(mistralError.message);
}
throw err;
throw normalizeEngineError(error, 'mistral', this.config.model);
}
};
}

View File

@@ -1,5 +1,6 @@
import axios, { AxiosInstance } from 'axios';
import { OpenAI } from 'openai';
import { normalizeEngineError } from '../utils/engineErrorHandler';
import { removeContentTags } from '../utils/removeContentTags';
import { AiEngine, AiEngineConfig } from './Engine';
@@ -39,9 +40,8 @@ export class MLXEngine implements AiEngine {
const message = choices[0].message;
let content = message?.content;
return removeContentTags(content, 'think');
} catch (err: any) {
const message = err.response?.data?.error ?? err.message;
throw new Error(`MLX provider error: ${message}`);
} catch (error) {
throw normalizeEngineError(error, 'mlx', this.config.model);
}
}
}

View File

@@ -1,6 +1,6 @@
import axios, { AxiosInstance } from 'axios';
import { OpenAI } from 'openai';
import { ModelNotFoundError } from '../utils/errors';
import { normalizeEngineError } from '../utils/engineErrorHandler';
import { removeContentTags } from '../utils/removeContentTags';
import { AiEngine, AiEngineConfig } from './Engine';
@@ -45,23 +45,8 @@ export class OllamaEngine implements AiEngine {
const { message } = response.data;
let content = message?.content;
return removeContentTags(content, 'think');
} catch (err: any) {
const message = err.response?.data?.error ?? err.message;
// Check for model not found errors
if (message?.toLowerCase().includes('model') &&
(message?.toLowerCase().includes('not found') ||
message?.toLowerCase().includes('does not exist') ||
message?.toLowerCase().includes('pull'))) {
throw new ModelNotFoundError(this.config.model, 'ollama', 404);
}
// Check for 404 status
if (err.response?.status === 404) {
throw new ModelNotFoundError(this.config.model, 'ollama', 404);
}
throw new Error(`Ollama provider error: ${message}`);
} catch (error) {
throw normalizeEngineError(error, 'ollama', this.config.model);
}
}
}

View File

@@ -1,8 +1,7 @@
import axios from 'axios';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { parseCustomHeaders } from '../utils/engine';
import { ModelNotFoundError } from '../utils/errors';
import { normalizeEngineError } from '../utils/engineErrorHandler';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine';
@@ -62,36 +61,7 @@ export class OpenAiEngine implements AiEngine {
let content = message?.content;
return removeContentTags(content, 'think');
} catch (error) {
const err = error as Error;
// Check for model not found errors
if (err.message?.toLowerCase().includes('model') &&
(err.message?.toLowerCase().includes('not found') ||
err.message?.toLowerCase().includes('does not exist') ||
err.message?.toLowerCase().includes('invalid'))) {
throw new ModelNotFoundError(this.config.model, 'openai', 404);
}
// Check for 404 errors from API
if ('status' in (error as any) && (error as any).status === 404) {
throw new ModelNotFoundError(this.config.model, 'openai', 404);
}
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const openAiError = error.response.data.error;
if (openAiError) throw new Error(openAiError.message);
}
// Check axios 404 errors
if (axios.isAxiosError(error) && error.response?.status === 404) {
throw new ModelNotFoundError(this.config.model, 'openai', 404);
}
throw err;
throw normalizeEngineError(error, 'openai', this.config.model);
}
};
}

View File

@@ -1,7 +1,8 @@
import OpenAI from 'openai';
import { AiEngine, AiEngineConfig } from './Engine';
import axios, { AxiosInstance } from 'axios';
import { normalizeEngineError } from '../utils/engineErrorHandler';
import { removeContentTags } from '../utils/removeContentTags';
import { AiEngine, AiEngineConfig } from './Engine';
interface OpenRouterConfig extends AiEngineConfig {}
@@ -33,17 +34,7 @@ export class OpenRouterEngine implements AiEngine {
let content = message?.content;
return removeContentTags(content, 'think');
} catch (error) {
const err = error as Error;
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const openRouterError = error.response.data.error;
if (openRouterError) throw new Error(openRouterError.message);
}
throw err;
throw normalizeEngineError(error, 'openrouter', this.config.model);
}
};
}