Skip to content

Commit 505972d

Browse files
committed
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
1 parent a971e07 commit 505972d

8 files changed

Lines changed: 69 additions & 1 deletion

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
"BASE_URL": "https://api.deepseek.com",
1414
"API_KEY": "sk-..."
1515
},
16+
"headers": {
17+
"User-Agent": "Mozilla/5.0 ..."
18+
},
1619
"thinkingEnabled": true,
1720
"reasoningEffort": "max"
1821
}

README_cn.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
"BASE_URL": "https://api.deepseek.com",
1414
"API_KEY": "sk-..."
1515
},
16+
"headers": {
17+
"User-Agent": "Mozilla/5.0 ..."
18+
},
1619
"thinkingEnabled": true,
1720
"reasoningEffort": "max"
1821
}

README_en.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ Create `~/.deepcode/settings.json` with:
1313
"BASE_URL": "https://api.deepseek.com",
1414
"API_KEY": "sk-..."
1515
},
16+
"headers": {
17+
"User-Agent": "Mozilla/5.0 ..."
18+
},
1619
"thinkingEnabled": true,
1720
"reasoningEffort": "max"
1821
}

docs/guide.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@ Persist state and notify the webview
228228
"BASE_URL": "https://api.deepseek.com",
229229
"MODEL": "deepseek-v4-pro"
230230
},
231+
"headers": {
232+
"User-Agent": "Mozilla/5.0 ..."
233+
},
231234
"thinkingEnabled": true,
232235
"reasoningEffort": "max",
233236
"notify": "~/.deepcode/notify.sh"
@@ -241,6 +244,7 @@ Persist state and notify the webview
241244
| `env.API_KEY` | string | Yes | - | API key for the configured provider |
242245
| `env.BASE_URL` | string | No | `https://api.deepseek.com` | Base URL for a DeepSeek or other OpenAI-compatible endpoint |
243246
| `env.MODEL` | string | No | `deepseek-v4-pro` | Model identifier passed to `chat.completions.create()` |
247+
| `headers` | object | No | `{}` | Extra HTTP headers passed to the OpenAI-compatible SDK client, useful for providers that require custom headers such as `User-Agent` |
244248
| `thinkingEnabled` | boolean | No | `true` for `deepseek-v4-flash` and `deepseek-v4-pro`; otherwise `false` | Enables the optional `thinking` request field when set to `true` |
245249
| `reasoningEffort` | `"high"` or `"max"` | No | `"max"` | Controls DeepSeek thinking strength via `reasoning_effort` when thinking mode is enabled |
246250
| `notify` | string | No | - | Executable script path triggered when a task ends in `completed` or `failed`, with `DURATION` set to the elapsed seconds |

src/common/openai-client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): {
5151
};
5252
}
5353

54-
const cacheKey = `${settings.apiKey}::${settings.baseURL}`;
54+
const cacheKey = `${settings.apiKey}::${settings.baseURL}::${JSON.stringify(settings.headers)}`;
5555
if (cachedOpenAI && cachedOpenAIKey === cacheKey) {
5656
return {
5757
client: cachedOpenAI,
@@ -72,6 +72,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): {
7272
cachedOpenAI = new OpenAI({
7373
apiKey: settings.apiKey,
7474
baseURL: settings.baseURL || undefined,
75+
defaultHeaders: settings.headers,
7576
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7677
fetch: (url: any, init: any) => undiciFetch(url, { ...init, dispatcher: keepAliveAgent }),
7778
});

src/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ class DeepcodingViewProvider implements vscode.WebviewViewProvider {
424424
const client = new OpenAI({
425425
apiKey,
426426
baseURL: baseURL || undefined,
427+
defaultHeaders: settings.headers,
427428
});
428429

429430
return {

src/settings.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export type EnabledSkillsSettings = Record<string, boolean>;
4747

4848
export type DeepcodingSettings = {
4949
env?: DeepcodingEnv;
50+
headers?: Record<string, string | undefined>;
5051
model?: string;
5152
temperature?: number;
5253
thinkingEnabled?: boolean;
@@ -62,6 +63,7 @@ export type DeepcodingSettings = {
6263

6364
export type ResolvedDeepcodingSettings = {
6465
env: Record<string, string>;
66+
headers: Record<string, string>;
6567
apiKey?: string;
6668
baseURL: string;
6769
model: string;
@@ -230,6 +232,22 @@ function normalizeEnv(env: DeepcodingSettings["env"]): Record<string, string> {
230232
return result;
231233
}
232234

235+
function normalizeHeaders(headers: unknown): Record<string, string> {
236+
const result: Record<string, string> = {};
237+
if (!headers || typeof headers !== "object" || Array.isArray(headers)) {
238+
return result;
239+
}
240+
241+
for (const [key, value] of Object.entries(headers)) {
242+
const headerName = key.trim();
243+
if (!headerName || typeof value !== "string") {
244+
continue;
245+
}
246+
result[headerName] = value;
247+
}
248+
return result;
249+
}
250+
233251
export function collectDeepcodeEnv(processEnv: SettingsProcessEnv = process.env): Record<string, string> {
234252
const result: Record<string, string> = {};
235253
for (const [key, value] of Object.entries(processEnv)) {
@@ -322,6 +340,10 @@ export function resolveSettingsSources(
322340
...projectEnv,
323341
...systemEnv,
324342
};
343+
const headers = {
344+
...normalizeHeaders(userSettings?.headers),
345+
...normalizeHeaders(projectSettings?.headers),
346+
};
325347

326348
const model =
327349
trimString(systemEnv.MODEL) ||
@@ -380,6 +402,7 @@ export function resolveSettingsSources(
380402

381403
return {
382404
env,
405+
headers,
383406
apiKey: trimString(env.API_KEY) || undefined,
384407
baseURL: trimString(env.BASE_URL) || defaults.baseURL,
385408
model,

src/tests/settings-and-notify.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,36 @@ test("resolveSettingsSources applies user, project, and DEEPCODE environment pre
191191
assert.equal(resolved.env.WEBHOOK, "system-webhook");
192192
});
193193

194+
test("resolveSettingsSources merges headers with project precedence", () => {
195+
const resolved = resolveSettingsSources(
196+
{
197+
headers: {
198+
"User-Agent": "user-agent",
199+
"X-User": "1",
200+
"X-Ignore": undefined,
201+
},
202+
},
203+
{
204+
headers: {
205+
"User-Agent": "project-agent",
206+
"X-Project": "2",
207+
"X-Number": 123 as never,
208+
},
209+
},
210+
{
211+
model: "default-model",
212+
baseURL: "https://default.example.com",
213+
},
214+
TEST_PROCESS_ENV
215+
);
216+
217+
assert.deepEqual(resolved.headers, {
218+
"User-Agent": "project-agent",
219+
"X-User": "1",
220+
"X-Project": "2",
221+
});
222+
});
223+
194224
test("resolveSettingsSources merges permission settings", () => {
195225
const resolved = resolveSettingsSources(
196226
{

0 commit comments

Comments
 (0)