feat: Add proxy support for AI providers (#1382)

This commit is contained in:
Alex Liu
2025-11-06 18:55:24 +08:00
committed by GitHub
parent 91e76b1ccc
commit ac4328ae86
16 changed files with 181 additions and 22 deletions

View File

@@ -0,0 +1,47 @@
---
"task-master-ai": patch
---
Added opt-in proxy support for all AI providers - respects http_proxy/https_proxy environment variables when enabled.
When using Task Master in corporate or restricted network environments that require HTTP/HTTPS proxies, API calls to AI providers (OpenAI, Anthropic, Google, AWS Bedrock, etc.) would previously fail with ECONNRESET errors. This update adds seamless proxy support that can be enabled via environment variable or configuration file.
**How to enable:**
Proxy support is opt-in. Enable it using either method:
**Method 1: Environment Variable**
```bash
export TASKMASTER_ENABLE_PROXY=true
export http_proxy=http://your-proxy:port
export https_proxy=http://your-proxy:port
export no_proxy=localhost,127.0.0.1 # Optional: bypass proxy for specific hosts
# Then use Task Master normally
task-master add-task "Create a new feature"
```
**Method 2: Configuration File**
Add to `.taskmaster/config.json`:
```json
{
"global": {
"enableProxy": true
}
}
```
Then set your proxy environment variables:
```bash
export http_proxy=http://your-proxy:port
export https_proxy=http://your-proxy:port
```
**Technical details:**
- Uses undici's `EnvHttpProxyAgent` for automatic proxy detection
- Centralized implementation in `BaseAIProvider` for consistency across all providers
- Supports all AI providers: OpenAI, Anthropic, Perplexity, Azure OpenAI, Google AI, Google Vertex AI, AWS Bedrock, and OpenAI-compatible providers
- Opt-in design ensures users without proxy requirements are not affected
- Priority: `TASKMASTER_ENABLE_PROXY` environment variable > `config.json` setting

View File

@@ -14,4 +14,9 @@ OLLAMA_API_KEY=YOUR_OLLAMA_API_KEY_HERE
VERTEX_PROJECT_ID=your-gcp-project-id
VERTEX_LOCATION=us-central1
# Optional: Path to service account credentials JSON file (alternative to API key)
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-credentials.json
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-credentials.json
# Proxy Configuration (Optional)
# Enable proxy support for AI provider API calls
# When enabled, automatically uses http_proxy/https_proxy environment variables
TASKMASTER_ENABLE_PROXY=false

37
package-lock.json generated
View File

@@ -66,6 +66,7 @@
"ora": "^8.2.0",
"simple-git": "^3.28.0",
"steno": "^4.0.2",
"undici": "^7.16.0",
"uuid": "^11.1.0",
"zod": "^4.1.11"
},
@@ -738,6 +739,7 @@
"version": "3.25.76",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -2815,6 +2817,7 @@
"version": "7.28.4",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -3857,6 +3860,7 @@
"version": "6.3.1",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@@ -5763,7 +5767,6 @@
"version": "0.23.2",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
@@ -6301,7 +6304,6 @@
"version": "0.23.2",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
@@ -6310,6 +6312,7 @@
"version": "3.25.76",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -6600,6 +6603,7 @@
"node_modules/@modelcontextprotocol/sdk/node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -6700,6 +6704,7 @@
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -9733,6 +9738,7 @@
"node_modules/@types/node": {
"version": "22.18.6",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -9754,6 +9760,7 @@
"version": "19.1.8",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -9762,6 +9769,7 @@
"version": "19.1.6",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -10208,6 +10216,7 @@
"node_modules/acorn": {
"version": "8.15.0",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -10274,6 +10283,7 @@
"node_modules/ai": {
"version": "5.0.57",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@ai-sdk/gateway": "1.0.30",
"@ai-sdk/provider": "2.0.0",
@@ -10493,6 +10503,7 @@
"node_modules/ajv": {
"version": "8.17.1",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -11499,6 +11510,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -13365,7 +13377,8 @@
"node_modules/devtools-protocol": {
"version": "0.0.1312386",
"dev": true,
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/dezalgo": {
"version": "1.0.4",
@@ -13961,6 +13974,7 @@
"version": "0.25.10",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -14274,6 +14288,7 @@
"node_modules/express": {
"version": "4.21.2",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -16616,6 +16631,7 @@
"version": "6.3.1",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.0",
"ansi-escapes": "^7.0.0",
@@ -17573,6 +17589,7 @@
"version": "29.7.0",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -19190,6 +19207,7 @@
"version": "1.4.0",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 10.16.0"
}
@@ -19515,7 +19533,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -19740,7 +19757,6 @@
"version": "1.4.0",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@@ -19871,6 +19887,7 @@
"node_modules/marked": {
"version": "15.0.12",
"license": "MIT",
"peer": true,
"bin": {
"marked": "bin/marked.js"
},
@@ -22596,6 +22613,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -23979,6 +23997,7 @@
"integrity": "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@oxc-project/types": "=0.93.0",
"@rolldown/pluginutils": "1.0.0-beta.41",
@@ -26444,6 +26463,7 @@
"version": "5.9.2",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -26530,6 +26550,8 @@
},
"node_modules/undici": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
@@ -26560,6 +26582,7 @@
"version": "11.0.5",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^3.0.0",
"bail": "^2.0.0",
@@ -27002,6 +27025,7 @@
"version": "5.4.20",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -27114,7 +27138,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=12"
}
@@ -27842,6 +27865,7 @@
"node_modules/zod": {
"version": "4.1.11",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -28870,6 +28894,7 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",

View File

@@ -104,6 +104,7 @@
"ora": "^8.2.0",
"simple-git": "^3.28.0",
"steno": "^4.0.2",
"undici": "^7.16.0",
"uuid": "^11.1.0",
"zod": "^4.1.11"
},

View File

@@ -55,7 +55,8 @@ const DEFAULTS = {
ollamaBaseURL: 'http://localhost:11434/api',
bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com',
responseLanguage: 'English',
enableCodebaseAnalysis: true
enableCodebaseAnalysis: true,
enableProxy: false
},
claudeCode: {},
codexCli: {},
@@ -703,6 +704,32 @@ function getCodebaseAnalysisEnabled(explicitRoot = null) {
return getGlobalConfig(explicitRoot).enableCodebaseAnalysis !== false;
}
function getProxyEnabled(explicitRoot = null) {
// Return boolean-safe value with default false
return getGlobalConfig(explicitRoot).enableProxy === true;
}
function isProxyEnabled(session = null, projectRoot = null) {
// Priority 1: Environment variable
const envFlag = resolveEnvVariable(
'TASKMASTER_ENABLE_PROXY',
session,
projectRoot
);
if (envFlag !== null && envFlag !== undefined && envFlag !== '') {
return envFlag.toLowerCase() === 'true' || envFlag === '1';
}
// Priority 2: MCP session environment (explicit check for parity with other flags)
if (session?.env?.TASKMASTER_ENABLE_PROXY) {
const mcpFlag = session.env.TASKMASTER_ENABLE_PROXY;
return mcpFlag.toLowerCase() === 'true' || mcpFlag === '1';
}
// Priority 3: Configuration file
return getProxyEnabled(projectRoot);
}
/**
* Gets model parameters (maxTokens, temperature) for a specific role,
* considering model-specific overrides from supported-models.json.
@@ -807,9 +834,9 @@ function isApiKeySet(providerName, session = null, projectRoot = null) {
const providersWithoutApiKeys = [
CUSTOM_PROVIDERS.OLLAMA,
CUSTOM_PROVIDERS.BEDROCK,
CUSTOM_PROVIDERS.MCP,
CUSTOM_PROVIDERS.GEMINI_CLI,
CUSTOM_PROVIDERS.GROK_CLI,
CUSTOM_PROVIDERS.MCP,
CUSTOM_PROVIDERS.CODEX_CLI
];
@@ -1188,6 +1215,8 @@ export {
getResponseLanguage,
getCodebaseAnalysisEnabled,
isCodebaseAnalysisEnabled,
getProxyEnabled,
isProxyEnabled,
getParametersForRole,
getUserId,
// API Key Checkers (still relevant)

View File

@@ -52,7 +52,8 @@ export class AnthropicAIProvider extends BaseAIProvider {
...(baseURL && { baseURL }),
headers: {
'anthropic-beta': 'output-128k-2025-02-19'
}
},
fetch: this.createProxyFetch()
});
} catch (error) {
this.handleError('client initialization', error);

View File

@@ -51,7 +51,8 @@ export class AzureProvider extends BaseAIProvider {
return createAzure({
apiKey,
baseURL
baseURL,
fetch: this.createProxyFetch()
});
} catch (error) {
this.handleError('client initialization', error);

View File

@@ -8,7 +8,9 @@ import {
NoObjectGeneratedError
} from 'ai';
import { jsonrepair } from 'jsonrepair';
import { log } from '../../scripts/modules/utils.js';
import { log, findProjectRoot } from '../../scripts/modules/utils.js';
import { isProxyEnabled } from '../../scripts/modules/config-manager.js';
import { EnvHttpProxyAgent } from 'undici';
/**
* Base class for all AI providers
@@ -22,6 +24,9 @@ export class BaseAIProvider {
// Each provider must set their name
this.name = this.constructor.name;
// Cache proxy agent to avoid creating multiple instances
this._proxyAgent = null;
/**
* Whether this provider needs explicit schema in JSON mode
* Can be overridden by subclasses
@@ -48,6 +53,37 @@ export class BaseAIProvider {
}
}
/**
* Creates a custom fetch function with proxy support.
* Only enables proxy when TASKMASTER_ENABLE_PROXY environment variable is set to 'true'
* or enableProxy is set to true in config.json.
* Automatically reads http_proxy/https_proxy environment variables when enabled.
* @returns {Function} Custom fetch function with proxy support, or undefined if proxy is disabled
*/
createProxyFetch() {
// Cache project root to avoid repeated lookups
if (!this._projectRoot) {
this._projectRoot = findProjectRoot();
}
const projectRoot = this._projectRoot;
if (!isProxyEnabled(null, projectRoot)) {
// Return undefined to use default fetch without proxy
return undefined;
}
// Proxy is enabled, create and return proxy fetch
if (!this._proxyAgent) {
this._proxyAgent = new EnvHttpProxyAgent();
}
return (url, options = {}) => {
return fetch(url, {
...options,
dispatcher: this._proxyAgent
});
};
}
/**
* Validates common parameters across all methods
* @param {object} params - Parameters to validate

View File

@@ -37,7 +37,8 @@ export class BedrockAIProvider extends BaseAIProvider {
const credentialProvider = fromNodeProviderChain();
return createAmazonBedrock({
credentialProvider
credentialProvider,
fetch: this.createProxyFetch()
});
} catch (error) {
this.handleError('client initialization', error);

View File

@@ -109,7 +109,8 @@ export class VertexAIProvider extends BaseAIProvider {
...authOptions,
projectId,
location,
...(baseURL && { baseURL })
...(baseURL && { baseURL }),
fetch: this.createProxyFetch()
});
} catch (error) {
this.handleError('client initialization', error);

View File

@@ -38,7 +38,8 @@ export class GoogleAIProvider extends BaseAIProvider {
return createGoogleGenerativeAI({
apiKey,
...(baseURL && { baseURL })
...(baseURL && { baseURL }),
fetch: this.createProxyFetch()
});
} catch (error) {
this.handleError('client initialization', error);

View File

@@ -106,7 +106,9 @@ export class OpenAICompatibleProvider extends BaseAIProvider {
const clientConfig = {
// Provider name for SDK (required, used for logging/debugging)
name: this.name.toLowerCase().replace(/[^a-z0-9]/g, '-')
name: this.name.toLowerCase().replace(/[^a-z0-9]/g, '-'),
// Add proxy support
fetch: this.createProxyFetch()
};
// Only include apiKey if provider requires it

View File

@@ -38,7 +38,8 @@ export class OpenAIProvider extends BaseAIProvider {
return createOpenAI({
apiKey,
...(baseURL && { baseURL })
...(baseURL && { baseURL }),
fetch: this.createProxyFetch()
});
} catch (error) {
this.handleError('client initialization', error);

View File

@@ -38,7 +38,8 @@ export class PerplexityAIProvider extends BaseAIProvider {
return createPerplexity({
apiKey,
baseURL: baseURL || 'https://api.perplexity.ai'
baseURL: baseURL || 'https://api.perplexity.ai',
fetch: this.createProxyFetch()
});
} catch (error) {
this.handleError('client initialization', error);

View File

@@ -10,7 +10,13 @@ jest.unstable_mockModule('@ai-sdk/openai-compatible', () => ({
// Mock utils
jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
log: jest.fn(),
resolveEnvVariable: jest.fn((key) => process.env[key])
resolveEnvVariable: jest.fn((key) => process.env[key]),
findProjectRoot: jest.fn(() => process.cwd()),
isEmpty: jest.fn(() => false)
}));
jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
isProxyEnabled: jest.fn(() => false)
}));
// Import after mocking

View File

@@ -32,7 +32,7 @@ jest.mock('chalk', () => ({
blue: jest.fn((text) => text),
green: jest.fn((text) => text),
yellow: jest.fn((text) => text),
white: jest.fn((text) => ({
white: jest.fn(() => ({
bold: jest.fn((text) => text)
})),
reset: jest.fn((text) => text),
@@ -70,13 +70,13 @@ const realSupportedModelsPath = path.resolve(
'../../scripts/modules/supported-models.json'
);
let REAL_SUPPORTED_MODELS_CONTENT;
let REAL_SUPPORTED_MODELS_DATA;
let _REAL_SUPPORTED_MODELS_DATA;
try {
REAL_SUPPORTED_MODELS_CONTENT = fs.readFileSync(
realSupportedModelsPath,
'utf-8'
);
REAL_SUPPORTED_MODELS_DATA = JSON.parse(REAL_SUPPORTED_MODELS_CONTENT);
_REAL_SUPPORTED_MODELS_DATA = JSON.parse(REAL_SUPPORTED_MODELS_CONTENT);
} catch (err) {
console.error(
'FATAL TEST SETUP ERROR: Could not read or parse real supported-models.json',
@@ -146,6 +146,7 @@ const DEFAULT_CONFIG = {
ollamaBaseURL: 'http://localhost:11434/api',
bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com',
enableCodebaseAnalysis: true,
enableProxy: false,
responseLanguage: 'English'
},
claudeCode: {},