Merge pull request #467 from EmilienMottet/master

Add OCO_API_CUSTOM_HEADERS
This commit is contained in:
GPT8
2025-05-03 12:06:21 +03:00
committed by GitHub
7 changed files with 94 additions and 7 deletions
+1
View File
@@ -109,6 +109,7 @@ Create a `.env` file and add OpenCommit config variables there like this:
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama, gemini, flowise, deepseek> OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama, gemini, flowise, deepseek>
OCO_API_KEY=<your OpenAI API token> // or other LLM provider API token 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_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>
OCO_TOKENS_MAX_INPUT=<max model token limit (default: 4096)> OCO_TOKENS_MAX_INPUT=<max model token limit (default: 4096)>
OCO_TOKENS_MAX_OUTPUT=<max response tokens (default: 500)> OCO_TOKENS_MAX_OUTPUT=<max response tokens (default: 500)>
OCO_DESCRIPTION=<postface a message with ~3 sentences description of the changes> OCO_DESCRIPTION=<postface a message with ~3 sentences description of the changes>
+19
View File
@@ -25,6 +25,7 @@ export enum CONFIG_KEYS {
OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT', OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT',
OCO_TEST_MOCK_TYPE = 'OCO_TEST_MOCK_TYPE', OCO_TEST_MOCK_TYPE = 'OCO_TEST_MOCK_TYPE',
OCO_API_URL = 'OCO_API_URL', OCO_API_URL = 'OCO_API_URL',
OCO_API_CUSTOM_HEADERS = 'OCO_API_CUSTOM_HEADERS',
OCO_OMIT_SCOPE = 'OCO_OMIT_SCOPE', OCO_OMIT_SCOPE = 'OCO_OMIT_SCOPE',
OCO_GITPUSH = 'OCO_GITPUSH' // todo: deprecate OCO_GITPUSH = 'OCO_GITPUSH' // todo: deprecate
} }
@@ -204,6 +205,22 @@ export const configValidators = {
return value; return value;
}, },
[CONFIG_KEYS.OCO_API_CUSTOM_HEADERS](value) {
try {
// Custom headers must be a valid JSON string
if (typeof value === 'string') {
JSON.parse(value);
}
return value;
} catch (error) {
validateConfig(
CONFIG_KEYS.OCO_API_CUSTOM_HEADERS,
false,
'Must be a valid JSON string of headers'
);
}
},
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT](value: any) { [CONFIG_KEYS.OCO_TOKENS_MAX_INPUT](value: any) {
value = parseInt(value); value = parseInt(value);
validateConfig( validateConfig(
@@ -380,6 +397,7 @@ export type ConfigType = {
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT]: number; [CONFIG_KEYS.OCO_TOKENS_MAX_INPUT]: number;
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT]: number; [CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT]: number;
[CONFIG_KEYS.OCO_API_URL]?: string; [CONFIG_KEYS.OCO_API_URL]?: string;
[CONFIG_KEYS.OCO_API_CUSTOM_HEADERS]?: string;
[CONFIG_KEYS.OCO_DESCRIPTION]: boolean; [CONFIG_KEYS.OCO_DESCRIPTION]: boolean;
[CONFIG_KEYS.OCO_EMOJI]: boolean; [CONFIG_KEYS.OCO_EMOJI]: boolean;
[CONFIG_KEYS.OCO_WHY]: boolean; [CONFIG_KEYS.OCO_WHY]: boolean;
@@ -462,6 +480,7 @@ const getEnvConfig = (envPath: string) => {
OCO_MODEL: process.env.OCO_MODEL, OCO_MODEL: process.env.OCO_MODEL,
OCO_API_URL: process.env.OCO_API_URL, OCO_API_URL: process.env.OCO_API_URL,
OCO_API_KEY: process.env.OCO_API_KEY, OCO_API_KEY: process.env.OCO_API_KEY,
OCO_API_CUSTOM_HEADERS: process.env.OCO_API_CUSTOM_HEADERS,
OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER as OCO_AI_PROVIDER_ENUM, OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER as OCO_AI_PROVIDER_ENUM,
OCO_TOKENS_MAX_INPUT: parseConfigVarValue(process.env.OCO_TOKENS_MAX_INPUT), OCO_TOKENS_MAX_INPUT: parseConfigVarValue(process.env.OCO_TOKENS_MAX_INPUT),
+1
View File
@@ -11,6 +11,7 @@ export interface AiEngineConfig {
maxTokensOutput: number; maxTokensOutput: number;
maxTokensInput: number; maxTokensInput: number;
baseURL?: string; baseURL?: string;
customHeaders?: Record<string, string>;
} }
type Client = type Client =
+8 -1
View File
@@ -11,11 +11,18 @@ export class OllamaEngine implements AiEngine {
constructor(config) { constructor(config) {
this.config = config; this.config = config;
// Combine base headers with custom headers
const headers = {
'Content-Type': 'application/json',
...config.customHeaders
};
this.client = axios.create({ this.client = axios.create({
url: config.baseURL url: config.baseURL
? `${config.baseURL}/${config.apiKey}` ? `${config.baseURL}/${config.apiKey}`
: 'http://localhost:11434/api/chat', : 'http://localhost:11434/api/chat',
headers: { 'Content-Type': 'application/json' } headers
}); });
} }
+17 -5
View File
@@ -1,6 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import { OpenAI } from 'openai'; import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff'; import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { parseCustomHeaders } from '../utils/engine';
import { removeContentTags } from '../utils/removeContentTags'; import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount'; import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine'; import { AiEngine, AiEngineConfig } from './Engine';
@@ -14,11 +15,22 @@ export class OpenAiEngine implements AiEngine {
constructor(config: OpenAiConfig) { constructor(config: OpenAiConfig) {
this.config = config; this.config = config;
if (!config.baseURL) { const clientOptions: OpenAI.ClientOptions = {
this.client = new OpenAI({ apiKey: config.apiKey }); apiKey: config.apiKey
} else { };
this.client = new OpenAI({ apiKey: config.apiKey, baseURL: config.baseURL });
if (config.baseURL) {
clientOptions.baseURL = config.baseURL;
} }
if (config.customHeaders) {
const headers = parseCustomHeaders(config.customHeaders);
if (Object.keys(headers).length > 0) {
clientOptions.defaultHeaders = headers;
}
}
this.client = new OpenAI(clientOptions);
} }
public generateCommitMessage = async ( public generateCommitMessage = async (
@@ -42,7 +54,7 @@ export class OpenAiEngine implements AiEngine {
this.config.maxTokensInput - this.config.maxTokensOutput this.config.maxTokensInput - this.config.maxTokensOutput
) )
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens); throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
const completion = await this.client.chat.completions.create(params); const completion = await this.client.chat.completions.create(params);
const message = completion.choices[0].message; const message = completion.choices[0].message;
+24 -1
View File
@@ -12,16 +12,39 @@ import { GroqEngine } from '../engine/groq';
import { MLXEngine } from '../engine/mlx'; import { MLXEngine } from '../engine/mlx';
import { DeepseekEngine } from '../engine/deepseek'; import { DeepseekEngine } from '../engine/deepseek';
export function parseCustomHeaders(headers: any): Record<string, string> {
let parsedHeaders = {};
if (!headers) {
return parsedHeaders;
}
try {
if (typeof headers === 'object' && !Array.isArray(headers)) {
parsedHeaders = headers;
} else {
parsedHeaders = JSON.parse(headers);
}
} catch (error) {
console.warn('Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers');
}
return parsedHeaders;
}
export function getEngine(): AiEngine { export function getEngine(): AiEngine {
const config = getConfig(); const config = getConfig();
const provider = config.OCO_AI_PROVIDER; const provider = config.OCO_AI_PROVIDER;
const customHeaders = parseCustomHeaders(config.OCO_API_CUSTOM_HEADERS);
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
model: config.OCO_MODEL!, model: config.OCO_MODEL!,
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!, maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!, maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
baseURL: config.OCO_API_URL!, baseURL: config.OCO_API_URL!,
apiKey: config.OCO_API_KEY! apiKey: config.OCO_API_KEY!,
customHeaders
}; };
switch (provider) { switch (provider) {
+24
View File
@@ -122,6 +122,30 @@ describe('config', () => {
expect(config.OCO_ONE_LINE_COMMIT).toEqual(false); expect(config.OCO_ONE_LINE_COMMIT).toEqual(false);
expect(config.OCO_OMIT_SCOPE).toEqual(true); expect(config.OCO_OMIT_SCOPE).toEqual(true);
}); });
it('should handle custom HTTP headers correctly', async () => {
globalConfigFile = await generateConfig('.opencommit', {
OCO_API_CUSTOM_HEADERS: '{"X-Global-Header": "global-value"}'
});
envConfigFile = await generateConfig('.env', {
OCO_API_CUSTOM_HEADERS: '{"Authorization": "Bearer token123", "X-Custom-Header": "test-value"}'
});
const config = getConfig({
globalPath: globalConfigFile.filePath,
envPath: envConfigFile.filePath
});
expect(config).not.toEqual(null);
expect(config.OCO_API_CUSTOM_HEADERS).toEqual({"Authorization": "Bearer token123", "X-Custom-Header": "test-value"});
// No need to parse JSON again since it's already an object
const parsedHeaders = config.OCO_API_CUSTOM_HEADERS;
expect(parsedHeaders).toHaveProperty('Authorization', 'Bearer token123');
expect(parsedHeaders).toHaveProperty('X-Custom-Header', 'test-value');
expect(parsedHeaders).not.toHaveProperty('X-Global-Header');
});
it('should handle empty local config correctly', async () => { it('should handle empty local config correctly', async () => {
globalConfigFile = await generateConfig('.opencommit', { globalConfigFile = await generateConfig('.opencommit', {