Skip to content

Commit 6c48c93

Browse files
committed
✨ add custom HTTP headers support via OCO_API_CUSTOM_HEADERS
Add OCO_API_CUSTOM_HEADERS variable to README, config enum, and env parsing to allow JSON string of custom headers. Validate that custom headers are valid JSON in config validator. Extend AiEngineConfig with customHeaders and pass headers to OllamaEngine and OpenAiEngine clients when creating requests. Parse custom headers in utils/engine and warn on invalid format. Add unit tests to ensure OCO_API_CUSTOM_HEADERS is handled correctly and merged from env over global config. This enables users to send additional headers such as Authorization or tracing headers with LLM API calls.
1 parent 25c6a0d commit 6c48c93

File tree

7 files changed

+99
-7
lines changed

7 files changed

+99
-7
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ Create a `.env` file and add OpenCommit config variables there like this:
109109
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama, gemini, flowise, deepseek>
110110
OCO_API_KEY=<your OpenAI API token> // or other LLM provider API token
111111
OCO_API_URL=<may be used to set proxy path to OpenAI api>
112+
OCO_API_CUSTOM_HEADERS=<JSON string of custom HTTP headers to include in API requests>
112113
OCO_TOKENS_MAX_INPUT=<max model token limit (default: 4096)>
113114
OCO_TOKENS_MAX_OUTPUT=<max response tokens (default: 500)>
114115
OCO_DESCRIPTION=<postface a message with ~3 sentences description of the changes>

src/commands/config.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export enum CONFIG_KEYS {
2525
OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT',
2626
OCO_TEST_MOCK_TYPE = 'OCO_TEST_MOCK_TYPE',
2727
OCO_API_URL = 'OCO_API_URL',
28+
OCO_API_CUSTOM_HEADERS = 'OCO_API_CUSTOM_HEADERS',
2829
OCO_OMIT_SCOPE = 'OCO_OMIT_SCOPE',
2930
OCO_GITPUSH = 'OCO_GITPUSH' // todo: deprecate
3031
}
@@ -204,6 +205,22 @@ export const configValidators = {
204205
return value;
205206
},
206207

