feat(proxy): add universal proxy support and fix Gemini model resolution (#536)

Integrated undici ProxyAgent for native fetch and HttpsProxyAgent for axios/openai/anthropic. Upgraded @google/generative-ai to fix #536. Added OCO_PROXY config.

Co-authored-by: uni <uni@hanwei.ink>
This commit is contained in:
sky
2026-03-28 16:14:49 +00:00
committed by uni
parent f51393e37a
commit e27007b6fe
16 changed files with 123 additions and 158893 deletions

View File

@@ -5,9 +5,10 @@ import { cli } from 'cleye';
import packageJSON from '../package.json';
import { commit } from './commands/commit';
import { commitlintConfigCommand } from './commands/commitlint';
import { configCommand } from './commands/config';
import { configCommand, getConfig } from './commands/config';
import { hookCommand, isHookCalled } from './commands/githook.js';
import { prepareCommitMessageHook } from './commands/prepare-commit-msg-hook';
import { setupProxy } from './utils/proxy';
import {
setupCommand,
isFirstRun,
@@ -18,6 +19,9 @@ import { modelsCommand } from './commands/models';
import { checkIsLatestVersion } from './utils/checkIsLatestVersion';
import { runMigrations } from './migrations/_run.js';
const config = getConfig();
setupProxy(config.OCO_PROXY);
const extraArgs = process.argv.slice(2);
cli(

View File

@@ -25,6 +25,7 @@ export enum CONFIG_KEYS {
OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT',
OCO_TEST_MOCK_TYPE = 'OCO_TEST_MOCK_TYPE',
OCO_API_URL = 'OCO_API_URL',
OCO_PROXY = 'OCO_PROXY',
OCO_API_CUSTOM_HEADERS = 'OCO_API_CUSTOM_HEADERS',
OCO_OMIT_SCOPE = 'OCO_OMIT_SCOPE',
OCO_GITPUSH = 'OCO_GITPUSH', // todo: deprecate
@@ -727,6 +728,15 @@ export const configValidators = {
return value;
},
[CONFIG_KEYS.OCO_PROXY](value: any) {
validateConfig(
CONFIG_KEYS.OCO_PROXY,
typeof value === 'string',
`${value} is not a valid URL. It should start with 'http://' or 'https://'.`
);
return value;
},
[CONFIG_KEYS.OCO_MODEL](value: any, config: any = {}) {
validateConfig(
CONFIG_KEYS.OCO_MODEL,
@@ -880,6 +890,7 @@ export type ConfigType = {
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT]: number;
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT]: number;
[CONFIG_KEYS.OCO_API_URL]?: string;
[CONFIG_KEYS.OCO_PROXY]?: string;
[CONFIG_KEYS.OCO_API_CUSTOM_HEADERS]?: string;
[CONFIG_KEYS.OCO_DESCRIPTION]: boolean;
[CONFIG_KEYS.OCO_EMOJI]: boolean;
@@ -964,6 +975,10 @@ const getEnvConfig = (envPath: string) => {
return {
OCO_MODEL: process.env.OCO_MODEL,
OCO_API_URL: process.env.OCO_API_URL,
OCO_PROXY:
process.env.OCO_PROXY ||
process.env.HTTPS_PROXY ||
process.env.HTTP_PROXY,
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,
@@ -1189,6 +1204,11 @@ function getConfigKeyDetails(key) {
'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_PROXY:
return {
description: 'HTTP/HTTPS Proxy URL',
values: ["URL string (must start with 'http://' or 'https://')"]
};
case CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
return {
description: 'Message template placeholder',

View File

@@ -11,6 +11,7 @@ export interface AiEngineConfig {
maxTokensOutput: number;
maxTokensInput: number;
baseURL?: string;
proxy?: string;
customHeaders?: Record<string, string>;
}

View File

@@ -1,4 +1,5 @@
import AnthropicClient from '@anthropic-ai/sdk';
import { HttpsProxyAgent } from 'https-proxy-agent';
import {
MessageCreateParamsNonStreaming,
MessageParam
@@ -18,7 +19,15 @@ export class AnthropicEngine implements AiEngine {
constructor(config) {
this.config = config;
this.client = new AnthropicClient({ apiKey: this.config.apiKey });
const clientOptions: any = { apiKey: this.config.apiKey };
const proxy =
config.proxy || process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
if (proxy) {
clientOptions.httpAgent = new HttpsProxyAgent(proxy);
}
this.client = new AnthropicClient(clientOptions);
}
public generateCommitMessage = async (

View File

@@ -29,10 +29,15 @@ export class GeminiEngine implements AiEngine {
.map((m) => m.content)
.join('\n');
const gemini = this.client.getGenerativeModel({
model: this.config.model,
systemInstruction
});
const gemini = this.client.getGenerativeModel(
{
model: this.config.model,
systemInstruction
},
{
baseUrl: this.config.baseURL
}
);
const contents = messages
.filter((m) => m.role !== 'system')

View File

@@ -1,4 +1,5 @@
import { OpenAI } from 'openai';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { normalizeEngineError } from '../utils/engineErrorHandler';
import { removeContentTags } from '../utils/removeContentTags';

View File

@@ -1,4 +1,5 @@
import { OpenAI } from 'openai';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { parseCustomHeaders } from '../utils/engine';
import { normalizeEngineError } from '../utils/engineErrorHandler';
@@ -23,6 +24,12 @@ export class OpenAiEngine implements AiEngine {
clientOptions.baseURL = config.baseURL;
}
const proxy =
config.proxy || process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
if (proxy) {
clientOptions.httpAgent = new HttpsProxyAgent(proxy);
}
if (config.customHeaders) {
const headers = parseCustomHeaders(config.customHeaders);
if (Object.keys(headers).length > 0) {

View File

@@ -47,6 +47,7 @@ export function getEngine(): AiEngine {
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
baseURL: config.OCO_API_URL!,
proxy: config.OCO_PROXY!,
apiKey: config.OCO_API_KEY!,
customHeaders
};

21
src/utils/proxy.ts Normal file
View File

@@ -0,0 +1,21 @@
import { setGlobalDispatcher, ProxyAgent } from 'undici';
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
export function setupProxy(proxyUrl?: string) {
const proxy = proxyUrl || process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
if (proxy) {
try {
// Set global dispatcher for undici (affects globalThis.fetch used by Gemini and others)
const dispatcher = new ProxyAgent(proxy);
setGlobalDispatcher(dispatcher);
// Set axios global agent
const agent = new HttpsProxyAgent(proxy);
axios.defaults.httpsAgent = agent;
axios.defaults.proxy = false; // Disable axios built-in proxy handling to use agent
} catch (error) {
console.warn(`[Proxy Error] Failed to set proxy: ${error.message}`);
}
}
}