mirror of
https://github.com/di-sukharev/opencommit.git
synced 2026-04-20 03:02:51 -04:00
fix: stabilize e2e flow in clean CI env
Signed-off-by: majiayu000 <majiayu000@users.noreply.github.com>
This commit is contained in:
@@ -11,10 +11,7 @@ import {
|
||||
import chalk from 'chalk';
|
||||
import { execa } from 'execa';
|
||||
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
|
||||
import {
|
||||
formatUserFriendlyError,
|
||||
printFormattedError
|
||||
} from '../utils/errors';
|
||||
import { formatUserFriendlyError, printFormattedError } from '../utils/errors';
|
||||
import {
|
||||
assertGitRepo,
|
||||
getChangedFiles,
|
||||
|
||||
@@ -849,7 +849,8 @@ export enum OCO_AI_PROVIDER_ENUM {
|
||||
|
||||
export const PROVIDER_API_KEY_URLS: Record<string, string | null> = {
|
||||
[OCO_AI_PROVIDER_ENUM.OPENAI]: 'https://platform.openai.com/api-keys',
|
||||
[OCO_AI_PROVIDER_ENUM.ANTHROPIC]: 'https://console.anthropic.com/settings/keys',
|
||||
[OCO_AI_PROVIDER_ENUM.ANTHROPIC]:
|
||||
'https://console.anthropic.com/settings/keys',
|
||||
[OCO_AI_PROVIDER_ENUM.GEMINI]: 'https://aistudio.google.com/app/apikey',
|
||||
[OCO_AI_PROVIDER_ENUM.GROQ]: 'https://console.groq.com/keys',
|
||||
[OCO_AI_PROVIDER_ENUM.MISTRAL]: 'https://console.mistral.ai/api-keys/',
|
||||
@@ -872,7 +873,7 @@ export const RECOMMENDED_MODELS: Record<string, string> = {
|
||||
[OCO_AI_PROVIDER_ENUM.DEEPSEEK]: 'deepseek-chat',
|
||||
[OCO_AI_PROVIDER_ENUM.OPENROUTER]: 'openai/gpt-4o-mini',
|
||||
[OCO_AI_PROVIDER_ENUM.AIMLAPI]: 'gpt-4o-mini'
|
||||
}
|
||||
};
|
||||
|
||||
export type ConfigType = {
|
||||
[CONFIG_KEYS.OCO_API_KEY]?: string;
|
||||
|
||||
@@ -2,11 +2,7 @@ import { intro, outro, spinner } from '@clack/prompts';
|
||||
import chalk from 'chalk';
|
||||
import { command } from 'cleye';
|
||||
import { COMMANDS } from './ENUMS';
|
||||
import {
|
||||
MODEL_LIST,
|
||||
OCO_AI_PROVIDER_ENUM,
|
||||
getConfig
|
||||
} from './config';
|
||||
import { MODEL_LIST, OCO_AI_PROVIDER_ENUM, getConfig } from './config';
|
||||
import {
|
||||
fetchModelsForProvider,
|
||||
clearModelCache,
|
||||
@@ -31,7 +27,10 @@ function formatCacheAge(timestamp: number | null): string {
|
||||
return 'just now';
|
||||
}
|
||||
|
||||
async function listModels(provider: string, useCache: boolean = true): Promise<void> {
|
||||
async function listModels(
|
||||
provider: string,
|
||||
useCache: boolean = true
|
||||
): Promise<void> {
|
||||
const config = getConfig();
|
||||
const apiKey = config.OCO_API_KEY;
|
||||
const currentModel = config.OCO_MODEL;
|
||||
@@ -52,7 +51,9 @@ async function listModels(provider: string, useCache: boolean = true): Promise<v
|
||||
models = MODEL_LIST[providerKey] || [];
|
||||
}
|
||||
|
||||
console.log(`\n${chalk.bold('Available models for')} ${chalk.cyan(provider)}:\n`);
|
||||
console.log(
|
||||
`\n${chalk.bold('Available models for')} ${chalk.cyan(provider)}:\n`
|
||||
);
|
||||
|
||||
if (models.length === 0) {
|
||||
console.log(chalk.dim(' No models found'));
|
||||
@@ -79,14 +80,23 @@ async function refreshModels(provider: string): Promise<void> {
|
||||
clearModelCache();
|
||||
|
||||
try {
|
||||
const models = await fetchModelsForProvider(provider, apiKey, undefined, true);
|
||||
const models = await fetchModelsForProvider(
|
||||
provider,
|
||||
apiKey,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
loadingSpinner.stop(`${chalk.green('+')} Fetched ${models.length} models`);
|
||||
|
||||
// List the models
|
||||
await listModels(provider, true);
|
||||
} catch (error) {
|
||||
loadingSpinner.stop(chalk.red('Failed to fetch models'));
|
||||
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +122,8 @@ export const modelsCommand = command(
|
||||
},
|
||||
async ({ flags }) => {
|
||||
const config = getConfig();
|
||||
const provider = flags.provider || config.OCO_AI_PROVIDER || OCO_AI_PROVIDER_ENUM.OPENAI;
|
||||
const provider =
|
||||
flags.provider || config.OCO_AI_PROVIDER || OCO_AI_PROVIDER_ENUM.OPENAI;
|
||||
|
||||
intro(chalk.bgCyan(' OpenCommit Models '));
|
||||
|
||||
@@ -120,7 +131,9 @@ export const modelsCommand = command(
|
||||
const cacheInfo = getCacheInfo();
|
||||
if (cacheInfo.timestamp) {
|
||||
console.log(
|
||||
chalk.dim(` Cache last updated: ${formatCacheAge(cacheInfo.timestamp)}`)
|
||||
chalk.dim(
|
||||
` Cache last updated: ${formatCacheAge(cacheInfo.timestamp)}`
|
||||
)
|
||||
);
|
||||
if (cacheInfo.providers.length > 0) {
|
||||
console.log(
|
||||
@@ -137,8 +150,6 @@ export const modelsCommand = command(
|
||||
await listModels(provider);
|
||||
}
|
||||
|
||||
outro(
|
||||
`Run ${chalk.cyan('oco models --refresh')} to update the model list`
|
||||
);
|
||||
outro(`Run ${chalk.cyan('oco models --refresh')} to update the model list`);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -52,6 +52,12 @@ const OTHER_PROVIDERS = [
|
||||
];
|
||||
|
||||
const NO_API_KEY_PROVIDERS = [
|
||||
OCO_AI_PROVIDER_ENUM.OLLAMA,
|
||||
OCO_AI_PROVIDER_ENUM.MLX,
|
||||
OCO_AI_PROVIDER_ENUM.TEST
|
||||
];
|
||||
|
||||
const MODEL_REQUIRED_PROVIDERS = [
|
||||
OCO_AI_PROVIDER_ENUM.OLLAMA,
|
||||
OCO_AI_PROVIDER_ENUM.MLX
|
||||
];
|
||||
@@ -90,7 +96,8 @@ async function selectProvider(): Promise<string | symbol> {
|
||||
}
|
||||
|
||||
async function getApiKey(provider: string): Promise<string | symbol> {
|
||||
const url = PROVIDER_API_KEY_URLS[provider as keyof typeof PROVIDER_API_KEY_URLS];
|
||||
const url =
|
||||
PROVIDER_API_KEY_URLS[provider as keyof typeof PROVIDER_API_KEY_URLS];
|
||||
|
||||
let message = `Enter your ${provider} API key:`;
|
||||
if (url) {
|
||||
@@ -127,7 +134,8 @@ async function selectModel(
|
||||
provider: string,
|
||||
apiKey?: string
|
||||
): Promise<string | symbol> {
|
||||
const providerDisplayName = PROVIDER_DISPLAY_NAMES[provider]?.split(' (')[0] || provider;
|
||||
const providerDisplayName =
|
||||
PROVIDER_DISPLAY_NAMES[provider]?.split(' (')[0] || provider;
|
||||
const loadingSpinner = spinner();
|
||||
loadingSpinner.start(`Fetching models from ${providerDisplayName}...`);
|
||||
|
||||
@@ -178,7 +186,8 @@ async function selectModel(
|
||||
}
|
||||
|
||||
// Get recommended model for this provider
|
||||
const recommended = RECOMMENDED_MODELS[provider as keyof typeof RECOMMENDED_MODELS];
|
||||
const recommended =
|
||||
RECOMMENDED_MODELS[provider as keyof typeof RECOMMENDED_MODELS];
|
||||
|
||||
// Build options with recommended first
|
||||
const options: Array<{ value: string; label: string }> = [];
|
||||
@@ -191,9 +200,7 @@ async function selectModel(
|
||||
}
|
||||
|
||||
// Add other models (first 10, excluding recommended)
|
||||
const otherModels = models
|
||||
.filter((m) => m !== recommended)
|
||||
.slice(0, 10);
|
||||
const otherModels = models.filter((m) => m !== recommended).slice(0, 10);
|
||||
|
||||
otherModels.forEach((model) => {
|
||||
options.push({ value: model, label: model });
|
||||
@@ -409,27 +416,31 @@ export async function runSetup(): Promise<boolean> {
|
||||
setGlobalConfig(newConfig as any);
|
||||
|
||||
outro(
|
||||
`${chalk.green('✔')} Configuration saved to ~/.opencommit\n\n Run ${chalk.cyan('oco')} to generate commit messages!`
|
||||
`${chalk.green(
|
||||
'✔'
|
||||
)} Configuration saved to ~/.opencommit\n\n Run ${chalk.cyan(
|
||||
'oco'
|
||||
)} to generate commit messages!`
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isFirstRun(): boolean {
|
||||
if (!getIsGlobalConfigFileExist()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
// Check if API key is missing for providers that need it
|
||||
const provider = config.OCO_AI_PROVIDER || OCO_AI_PROVIDER_ENUM.OPENAI;
|
||||
|
||||
if (NO_API_KEY_PROVIDERS.includes(provider as OCO_AI_PROVIDER_ENUM)) {
|
||||
if (MODEL_REQUIRED_PROVIDERS.includes(provider as OCO_AI_PROVIDER_ENUM)) {
|
||||
// For Ollama/MLX, check if model is set
|
||||
return !config.OCO_MODEL;
|
||||
}
|
||||
|
||||
if (provider === OCO_AI_PROVIDER_ENUM.TEST) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For other providers, check if API key is set
|
||||
return !config.OCO_API_KEY;
|
||||
}
|
||||
@@ -447,9 +458,7 @@ export async function promptForMissingApiKey(): Promise<boolean> {
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`\nAPI key missing for ${provider}. Let's set it up.\n`
|
||||
)
|
||||
chalk.yellow(`\nAPI key missing for ${provider}. Let's set it up.\n`)
|
||||
);
|
||||
|
||||
const apiKey = await getApiKey(provider);
|
||||
|
||||
@@ -55,9 +55,7 @@ async function handleModelNotFoundError(
|
||||
provider: string,
|
||||
currentModel: string
|
||||
): Promise<string | null> {
|
||||
console.log(
|
||||
chalk.red(`\n✖ Model '${currentModel}' not found\n`)
|
||||
);
|
||||
console.log(chalk.red(`\n✖ Model '${currentModel}' not found\n`));
|
||||
|
||||
const suggestedModels = getSuggestedModels(provider, currentModel);
|
||||
const recommended =
|
||||
|
||||
@@ -3,15 +3,18 @@ import { MODEL_LIST, OCO_AI_PROVIDER_ENUM } from '../commands/config';
|
||||
|
||||
// Provider billing/help URLs for common errors
|
||||
export const PROVIDER_BILLING_URLS: Record<string, string | null> = {
|
||||
[OCO_AI_PROVIDER_ENUM.ANTHROPIC]: 'https://console.anthropic.com/settings/billing',
|
||||
[OCO_AI_PROVIDER_ENUM.OPENAI]: 'https://platform.openai.com/settings/organization/billing',
|
||||
[OCO_AI_PROVIDER_ENUM.ANTHROPIC]:
|
||||
'https://console.anthropic.com/settings/billing',
|
||||
[OCO_AI_PROVIDER_ENUM.OPENAI]:
|
||||
'https://platform.openai.com/settings/organization/billing',
|
||||
[OCO_AI_PROVIDER_ENUM.GEMINI]: 'https://aistudio.google.com/app/plan',
|
||||
[OCO_AI_PROVIDER_ENUM.GROQ]: 'https://console.groq.com/settings/billing',
|
||||
[OCO_AI_PROVIDER_ENUM.MISTRAL]: 'https://console.mistral.ai/billing/',
|
||||
[OCO_AI_PROVIDER_ENUM.DEEPSEEK]: 'https://platform.deepseek.com/usage',
|
||||
[OCO_AI_PROVIDER_ENUM.OPENROUTER]: 'https://openrouter.ai/credits',
|
||||
[OCO_AI_PROVIDER_ENUM.AIMLAPI]: 'https://aimlapi.com/app/billing',
|
||||
[OCO_AI_PROVIDER_ENUM.AZURE]: 'https://portal.azure.com/#view/Microsoft_Azure_CostManagement',
|
||||
[OCO_AI_PROVIDER_ENUM.AZURE]:
|
||||
'https://portal.azure.com/#view/Microsoft_Azure_CostManagement',
|
||||
[OCO_AI_PROVIDER_ENUM.OLLAMA]: null,
|
||||
[OCO_AI_PROVIDER_ENUM.MLX]: null,
|
||||
[OCO_AI_PROVIDER_ENUM.FLOWISE]: null,
|
||||
@@ -23,7 +26,9 @@ export class InsufficientCreditsError extends Error {
|
||||
public readonly provider: string;
|
||||
|
||||
constructor(provider: string, message?: string) {
|
||||
super(message || `Insufficient credits or quota for provider '${provider}'`);
|
||||
super(
|
||||
message || `Insufficient credits or quota for provider '${provider}'`
|
||||
);
|
||||
this.name = 'InsufficientCreditsError';
|
||||
this.provider = provider;
|
||||
}
|
||||
@@ -345,7 +350,10 @@ export interface FormattedError {
|
||||
}
|
||||
|
||||
// Format an error into a user-friendly structure
|
||||
export function formatUserFriendlyError(error: unknown, provider: string): FormattedError {
|
||||
export function formatUserFriendlyError(
|
||||
error: unknown,
|
||||
provider: string
|
||||
): FormattedError {
|
||||
const billingUrl = PROVIDER_BILLING_URLS[provider] || null;
|
||||
|
||||
// Handle our custom error types first
|
||||
@@ -460,7 +468,9 @@ export function printFormattedError(formatted: FormattedError): string {
|
||||
output += ` ${formatted.message}\n`;
|
||||
|
||||
if (formatted.helpUrl) {
|
||||
output += `\n ${chalk.cyan('Help:')} ${chalk.underline(formatted.helpUrl)}\n`;
|
||||
output += `\n ${chalk.cyan('Help:')} ${chalk.underline(
|
||||
formatted.helpUrl
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
if (formatted.suggestion) {
|
||||
|
||||
@@ -125,9 +125,7 @@ export async function fetchMistralModels(apiKey: string): Promise<string[]> {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const models = data.data
|
||||
?.map((m: { id: string }) => m.id)
|
||||
.sort();
|
||||
const models = data.data?.map((m: { id: string }) => m.id).sort();
|
||||
|
||||
return models && models.length > 0 ? models : MODEL_LIST.mistral;
|
||||
} catch {
|
||||
@@ -148,9 +146,7 @@ export async function fetchGroqModels(apiKey: string): Promise<string[]> {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const models = data.data
|
||||
?.map((m: { id: string }) => m.id)
|
||||
.sort();
|
||||
const models = data.data?.map((m: { id: string }) => m.id).sort();
|
||||
|
||||
return models && models.length > 0 ? models : MODEL_LIST.groq;
|
||||
} catch {
|
||||
@@ -173,8 +169,9 @@ export async function fetchOpenRouterModels(apiKey: string): Promise<string[]> {
|
||||
const data = await response.json();
|
||||
// Filter to text-capable models only (exclude image/audio models)
|
||||
const models = data.data
|
||||
?.filter((m: { id: string; context_length?: number }) =>
|
||||
m.context_length && m.context_length > 0
|
||||
?.filter(
|
||||
(m: { id: string; context_length?: number }) =>
|
||||
m.context_length && m.context_length > 0
|
||||
)
|
||||
.map((m: { id: string }) => m.id)
|
||||
.sort();
|
||||
@@ -198,9 +195,7 @@ export async function fetchDeepSeekModels(apiKey: string): Promise<string[]> {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const models = data.data
|
||||
?.map((m: { id: string }) => m.id)
|
||||
.sort();
|
||||
const models = data.data?.map((m: { id: string }) => m.id).sort();
|
||||
|
||||
return models && models.length > 0 ? models : MODEL_LIST.deepseek;
|
||||
} catch {
|
||||
@@ -312,7 +307,10 @@ export function clearModelCache(): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function getCacheInfo(): { timestamp: number | null; providers: string[] } {
|
||||
export function getCacheInfo(): {
|
||||
timestamp: number | null;
|
||||
providers: string[];
|
||||
} {
|
||||
const cache = readCache();
|
||||
if (!cache) {
|
||||
return { timestamp: null, providers: [] };
|
||||
|
||||
Reference in New Issue
Block a user