208+
[CONFIG_KEYS.OCO_API_CUSTOM_HEADERS](value) {
209+
try {
210+
// Custom headers must be a valid JSON string
211+
if (typeof value === 'string') {
212+
JSON.parse(value);
213+
}
214+
return value;
215+
} catch (error) {
216+
validateConfig(
217+
CONFIG_KEYS.OCO_API_CUSTOM_HEADERS,
218+
false,
219+
'Must be a valid JSON string of headers'
220+
);
221+
}
222+
},
223+
207224
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT](value: any) {
208225
value = parseInt(value);
209226
validateConfig(
@@ -380,6 +397,7 @@ export type ConfigType = {
380397
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT]: number;
381398
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT]: number;
382399
[CONFIG_KEYS.OCO_API_URL]?: string;
400+
[CONFIG_KEYS.OCO_API_CUSTOM_HEADERS]?: string;
383401
[CONFIG_KEYS.OCO_DESCRIPTION]: boolean;
384402
[CONFIG_KEYS.OCO_EMOJI]: boolean;
385403
[CONFIG_KEYS.OCO_WHY]: boolean;
@@ -462,6 +480,7 @@ const getEnvConfig = (envPath: string) => {
462480
OCO_MODEL: process.env.OCO_MODEL,
463481
OCO_API_URL: process.env.OCO_API_URL,
464482
OCO_API_KEY: process.env.OCO_API_KEY,
483+
OCO_API_CUSTOM_HEADERS: process.env.OCO_API_CUSTOM_HEADERS,
465484
OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER as OCO_AI_PROVIDER_ENUM,
466485

467486
OCO_TOKENS_MAX_INPUT: parseConfigVarValue(process.env.OCO_TOKENS_MAX_INPUT),

src/engine/Engine.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface AiEngineConfig {
1111
maxTokensOutput: number;
1212
maxTokensInput: number;
1313
baseURL?: string;
14+
customHeaders?: Record<string, string>;
1415
}
1516

1617
type Client =

src/engine/ollama.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@ export class OllamaEngine implements AiEngine {
1111

1212
constructor(config) {
1313
this.config = config;
14+
15+
// Combine base headers with custom headers
16+
const headers = {
17+
'Content-Type': 'application/json',
18+
...config.customHeaders
19+
};
20+
1421
this.client = axios.create({
1522
url: config.baseURL
1623
? `${config.baseURL}/${config.apiKey}`
1724
: 'http://localhost:11434/api/chat',
18-
headers: { 'Content-Type': 'application/json' }
25+
headers
1926
});
2027
}
2128

src/engine/openAi.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,34 @@ export class OpenAiEngine implements AiEngine {
1414
constructor(config: OpenAiConfig) {
1515
this.config = config;
1616

17-
if (!config.baseURL) {
18-
this.client = new OpenAI({ apiKey: config.apiKey });
19-
} else {
20-
this.client = new OpenAI({ apiKey: config.apiKey, baseURL: config.baseURL });
17+
// Configuration options for the OpenAI client
18+
const clientOptions: any = {
19+
apiKey: config.apiKey
20+
};
21+
22+
// Add baseURL if present
23+
if (config.baseURL) {
24+
clientOptions.baseURL = config.baseURL;
25+
}
26+
27+
// Add custom headers if present
28+
if (config.customHeaders) {
29+
try {
30+
let headers = config.customHeaders;
31+
// If the headers are a string, try to parse them as JSON
32+
if (typeof config.customHeaders === 'string') {
33+
headers = JSON.parse(config.customHeaders);
34+
}
35+
36+
if (headers && typeof headers === 'object' && Object.keys(headers).length > 0) {
37+
clientOptions.defaultHeaders = headers;
38+
}
39+
} catch (error) {
40+
// Silently ignore parsing errors
41+
}
2142
}
43+
44+
this.client = new OpenAI(clientOptions);
2245
}
2346

2447
public generateCommitMessage = async (
@@ -42,7 +65,7 @@ export class OpenAiEngine implements AiEngine {
4265
this.config.maxTokensInput - this.config.maxTokensOutput
4366
)
4467
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
45-
68+
4669
const completion = await this.client.chat.completions.create(params);
4770

4871
const message = completion.choices[0].message;

src/utils/engine.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,29 @@ export function getEngine(): AiEngine {
1616
const config = getConfig();
1717
const provider = config.OCO_AI_PROVIDER;
1818

19+
// Parse custom headers if provided
20+
let customHeaders = {};
21+
if (config.OCO_API_CUSTOM_HEADERS) {
22+
try {
23+
// If it's already an object, no need to parse it
24+
if (typeof config.OCO_API_CUSTOM_HEADERS === 'object' && !Array.isArray(config.OCO_API_CUSTOM_HEADERS)) {
25+
customHeaders = config.OCO_API_CUSTOM_HEADERS;
26+
} else {
27+
// Try to parse as JSON
28+
customHeaders = JSON.parse(config.OCO_API_CUSTOM_HEADERS);
29+
}
30+
} catch (error) {
31+
console.warn('Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers');
32+
}
33+
}
34+
1935
const DEFAULT_CONFIG = {
2036
model: config.OCO_MODEL!,
2137
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
2238
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
2339
baseURL: config.OCO_API_URL!,
24-
apiKey: config.OCO_API_KEY!
40+
apiKey: config.OCO_API_KEY!,
41+
customHeaders // Add custom headers to the configuration
2542
};
2643

2744
switch (provider) {

test/unit/config.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,30 @@ describe('config', () => {
122122
expect(config.OCO_ONE_LINE_COMMIT).toEqual(false);
123123
expect(config.OCO_OMIT_SCOPE).toEqual(true);
124124
});
125+
126+
it('should handle custom HTTP headers correctly', async () => {
127+
globalConfigFile = await generateConfig('.opencommit', {
128+
OCO_API_CUSTOM_HEADERS: '{"X-Global-Header": "global-value"}'
129+
});
130+
131+
envConfigFile = await generateConfig('.env', {
132+
OCO_API_CUSTOM_HEADERS: '{"Authorization": "Bearer token123", "X-Custom-Header": "test-value"}'
133+
});
134+
135+
const config = getConfig({
136+
globalPath: globalConfigFile.filePath,
137+
envPath: envConfigFile.filePath
138+
});
139+
140+
expect(config).not.toEqual(null);
141+
expect(config.OCO_API_CUSTOM_HEADERS).toEqual('{"Authorization": "Bearer token123", "X-Custom-Header": "test-value"}');
142+
143+
// Verify that the JSON can be parsed correctly
144+
const parsedHeaders = JSON.parse(config.OCO_API_CUSTOM_HEADERS);
145+
expect(parsedHeaders).toHaveProperty('Authorization', 'Bearer token123');
146+
expect(parsedHeaders).toHaveProperty('X-Custom-Header', 'test-value');
147+
expect(parsedHeaders).not.toHaveProperty('X-Global-Header');
148+
});
125149

126150
it('should handle empty local config correctly', async () => {
127151
globalConfigFile = await generateConfig('.opencommit', {

0 commit comments

Comments
 (0)