From 505972d2d8ad90e31f5dafdffabf3e0329c5cba2 Mon Sep 17 00:00:00 2001 From: Huoyuuu Date: Tue, 16 Jun 2026 01:25:36 +0800 Subject: [PATCH] feat: support custom OpenAI headers Some OpenAI-compatible relay/NewAPI providers sit behind WAF or Cloudflare rules that may block requests using the SDK default request headers. Allow users to configure additional HTTP headers, such as a custom User-Agent, without patching the extension source code. Changes: - add a top-level `headers` setting and normalize string header values - merge user and project headers with project-level precedence - pass configured headers to the OpenAI SDK via `defaultHeaders` - include headers in the shared client cache key - document the setting with a User-Agent example - add resolver coverage for header merging behavior Validation: - npx tsx --test src/tests/settings-and-notify.test.ts - npm run typecheck - npm run bundle --- README.md | 3 +++ README_cn.md | 3 +++ README_en.md | 3 +++ docs/guide.md | 4 ++++ src/common/openai-client.ts | 3 ++- src/extension.ts | 1 + src/settings.ts | 23 ++++++++++++++++++++ src/tests/settings-and-notify.test.ts | 30 +++++++++++++++++++++++++++ 8 files changed, 69 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ee11be0..76b677b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ "BASE_URL": "https://api.deepseek.com", "API_KEY": "sk-..." }, + "headers": { + "User-Agent": "Mozilla/5.0 ..." + }, "thinkingEnabled": true, "reasoningEffort": "max" } diff --git a/README_cn.md b/README_cn.md index ee11be0..76b677b 100644 --- a/README_cn.md +++ b/README_cn.md @@ -13,6 +13,9 @@ "BASE_URL": "https://api.deepseek.com", "API_KEY": "sk-..." }, + "headers": { + "User-Agent": "Mozilla/5.0 ..." + }, "thinkingEnabled": true, "reasoningEffort": "max" } diff --git a/README_en.md b/README_en.md index 40b199b..22ba7d4 100644 --- a/README_en.md +++ b/README_en.md @@ -13,6 +13,9 @@ Create `~/.deepcode/settings.json` with: "BASE_URL": "https://api.deepseek.com", "API_KEY": "sk-..." }, + "headers": { + "User-Agent": "Mozilla/5.0 ..." + }, "thinkingEnabled": true, "reasoningEffort": "max" } diff --git a/docs/guide.md b/docs/guide.md index 7b2afcc..199510f 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -228,6 +228,9 @@ Persist state and notify the webview "BASE_URL": "https://api.deepseek.com", "MODEL": "deepseek-v4-pro" }, + "headers": { + "User-Agent": "Mozilla/5.0 ..." + }, "thinkingEnabled": true, "reasoningEffort": "max", "notify": "~/.deepcode/notify.sh" @@ -241,6 +244,7 @@ Persist state and notify the webview | `env.API_KEY` | string | Yes | - | API key for the configured provider | | `env.BASE_URL` | string | No | `https://api.deepseek.com` | Base URL for a DeepSeek or other OpenAI-compatible endpoint | | `env.MODEL` | string | No | `deepseek-v4-pro` | Model identifier passed to `chat.completions.create()` | +| `headers` | object | No | `{}` | Extra HTTP headers passed to the OpenAI-compatible SDK client, useful for providers that require custom headers such as `User-Agent` | | `thinkingEnabled` | boolean | No | `true` for `deepseek-v4-flash` and `deepseek-v4-pro`; otherwise `false` | Enables the optional `thinking` request field when set to `true` | | `reasoningEffort` | `"high"` or `"max"` | No | `"max"` | Controls DeepSeek thinking strength via `reasoning_effort` when thinking mode is enabled | | `notify` | string | No | - | Executable script path triggered when a task ends in `completed` or `failed`, with `DURATION` set to the elapsed seconds | diff --git a/src/common/openai-client.ts b/src/common/openai-client.ts index d3b56c0..7faa8ad 100644 --- a/src/common/openai-client.ts +++ b/src/common/openai-client.ts @@ -51,7 +51,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { }; } - const cacheKey = `${settings.apiKey}::${settings.baseURL}`; + const cacheKey = `${settings.apiKey}::${settings.baseURL}::${JSON.stringify(settings.headers)}`; if (cachedOpenAI && cachedOpenAIKey === cacheKey) { return { client: cachedOpenAI, @@ -72,6 +72,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { cachedOpenAI = new OpenAI({ apiKey: settings.apiKey, baseURL: settings.baseURL || undefined, + defaultHeaders: settings.headers, // eslint-disable-next-line @typescript-eslint/no-explicit-any fetch: (url: any, init: any) => undiciFetch(url, { ...init, dispatcher: keepAliveAgent }), }); diff --git a/src/extension.ts b/src/extension.ts index 1f800df..49a700d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -424,6 +424,7 @@ class DeepcodingViewProvider implements vscode.WebviewViewProvider { const client = new OpenAI({ apiKey, baseURL: baseURL || undefined, + defaultHeaders: settings.headers, }); return { diff --git a/src/settings.ts b/src/settings.ts index c91f030..7db2da4 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -47,6 +47,7 @@ export type EnabledSkillsSettings = Record; export type DeepcodingSettings = { env?: DeepcodingEnv; + headers?: Record; model?: string; temperature?: number; thinkingEnabled?: boolean; @@ -62,6 +63,7 @@ export type DeepcodingSettings = { export type ResolvedDeepcodingSettings = { env: Record; + headers: Record; apiKey?: string; baseURL: string; model: string; @@ -230,6 +232,22 @@ function normalizeEnv(env: DeepcodingSettings["env"]): Record { return result; } +function normalizeHeaders(headers: unknown): Record { + const result: Record = {}; + if (!headers || typeof headers !== "object" || Array.isArray(headers)) { + return result; + } + + for (const [key, value] of Object.entries(headers)) { + const headerName = key.trim(); + if (!headerName || typeof value !== "string") { + continue; + } + result[headerName] = value; + } + return result; +} + export function collectDeepcodeEnv(processEnv: SettingsProcessEnv = process.env): Record { const result: Record = {}; for (const [key, value] of Object.entries(processEnv)) { @@ -322,6 +340,10 @@ export function resolveSettingsSources( ...projectEnv, ...systemEnv, }; + const headers = { + ...normalizeHeaders(userSettings?.headers), + ...normalizeHeaders(projectSettings?.headers), + }; const model = trimString(systemEnv.MODEL) || @@ -380,6 +402,7 @@ export function resolveSettingsSources( return { env, + headers, apiKey: trimString(env.API_KEY) || undefined, baseURL: trimString(env.BASE_URL) || defaults.baseURL, model, diff --git a/src/tests/settings-and-notify.test.ts b/src/tests/settings-and-notify.test.ts index ceddc43..a678881 100644 --- a/src/tests/settings-and-notify.test.ts +++ b/src/tests/settings-and-notify.test.ts @@ -191,6 +191,36 @@ test("resolveSettingsSources applies user, project, and DEEPCODE environment pre assert.equal(resolved.env.WEBHOOK, "system-webhook"); }); +test("resolveSettingsSources merges headers with project precedence", () => { + const resolved = resolveSettingsSources( + { + headers: { + "User-Agent": "user-agent", + "X-User": "1", + "X-Ignore": undefined, + }, + }, + { + headers: { + "User-Agent": "project-agent", + "X-Project": "2", + "X-Number": 123 as never, + }, + }, + { + model: "default-model", + baseURL: "https://default.example.com", + }, + TEST_PROCESS_ENV + ); + + assert.deepEqual(resolved.headers, { + "User-Agent": "project-agent", + "X-User": "1", + "X-Project": "2", + }); +}); + test("resolveSettingsSources merges permission settings", () => { const resolved = resolveSettingsSources( {