Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,25 @@ export OLLAMA_HOST=0.0.0.0

This will make Ollama listen on all interfaces, including IPv6 and IPv4, resolving the connection issue. You can add this line to your shell configuration file (like `.bashrc` or `.zshrc`) to make it persistent across sessions.

### Running locally with llama.cpp

You can run OpenCommit with local models served by [llama.cpp](https://github.com/ggerganov/llama.cpp):

- install and build llama.cpp
- start the server with a GGUF model: `./llama-server -m model.gguf --port 8080`
- run (in your project directory):

```sh
git add <files...>
oco config set OCO_AI_PROVIDER='llamacpp' OCO_API_URL='http://localhost:8080'
```

If the server is running on a different machine, set the URL accordingly:

```sh
oco config set OCO_API_URL='http://192.168.1.10:8080'
```

### Flags

There are multiple optional flags that can be used with the `oco` command:
Expand Down Expand Up @@ -122,7 +141,7 @@ Create a `.env` file and add OpenCommit config variables there like this:

```env
...
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama, gemini, flowise, deepseek, aimlapi>
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama, llamacpp, gemini, flowise, deepseek, aimlapi>
OCO_API_KEY=<your OpenAI API token> // or other LLM provider API token
OCO_API_URL=<may be used to set proxy path to OpenAI api>
OCO_API_CUSTOM_HEADERS=<JSON string of custom HTTP headers to include in API requests>
Expand Down Expand Up @@ -227,14 +246,16 @@ oco models --provider anthropic

By default OpenCommit uses [OpenAI](https://openai.com).

You could switch to [Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/) or Flowise or Ollama.
You could switch to [Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/) or Flowise or Ollama or llama.cpp.

```sh
oco config set OCO_AI_PROVIDER=azure OCO_API_KEY=<your_azure_api_key> OCO_API_URL=<your_azure_endpoint>

oco config set OCO_AI_PROVIDER=flowise OCO_API_KEY=<your_flowise_api_key> OCO_API_URL=<your_flowise_endpoint>

oco config set OCO_AI_PROVIDER=ollama OCO_API_KEY=<your_ollama_api_key> OCO_API_URL=<your_ollama_endpoint>

oco config set OCO_AI_PROVIDER=llamacpp OCO_API_URL=<your_llamacpp_endpoint>
```

### Use with Proxy
Expand Down
10 changes: 8 additions & 2 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,8 @@ const getDefaultModel = (provider: string | undefined): string => {
switch (provider) {
case 'ollama':
return '';
case 'llamacpp':
return '';
case 'mlx':
return '';
case 'anthropic':
Expand Down Expand Up @@ -797,8 +799,10 @@ export const configValidators = {
'deepseek',
'aimlapi',
'openrouter'
].includes(value) || value.startsWith('ollama'),
`${value} is not supported yet, use 'ollama', 'mlx', 'anthropic', 'azure', 'gemini', 'flowise', 'mistral', 'deepseek', 'aimlapi' or 'openai' (default)`
].includes(value) ||
value.startsWith('ollama') ||
value.startsWith('llamacpp'),
`${value} is not supported yet, use 'ollama', 'llamacpp', 'mlx', 'anthropic', 'azure', 'gemini', 'flowise', 'mistral', 'deepseek', 'aimlapi' or 'openai' (default)`
);

return value;
Expand Down Expand Up @@ -854,6 +858,7 @@ export const configValidators = {

export enum OCO_AI_PROVIDER_ENUM {
OLLAMA = 'ollama',
LLAMACPP = 'llamacpp',
OPENAI = 'openai',
ANTHROPIC = 'anthropic',
GEMINI = 'gemini',
Expand All @@ -880,6 +885,7 @@ export const PROVIDER_API_KEY_URLS: Record<string, string | null> = {
[OCO_AI_PROVIDER_ENUM.AIMLAPI]: 'https://aimlapi.com/app/keys',
[OCO_AI_PROVIDER_ENUM.AZURE]: 'https://portal.azure.com/',
[OCO_AI_PROVIDER_ENUM.OLLAMA]: null,
[OCO_AI_PROVIDER_ENUM.LLAMACPP]: null,
[OCO_AI_PROVIDER_ENUM.MLX]: null,
[OCO_AI_PROVIDER_ENUM.FLOWISE]: null,
[OCO_AI_PROVIDER_ENUM.TEST]: null
Expand Down
52 changes: 50 additions & 2 deletions src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
[OCO_AI_PROVIDER_ENUM.OPENAI]: 'OpenAI (GPT-4o, GPT-4)',
[OCO_AI_PROVIDER_ENUM.ANTHROPIC]: 'Anthropic (Claude Sonnet, Opus)',
[OCO_AI_PROVIDER_ENUM.OLLAMA]: 'Ollama (Free, runs locally)',
[OCO_AI_PROVIDER_ENUM.LLAMACPP]: 'llama.cpp (Free, runs locally)',
[OCO_AI_PROVIDER_ENUM.GEMINI]: 'Google Gemini',
[OCO_AI_PROVIDER_ENUM.GROQ]: 'Groq (Fast inference, free tier)',
[OCO_AI_PROVIDER_ENUM.MISTRAL]: 'Mistral AI',
Expand All @@ -37,7 +38,8 @@ const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
const PRIMARY_PROVIDERS = [
OCO_AI_PROVIDER_ENUM.OPENAI,
OCO_AI_PROVIDER_ENUM.ANTHROPIC,
OCO_AI_PROVIDER_ENUM.OLLAMA
OCO_AI_PROVIDER_ENUM.OLLAMA,
OCO_AI_PROVIDER_ENUM.LLAMACPP
];

const OTHER_PROVIDERS = [
Expand All @@ -53,13 +55,15 @@ const OTHER_PROVIDERS = [

const NO_API_KEY_PROVIDERS = [
OCO_AI_PROVIDER_ENUM.OLLAMA,
OCO_AI_PROVIDER_ENUM.LLAMACPP,
OCO_AI_PROVIDER_ENUM.MLX,
OCO_AI_PROVIDER_ENUM.TEST
];

const MODEL_REQUIRED_PROVIDERS = [
OCO_AI_PROVIDER_ENUM.OLLAMA,
OCO_AI_PROVIDER_ENUM.MLX
OCO_AI_PROVIDER_ENUM.MLX,
OCO_AI_PROVIDER_ENUM.LLAMACPP
];

async function selectProvider(): Promise<string | symbol> {
Expand Down Expand Up @@ -335,6 +339,37 @@ async function setupOllama(): Promise<{
};
}

async function setupLlamaCpp(): Promise<{
provider: string;
model: string;
apiUrl: string;
} | null> {
console.log(chalk.cyan('\n llama.cpp - Free Local AI\n'));
console.log(chalk.dim(' Setup steps:'));
console.log(
chalk.dim(' 1. Install: https://github.com/ggerganov/llama.cpp')
);
console.log(
chalk.dim(' 2. Start server: llama-server -m <model.gguf> --port 8080\n')
);

const defaultUrl = 'http://localhost:8080';

const apiUrl = await text({
message: 'llama.cpp server URL (press Enter for default):',
placeholder: defaultUrl,
defaultValue: defaultUrl
});

if (isCancel(apiUrl)) return null;

return {
provider: OCO_AI_PROVIDER_ENUM.LLAMACPP,
model: '',
apiUrl: (apiUrl as string) || defaultUrl
};
}

export async function runSetup(): Promise<boolean> {
intro(chalk.bgCyan(' Welcome to OpenCommit! '));

Expand Down Expand Up @@ -382,6 +417,19 @@ export async function runSetup(): Promise<boolean> {
OCO_MODEL: model,
OCO_API_KEY: 'mlx' // Placeholder
};
} else if (provider === OCO_AI_PROVIDER_ENUM.LLAMACPP) {
const llamacppConfig = await setupLlamaCpp();
if (!llamacppConfig) {
outro('Setup cancelled');
return false;
}

config = {
OCO_AI_PROVIDER: llamacppConfig.provider,
OCO_MODEL: llamacppConfig.model,
OCO_API_URL: llamacppConfig.apiUrl,
OCO_API_KEY: 'llamacpp' // Placeholder
};
} else {
// Standard provider flow: API key then model
const apiKey = await getApiKey(provider as string);
Expand Down
52 changes: 52 additions & 0 deletions src/engine/llamacpp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import axios, { AxiosInstance } from 'axios';
import { OpenAI } from 'openai';
import { normalizeEngineError } from '../utils/engineErrorHandler';
import { removeContentTags } from '../utils/removeContentTags';
import { AiEngine, AiEngineConfig } from './Engine';

interface LlamaCppConfig extends AiEngineConfig {}

const DEFAULT_LLAMACPP_URL = 'http://localhost:8080';
const LLAMACPP_CHAT_PATH = '/v1/chat/completions';

export class LlamaCppEngine implements AiEngine {
config: LlamaCppConfig;
client: AxiosInstance;
private chatUrl: string;

constructor(config: LlamaCppConfig) {
this.config = config;

const baseUrl = config.baseURL || DEFAULT_LLAMACPP_URL;
this.chatUrl = `${baseUrl}${LLAMACPP_CHAT_PATH}`;

const headers = {
'Content-Type': 'application/json',
...config.customHeaders
};

this.client = axios.create({ headers });
}

async generateCommitMessage(
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | undefined> {
const params: Record<string, any> = {
model: this.config.model ?? '',
messages,
temperature: 0,
top_p: 0.1,
repeat_penalty: 1.1,
stream: false
};
try {
const response = await this.client.post(this.chatUrl, params);

const choices = response.data.choices;
const message = choices[0].message;
return removeContentTags(message?.content, 'think');
} catch (error) {
throw normalizeEngineError(error, 'llamacpp', this.config.model);
}
}
}
4 changes: 4 additions & 0 deletions src/utils/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AzureEngine } from '../engine/azure';
import { AiEngine } from '../engine/Engine';
import { FlowiseEngine } from '../engine/flowise';
import { GeminiEngine } from '../engine/gemini';
import { LlamaCppEngine } from '../engine/llamacpp';
import { OllamaEngine } from '../engine/ollama';
import { OpenAiEngine } from '../engine/openAi';
import { MistralAiEngine } from '../engine/mistral';
Expand Down Expand Up @@ -40,6 +41,9 @@ export function getEngine(): AiEngine {
ollamaThink: config.OCO_OLLAMA_THINK
});

case OCO_AI_PROVIDER_ENUM.LLAMACPP:
return new LlamaCppEngine(DEFAULT_CONFIG);

case OCO_AI_PROVIDER_ENUM.ANTHROPIC:
return new AnthropicEngine(DEFAULT_CONFIG);

Expand Down
21 changes: 21 additions & 0 deletions src/utils/modelCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,23 @@ export async function fetchOllamaModels(
}
}

export async function fetchLlamaCppModels(
baseUrl: string = 'http://localhost:8080'
): Promise<string[]> {
try {
const response = await fetch(`${baseUrl}/v1/models`);

if (!response.ok) {
return [];
}

const data = await response.json();
return data.data?.map((m: { id: string }) => m.id) || [];
} catch {
return [];
}
}

export async function fetchAnthropicModels(apiKey: string): Promise<string[]> {
try {
const response = await fetch('https://api.anthropic.com/v1/models', {
Expand Down Expand Up @@ -231,6 +248,10 @@ export async function fetchModelsForProvider(
models = await fetchOllamaModels(baseUrl);
break;

case OCO_AI_PROVIDER_ENUM.LLAMACPP:
models = await fetchLlamaCppModels(baseUrl);
break;

case OCO_AI_PROVIDER_ENUM.ANTHROPIC:
if (apiKey) {
models = await fetchAnthropicModels(apiKey);
Expand Down
103 changes: 103 additions & 0 deletions test/unit/llamacpp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { LlamaCppEngine } from '../../src/engine/llamacpp';

describe('LlamaCppEngine', () => {
it('sends request to /v1/chat/completions', async () => {
const engine = new LlamaCppEngine({
apiKey: 'llamacpp',
model: 'llama-3',
maxTokensOutput: 500,
maxTokensInput: 4096
});

const post = jest.fn().mockResolvedValue({
data: {
choices: [
{
message: {
content: 'feat: add support for llama.cpp provider'
}
}
]
}
});

engine.client = { post } as any;

await engine.generateCommitMessage([
{ role: 'user', content: 'diff --git a/file b/file' }
]);

expect(post).toHaveBeenCalledWith(
'http://localhost:8080/v1/chat/completions',
expect.objectContaining({
temperature: 0,
top_p: 0.1,
repeat_penalty: 1.1,
stream: false
})
);
});

it('uses custom baseURL when provided', async () => {
const engine = new LlamaCppEngine({
apiKey: 'llamacpp',
model: 'llama-3',
maxTokensOutput: 500,
maxTokensInput: 4096,
baseURL: 'http://192.168.1.10:8080'
});

const post = jest.fn().mockResolvedValue({
data: {
choices: [
{
message: {
content: 'fix: resolve connection issue'
}
}
]
}
});

engine.client = { post } as any;

await engine.generateCommitMessage([
{ role: 'user', content: 'diff --git a/file b/file' }
]);

expect(post).toHaveBeenCalledWith(
'http://192.168.1.10:8080/v1/chat/completions',
expect.anything()
);
});

it('strips <think> tags from response content', async () => {
const engine = new LlamaCppEngine({
apiKey: 'llamacpp',
model: 'llama-3',
maxTokensOutput: 500,
maxTokensInput: 4096
});

const post = jest.fn().mockResolvedValue({
data: {
choices: [
{
message: {
content:
'<think>reasoning here</think>feat: add support for llama.cpp provider'
}
}
]
}
});

engine.client = { post } as any;

const result = await engine.generateCommitMessage([
{ role: 'user', content: 'diff --git a/file b/file' }
]);

expect(result).toBe('feat: add support for llama.cpp provider');
});
});
Loading