diff --git a/src/commands/config.ts b/src/commands/config.ts index 1032940a..4af9f52d 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -132,7 +132,339 @@ export const MODEL_LIST = { 'mistral-moderation-2411', 'mistral-moderation-latest' ], - deepseek: ['deepseek-chat', 'deepseek-reasoner'] + + deepseek: ['deepseek-chat', 'deepseek-reasoner'], + + // OpenRouter available models + // input_modalities: 'text' + // output_modalities: 'text' + // https://openrouter.ai/api/v1/models + openrouter: [ + 'openai/gpt-4o-mini', // used by default + '01-ai/yi-large', + 'aetherwiing/mn-starcannon-12b', + 'agentica-org/deepcoder-14b-preview:free', + 'ai21/jamba-1.6-large', + 'ai21/jamba-1.6-mini', + 'aion-labs/aion-1.0', + 'aion-labs/aion-1.0-mini', + 'aion-labs/aion-rp-llama-3.1-8b', + 'alfredpros/codellama-7b-instruct-solidity', + 'all-hands/openhands-lm-32b-v0.1', + 'alpindale/goliath-120b', + 'alpindale/magnum-72b', + 'amazon/nova-lite-v1', + 'amazon/nova-micro-v1', + 'amazon/nova-pro-v1', + 'anthracite-org/magnum-v2-72b', + 'anthracite-org/magnum-v4-72b', + 'anthropic/claude-2', + 'anthropic/claude-2.0', + 'anthropic/claude-2.0:beta', + 'anthropic/claude-2.1', + 'anthropic/claude-2.1:beta', + 'anthropic/claude-2:beta', + 'anthropic/claude-3-haiku', + 'anthropic/claude-3-haiku:beta', + 'anthropic/claude-3-opus', + 'anthropic/claude-3-opus:beta', + 'anthropic/claude-3-sonnet', + 'anthropic/claude-3-sonnet:beta', + 'anthropic/claude-3.5-haiku', + 'anthropic/claude-3.5-haiku-20241022', + 'anthropic/claude-3.5-haiku-20241022:beta', + 'anthropic/claude-3.5-haiku:beta', + 'anthropic/claude-3.5-sonnet', + 'anthropic/claude-3.5-sonnet-20240620', + 'anthropic/claude-3.5-sonnet-20240620:beta', + 'anthropic/claude-3.5-sonnet:beta', + 'anthropic/claude-3.7-sonnet', + 'anthropic/claude-3.7-sonnet:beta', + 'anthropic/claude-3.7-sonnet:thinking', + 'anthropic/claude-opus-4', + 'anthropic/claude-sonnet-4', + 'arcee-ai/arcee-blitz', + 'arcee-ai/caller-large', + 'arcee-ai/coder-large', + 'arcee-ai/maestro-reasoning', + 'arcee-ai/spotlight', + 'arcee-ai/virtuoso-large', + 'arcee-ai/virtuoso-medium-v2', + 'arliai/qwq-32b-arliai-rpr-v1:free', + 'cognitivecomputations/dolphin-mixtral-8x22b', + 'cognitivecomputations/dolphin3.0-mistral-24b:free', + 'cognitivecomputations/dolphin3.0-r1-mistral-24b:free', + 'cohere/command', + 'cohere/command-a', + 'cohere/command-r', + 'cohere/command-r-03-2024', + 'cohere/command-r-08-2024', + 'cohere/command-r-plus', + 'cohere/command-r-plus-04-2024', + 'cohere/command-r-plus-08-2024', + 'cohere/command-r7b-12-2024', + 'deepseek/deepseek-chat', + 'deepseek/deepseek-chat-v3-0324', + 'deepseek/deepseek-chat-v3-0324:free', + 'deepseek/deepseek-chat:free', + 'deepseek/deepseek-prover-v2', + 'deepseek/deepseek-prover-v2:free', + 'deepseek/deepseek-r1', + 'deepseek/deepseek-r1-0528', + 'deepseek/deepseek-r1-0528-qwen3-8b', + 'deepseek/deepseek-r1-0528-qwen3-8b:free', + 'deepseek/deepseek-r1-0528:free', + 'deepseek/deepseek-r1-distill-llama-70b', + 'deepseek/deepseek-r1-distill-llama-70b:free', + 'deepseek/deepseek-r1-distill-llama-8b', + 'deepseek/deepseek-r1-distill-qwen-1.5b', + 'deepseek/deepseek-r1-distill-qwen-14b', + 'deepseek/deepseek-r1-distill-qwen-14b:free', + 'deepseek/deepseek-r1-distill-qwen-32b', + 'deepseek/deepseek-r1-distill-qwen-32b:free', + 'deepseek/deepseek-r1-distill-qwen-7b', + 'deepseek/deepseek-r1-zero:free', + 'deepseek/deepseek-r1:free', + 'deepseek/deepseek-v3-base:free', + 'eleutherai/llemma_7b', + 'eva-unit-01/eva-llama-3.33-70b', + 'eva-unit-01/eva-qwen-2.5-32b', + 'eva-unit-01/eva-qwen-2.5-72b', + 'featherless/qwerky-72b:free', + 'google/gemini-2.0-flash-001', + 'google/gemini-2.0-flash-exp:free', + 'google/gemini-2.0-flash-lite-001', + 'google/gemini-2.5-flash-preview', + 'google/gemini-2.5-flash-preview-05-20', + 'google/gemini-2.5-flash-preview-05-20:thinking', + 'google/gemini-2.5-flash-preview:thinking', + 'google/gemini-2.5-pro-exp-03-25', + 'google/gemini-2.5-pro-preview', + 'google/gemini-2.5-pro-preview-05-06', + 'google/gemini-flash-1.5', + 'google/gemini-flash-1.5-8b', + 'google/gemini-pro-1.5', + 'google/gemma-2-27b-it', + 'google/gemma-2-9b-it', + 'google/gemma-2-9b-it:free', + 'google/gemma-3-12b-it', + 'google/gemma-3-12b-it:free', + 'google/gemma-3-1b-it:free', + 'google/gemma-3-27b-it', + 'google/gemma-3-27b-it:free', + 'google/gemma-3-4b-it', + 'google/gemma-3-4b-it:free', + 'google/gemma-3n-e4b-it:free', + 'gryphe/mythomax-l2-13b', + 'inception/mercury-coder-small-beta', + 'infermatic/mn-inferor-12b', + 'inflection/inflection-3-pi', + 'inflection/inflection-3-productivity', + 'liquid/lfm-3b', + 'liquid/lfm-40b', + 'liquid/lfm-7b', + 'mancer/weaver', + 'meta-llama/llama-2-70b-chat', + 'meta-llama/llama-3-70b-instruct', + 'meta-llama/llama-3-8b-instruct', + 'meta-llama/llama-3.1-405b', + 'meta-llama/llama-3.1-405b-instruct', + 'meta-llama/llama-3.1-405b:free', + 'meta-llama/llama-3.1-70b-instruct', + 'meta-llama/llama-3.1-8b-instruct', + 'meta-llama/llama-3.1-8b-instruct:free', + 'meta-llama/llama-3.2-11b-vision-instruct', + 'meta-llama/llama-3.2-11b-vision-instruct:free', + 'meta-llama/llama-3.2-1b-instruct', + 'meta-llama/llama-3.2-1b-instruct:free', + 'meta-llama/llama-3.2-3b-instruct', + 'meta-llama/llama-3.2-3b-instruct:free', + 'meta-llama/llama-3.2-90b-vision-instruct', + 'meta-llama/llama-3.3-70b-instruct', + 'meta-llama/llama-3.3-70b-instruct:free', + 'meta-llama/llama-3.3-8b-instruct:free', + 'meta-llama/llama-4-maverick', + 'meta-llama/llama-4-maverick:free', + 'meta-llama/llama-4-scout', + 'meta-llama/llama-4-scout:free', + 'meta-llama/llama-guard-2-8b', + 'meta-llama/llama-guard-3-8b', + 'meta-llama/llama-guard-4-12b', + 'microsoft/mai-ds-r1:free', + 'microsoft/phi-3-medium-128k-instruct', + 'microsoft/phi-3-mini-128k-instruct', + 'microsoft/phi-3.5-mini-128k-instruct', + 'microsoft/phi-4', + 'microsoft/phi-4-multimodal-instruct', + 'microsoft/phi-4-reasoning-plus', + 'microsoft/phi-4-reasoning-plus:free', + 'microsoft/phi-4-reasoning:free', + 'microsoft/wizardlm-2-8x22b', + 'minimax/minimax-01', + 'mistralai/codestral-2501', + 'mistralai/devstral-small', + 'mistralai/devstral-small:free', + 'mistralai/magistral-medium-2506', + 'mistralai/magistral-medium-2506:thinking', + 'mistralai/magistral-small-2506', + 'mistralai/ministral-3b', + 'mistralai/ministral-8b', + 'mistralai/mistral-7b-instruct', + 'mistralai/mistral-7b-instruct-v0.1', + 'mistralai/mistral-7b-instruct-v0.2', + 'mistralai/mistral-7b-instruct-v0.3', + 'mistralai/mistral-7b-instruct:free', + 'mistralai/mistral-large', + 'mistralai/mistral-large-2407', + 'mistralai/mistral-large-2411', + 'mistralai/mistral-medium', + 'mistralai/mistral-medium-3', + 'mistralai/mistral-nemo', + 'mistralai/mistral-nemo:free', + 'mistralai/mistral-saba', + 'mistralai/mistral-small', + 'mistralai/mistral-small-24b-instruct-2501', + 'mistralai/mistral-small-24b-instruct-2501:free', + 'mistralai/mistral-small-3.1-24b-instruct', + 'mistralai/mistral-small-3.1-24b-instruct:free', + 'mistralai/mistral-tiny', + 'mistralai/mixtral-8x22b-instruct', + 'mistralai/mixtral-8x7b-instruct', + 'mistralai/pixtral-12b', + 'mistralai/pixtral-large-2411', + 'moonshotai/kimi-vl-a3b-thinking:free', + 'moonshotai/moonlight-16b-a3b-instruct:free', + 'neversleep/llama-3-lumimaid-70b', + 'neversleep/llama-3-lumimaid-8b', + 'neversleep/llama-3.1-lumimaid-70b', + 'neversleep/llama-3.1-lumimaid-8b', + 'neversleep/noromaid-20b', + 'nothingiisreal/mn-celeste-12b', + 'nousresearch/deephermes-3-llama-3-8b-preview:free', + 'nousresearch/deephermes-3-mistral-24b-preview:free', + 'nousresearch/hermes-2-pro-llama-3-8b', + 'nousresearch/hermes-3-llama-3.1-405b', + 'nousresearch/hermes-3-llama-3.1-70b', + 'nousresearch/nous-hermes-2-mixtral-8x7b-dpo', + 'nvidia/llama-3.1-nemotron-70b-instruct', + 'nvidia/llama-3.1-nemotron-ultra-253b-v1', + 'nvidia/llama-3.1-nemotron-ultra-253b-v1:free', + 'nvidia/llama-3.3-nemotron-super-49b-v1', + 'nvidia/llama-3.3-nemotron-super-49b-v1:free', + 'open-r1/olympiccoder-32b:free', + 'openai/chatgpt-4o-latest', + 'openai/codex-mini', + 'openai/gpt-3.5-turbo', + 'openai/gpt-3.5-turbo-0125', + 'openai/gpt-3.5-turbo-0613', + 'openai/gpt-3.5-turbo-1106', + 'openai/gpt-3.5-turbo-16k', + 'openai/gpt-3.5-turbo-instruct', + 'openai/gpt-4', + 'openai/gpt-4-0314', + 'openai/gpt-4-1106-preview', + 'openai/gpt-4-turbo', + 'openai/gpt-4-turbo-preview', + 'openai/gpt-4.1', + 'openai/gpt-4.1-mini', + 'openai/gpt-4.1-nano', + 'openai/gpt-4.5-preview', + 'openai/gpt-4o', + 'openai/gpt-4o-2024-05-13', + 'openai/gpt-4o-2024-08-06', + 'openai/gpt-4o-2024-11-20', + 'openai/gpt-4o-mini-2024-07-18', + 'openai/gpt-4o-mini-search-preview', + 'openai/gpt-4o-search-preview', + 'openai/gpt-4o:extended', + 'openai/o1', + 'openai/o1-mini', + 'openai/o1-mini-2024-09-12', + 'openai/o1-preview', + 'openai/o1-preview-2024-09-12', + 'openai/o1-pro', + 'openai/o3', + 'openai/o3-mini', + 'openai/o3-mini-high', + 'openai/o3-pro', + 'openai/o4-mini', + 'openai/o4-mini-high', + 'opengvlab/internvl3-14b:free', + 'opengvlab/internvl3-2b:free', + 'openrouter/auto', + 'perplexity/llama-3.1-sonar-large-128k-online', + 'perplexity/llama-3.1-sonar-small-128k-online', + 'perplexity/r1-1776', + 'perplexity/sonar', + 'perplexity/sonar-deep-research', + 'perplexity/sonar-pro', + 'perplexity/sonar-reasoning', + 'perplexity/sonar-reasoning-pro', + 'pygmalionai/mythalion-13b', + 'qwen/qwen-2-72b-instruct', + 'qwen/qwen-2.5-72b-instruct', + 'qwen/qwen-2.5-72b-instruct:free', + 'qwen/qwen-2.5-7b-instruct', + 'qwen/qwen-2.5-7b-instruct:free', + 'qwen/qwen-2.5-coder-32b-instruct', + 'qwen/qwen-2.5-coder-32b-instruct:free', + 'qwen/qwen-2.5-vl-7b-instruct', + 'qwen/qwen-2.5-vl-7b-instruct:free', + 'qwen/qwen-max', + 'qwen/qwen-plus', + 'qwen/qwen-turbo', + 'qwen/qwen-vl-max', + 'qwen/qwen-vl-plus', + 'qwen/qwen2.5-vl-32b-instruct', + 'qwen/qwen2.5-vl-32b-instruct:free', + 'qwen/qwen2.5-vl-3b-instruct:free', + 'qwen/qwen2.5-vl-72b-instruct', + 'qwen/qwen2.5-vl-72b-instruct:free', + 'qwen/qwen3-14b', + 'qwen/qwen3-14b:free', + 'qwen/qwen3-235b-a22b', + 'qwen/qwen3-235b-a22b:free', + 'qwen/qwen3-30b-a3b', + 'qwen/qwen3-30b-a3b:free', + 'qwen/qwen3-32b', + 'qwen/qwen3-32b:free', + 'qwen/qwen3-8b', + 'qwen/qwen3-8b:free', + 'qwen/qwq-32b', + 'qwen/qwq-32b-preview', + 'qwen/qwq-32b:free', + 'raifle/sorcererlm-8x22b', + 'rekaai/reka-flash-3:free', + 'sao10k/fimbulvetr-11b-v2', + 'sao10k/l3-euryale-70b', + 'sao10k/l3-lunaris-8b', + 'sao10k/l3.1-euryale-70b', + 'sao10k/l3.3-euryale-70b', + 'sarvamai/sarvam-m:free', + 'scb10x/llama3.1-typhoon2-70b-instruct', + 'sentientagi/dobby-mini-unhinged-plus-llama-3.1-8b', + 'shisa-ai/shisa-v2-llama3.3-70b:free', + 'sophosympatheia/midnight-rose-70b', + 'thedrummer/anubis-pro-105b-v1', + 'thedrummer/rocinante-12b', + 'thedrummer/skyfall-36b-v2', + 'thedrummer/unslopnemo-12b', + 'thedrummer/valkyrie-49b-v1', + 'thudm/glm-4-32b', + 'thudm/glm-4-32b:free', + 'thudm/glm-z1-32b', + 'thudm/glm-z1-32b:free', + 'thudm/glm-z1-rumination-32b', + 'tngtech/deepseek-r1t-chimera:free', + 'undi95/remm-slerp-l2-13b', + 'undi95/toppy-m-7b', + 'x-ai/grok-2-1212', + 'x-ai/grok-2-vision-1212', + 'x-ai/grok-3-beta', + 'x-ai/grok-3-mini-beta', + 'x-ai/grok-beta', + 'x-ai/grok-vision-beta' + ] }; const getDefaultModel = (provider: string | undefined): string => { @@ -151,6 +483,8 @@ const getDefaultModel = (provider: string | undefined): string => { return MODEL_LIST.mistral[0]; case 'deepseek': return MODEL_LIST.deepseek[0]; + case 'openrouter': + return MODEL_LIST.openrouter[0]; default: return MODEL_LIST.openai[0]; } @@ -340,7 +674,8 @@ export const configValidators = { 'test', 'flowise', 'groq', - 'deepseek' + 'deepseek', + 'openrouter' ].includes(value) || value.startsWith('ollama'), `${value} is not supported yet, use 'ollama', 'mlx', 'anthropic', 'azure', 'gemini', 'flowise', 'mistral', 'deepseek' or 'openai' (default)` ); @@ -390,7 +725,8 @@ export enum OCO_AI_PROVIDER_ENUM { GROQ = 'groq', MISTRAL = 'mistral', MLX = 'mlx', - DEEPSEEK = 'deepseek' + DEEPSEEK = 'deepseek', + OPENROUTER = 'openrouter' } export type ConfigType = { @@ -658,7 +994,8 @@ function getConfigKeyDetails(key) { }; case CONFIG_KEYS.OCO_DESCRIPTION: return { - description: 'Postface a message with ~3 sentences description of the changes', + description: + 'Postface a message with ~3 sentences description of the changes', values: ['true', 'false'] }; case CONFIG_KEYS.OCO_EMOJI: @@ -668,9 +1005,10 @@ function getConfigKeyDetails(key) { }; case CONFIG_KEYS.OCO_WHY: return { - description: 'Output a short description of why the changes were done after the commit message (default: false)', + description: + 'Output a short description of why the changes were done after the commit message (default: false)', values: ['true', 'false'] - } + }; case CONFIG_KEYS.OCO_OMIT_SCOPE: return { description: 'Do not include a scope in the commit message', @@ -678,7 +1016,8 @@ function getConfigKeyDetails(key) { }; case CONFIG_KEYS.OCO_GITPUSH: return { - description: 'Push to git after commit (deprecated). If false, oco will exit after committing', + description: + 'Push to git after commit (deprecated). If false, oco will exit after committing', values: ['true', 'false'] }; case CONFIG_KEYS.OCO_TOKENS_MAX_INPUT: @@ -698,13 +1037,14 @@ function getConfigKeyDetails(key) { }; case CONFIG_KEYS.OCO_API_URL: return { - description: 'Custom API URL - may be used to set proxy path to OpenAI API', + description: + 'Custom API URL - may be used to set proxy path to OpenAI API', values: ["URL string (must start with 'http://' or 'https://')"] }; case CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER: return { description: 'Message template placeholder', - values: ["String (must start with $)"] + values: ['String (must start with $)'] }; default: return { @@ -728,7 +1068,6 @@ function printConfigKeyHelp(param) { defaultValue = DEFAULT_CONFIG[param]; } - console.log(chalk.bold(`\n${param}:`)); console.log(chalk.gray(` Description: ${desc}`)); if (defaultValue !== undefined) { @@ -742,14 +1081,14 @@ function printConfigKeyHelp(param) { if (Array.isArray(details.values)) { console.log(chalk.gray(' Accepted values:')); - details.values.forEach(value => { + details.values.forEach((value) => { console.log(chalk.gray(` - ${value}`)); }); } else { console.log(chalk.gray(' Accepted values by provider:')); Object.entries(details.values).forEach(([provider, values]) => { console.log(chalk.gray(` ${provider}:`)); - (values as string[]).forEach(value => { + (values as string[]).forEach((value) => { console.log(chalk.gray(` - ${value}`)); }); }); @@ -765,7 +1104,7 @@ function printAllConfigHelp() { if (key in DEFAULT_CONFIG) { defaultValue = DEFAULT_CONFIG[key]; } - + console.log(chalk.bold(`\n${key}:`)); console.log(chalk.gray(` Description: ${details.description}`)); if (defaultValue !== undefined) { @@ -776,7 +1115,11 @@ function printAllConfigHelp() { } } } - console.log(chalk.yellow('\nUse "oco config describe [PARAMETER]" to see accepted values and more details for a specific config parameter.')); + console.log( + chalk.yellow( + '\nUse "oco config describe [PARAMETER]" to see accepted values and more details for a specific config parameter.' + ) + ); } export const configCommand = command( diff --git a/src/engine/openrouter.ts b/src/engine/openrouter.ts new file mode 100644 index 00000000..792d694b --- /dev/null +++ b/src/engine/openrouter.ts @@ -0,0 +1,49 @@ +import OpenAI from 'openai'; +import { AiEngine, AiEngineConfig } from './Engine'; +import axios, { AxiosInstance } from 'axios'; +import { removeContentTags } from '../utils/removeContentTags'; + +interface OpenRouterConfig extends AiEngineConfig {} + +export class OpenRouterEngine implements AiEngine { + client: AxiosInstance; + + constructor(public config: OpenRouterConfig) { + this.client = axios.create({ + baseURL: 'https://openrouter.ai/api/v1/chat/completions', + headers: { + Authorization: `Bearer ${config.apiKey}`, + 'HTTP-Referer': 'https://github.com/di-sukharev/opencommit', + 'X-Title': 'OpenCommit', + 'Content-Type': 'application/json' + } + }); + } + + public generateCommitMessage = async ( + messages: Array + ): Promise => { + try { + const response = await this.client.post('', { + model: this.config.model, + messages + }); + + const message = response.data.choices[0].message; + 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; + } + }; +} diff --git a/src/utils/engine.ts b/src/utils/engine.ts index dbc45a00..5dcc6619 100644 --- a/src/utils/engine.ts +++ b/src/utils/engine.ts @@ -11,14 +11,15 @@ import { TestAi, TestMockType } from '../engine/testAi'; import { GroqEngine } from '../engine/groq'; import { MLXEngine } from '../engine/mlx'; import { DeepseekEngine } from '../engine/deepseek'; +import { OpenRouterEngine } from '../engine/openrouter'; export function parseCustomHeaders(headers: any): Record { let parsedHeaders = {}; - + if (!headers) { return parsedHeaders; } - + try { if (typeof headers === 'object' && !Array.isArray(headers)) { parsedHeaders = headers; @@ -26,9 +27,11 @@ export function parseCustomHeaders(headers: any): Record { parsedHeaders = JSON.parse(headers); } } catch (error) { - console.warn('Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers'); + console.warn( + 'Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers' + ); } - + return parsedHeaders; } @@ -78,6 +81,9 @@ export function getEngine(): AiEngine { case OCO_AI_PROVIDER_ENUM.DEEPSEEK: return new DeepseekEngine(DEFAULT_CONFIG); + case OCO_AI_PROVIDER_ENUM.OPENROUTER: + return new OpenRouterEngine(DEFAULT_CONFIG); + default: return new OpenAiEngine(DEFAULT_CONFIG); }