diff --git a/packages/core/src/common/openai-client.ts b/packages/core/src/common/openai-client.ts index d3b56c08..f80a780a 100644 --- a/packages/core/src/common/openai-client.ts +++ b/packages/core/src/common/openai-client.ts @@ -3,6 +3,7 @@ import * as os from "os"; import * as path from "path"; import OpenAI from "openai"; import { Agent, fetch as undiciFetch } from "undici"; +import type { HeadersInit } from "undici"; import { resolveCurrentSettings } from "../settings"; // Custom undici Agent with a 180-second keepAlive timeout. The default @@ -12,6 +13,19 @@ import { resolveCurrentSettings } from "../settings"; // keep connections reusable for three minutes after the last request. const keepAliveAgent = new Agent({ keepAliveTimeout: 180_000 }); +const DEFAULT_USER_AGENT = `deepcode-cli (Node.js ${process.version})`; + +// Inject a default User-Agent header into outgoing requests. Some CDNs and +// WAFs (e.g. Cloudflare) block requests that arrive without a User-Agent as a +// basic anti-bot measure. (#177) +export function buildClientHeaders(initHeaders?: HeadersInit): Headers { + const headers = new Headers(initHeaders); + if (!headers.has("User-Agent")) { + headers.set("User-Agent", DEFAULT_USER_AGENT); + } + return headers; +} + // Module-level cache for the OpenAI client instance. The client itself is // a stateless fetch wrapper, so it is safe to share across calls as long as // the apiKey + baseURL stay the same. Model, thinking-mode and other @@ -73,7 +87,8 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { apiKey: settings.apiKey, baseURL: settings.baseURL || undefined, // eslint-disable-next-line @typescript-eslint/no-explicit-any - fetch: (url: any, init: any) => undiciFetch(url, { ...init, dispatcher: keepAliveAgent }), + fetch: (url: any, init: any) => + undiciFetch(url, { ...init, headers: buildClientHeaders(init?.headers), dispatcher: keepAliveAgent }), }); cachedOpenAIKey = cacheKey; diff --git a/packages/core/src/tests/openai-client.test.ts b/packages/core/src/tests/openai-client.test.ts new file mode 100644 index 00000000..6bb57d7a --- /dev/null +++ b/packages/core/src/tests/openai-client.test.ts @@ -0,0 +1,20 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { buildClientHeaders } from "../common/openai-client"; + +test("buildClientHeaders injects a default User-Agent when none is set", () => { + const headers = buildClientHeaders(undefined); + assert.match(headers.get("User-Agent") ?? "", /^deepcode-cli \(Node\.js v\d+\.\d+\.\d+\)$/); +}); + +test("buildClientHeaders preserves existing headers and a custom User-Agent", () => { + const headers = buildClientHeaders({ Authorization: "Bearer sk-test", "User-Agent": "custom-agent" }); + assert.equal(headers.get("Authorization"), "Bearer sk-test"); + assert.equal(headers.get("User-Agent"), "custom-agent"); +}); + +test("buildClientHeaders accepts tuple-array headers and keeps its entries", () => { + const headers = buildClientHeaders([["Content-Type", "application/json"]]); + assert.equal(headers.get("Content-Type"), "application/json"); + assert.match(headers.get("User-Agent") ?? "", /^deepcode-cli /); +});