From 59be5163fd7c9c0e644203ea6cf33fc30b4db7ed Mon Sep 17 00:00:00 2001 From: xwj02155382 Date: Fri, 9 Jan 2026 15:56:32 +0800 Subject: [PATCH 1/7] feat: add defaultHeaders support for all content generators - Add defaultHeaders field to ContentGeneratorConfig and ModelGenerationConfig - Implement defaultHeaders merging logic in resolveGenerationConfig - Support defaultHeaders in OpenAI providers (DefaultOpenAICompatibleProvider, DashScopeOpenAICompatibleProvider) - Support defaultHeaders in Gemini and Anthropic content generators - Add defaultHeaders to MODEL_GENERATION_CONFIG_FIELDS - Update resolveQwenOAuthConfig to support modelProvider.generationConfig Configuration hierarchy: - L1: modelProvider.generationConfig.defaultHeaders (high priority) - L2: settings.model.generationConfig.defaultHeaders (low priority) - Merge strategy: high priority headers override low priority headers with same name --- .../anthropicContentGenerator.ts | 7 +++- packages/core/src/core/contentGenerator.ts | 2 ++ .../geminiContentGenerator.ts | 14 +++++++- .../provider/dashscope.ts | 9 ++++- .../provider/default.ts | 9 ++++- packages/core/src/models/constants.ts | 1 + .../core/src/models/modelConfigResolver.ts | 34 ++++++++++++++++--- packages/core/src/models/types.ts | 1 + 8 files changed, 69 insertions(+), 8 deletions(-) diff --git a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts index 228f93853..54818184a 100644 --- a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts +++ b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts @@ -163,7 +163,12 @@ export class AnthropicContentGenerator implements ContentGenerator { headers['anthropic-beta'] = betas.join(','); } - return headers; + // Merge with custom defaultHeaders from config + const customHeaders = this.contentGeneratorConfig.defaultHeaders || {}; + return { + ...headers, + ...customHeaders, + }; } private async buildRequest( diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index fc36fda3c..d229b707f 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -91,6 +91,8 @@ export type ContentGeneratorConfig = { userAgent?: string; // Schema compliance mode for tool definitions schemaCompliance?: 'auto' | 'openapi_30'; + // Custom HTTP headers to be sent with requests + defaultHeaders?: Record; }; // Keep the public ContentGeneratorConfigSources API, but reuse the generic diff --git a/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts b/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts index 0008b8eb5..bb9206c96 100644 --- a/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts +++ b/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts @@ -35,7 +35,19 @@ export class GeminiContentGenerator implements ContentGenerator { }, contentGeneratorConfig?: ContentGeneratorConfig, ) { - this.googleGenAI = new GoogleGenAI(options); + // Merge custom defaultHeaders into httpOptions + const customHeaders = contentGeneratorConfig?.defaultHeaders || {}; + const mergedOptions = { + ...options, + httpOptions: { + headers: { + ...(options.httpOptions?.headers || {}), + ...customHeaders, + }, + }, + }; + + this.googleGenAI = new GoogleGenAI(mergedOptions); this.contentGeneratorConfig = contentGeneratorConfig; } diff --git a/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts b/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts index 5658eee47..6491e7bbf 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts @@ -48,12 +48,19 @@ export class DashScopeOpenAICompatibleProvider const version = this.cliConfig.getCliVersion() || 'unknown'; const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; const { authType } = this.contentGeneratorConfig; - return { + const baseHeaders: Record = { 'User-Agent': userAgent, 'X-DashScope-CacheControl': 'enable', 'X-DashScope-UserAgent': userAgent, 'X-DashScope-AuthType': authType, }; + + // Merge with custom defaultHeaders from config + const customHeaders = this.contentGeneratorConfig.defaultHeaders || {}; + return { + ...baseHeaders, + ...customHeaders, + }; } buildClient(): OpenAI { diff --git a/packages/core/src/core/openaiContentGenerator/provider/default.ts b/packages/core/src/core/openaiContentGenerator/provider/default.ts index 521a6768c..6b493f522 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/default.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/default.ts @@ -25,9 +25,16 @@ export class DefaultOpenAICompatibleProvider buildHeaders(): Record { const version = this.cliConfig.getCliVersion() || 'unknown'; const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; - return { + const baseHeaders: Record = { 'User-Agent': userAgent, }; + + // Merge with custom defaultHeaders from config + const customHeaders = this.contentGeneratorConfig.defaultHeaders || {}; + return { + ...baseHeaders, + ...customHeaders, + }; } buildClient(): OpenAI { diff --git a/packages/core/src/models/constants.ts b/packages/core/src/models/constants.ts index 9dd69620c..2c550a5d6 100644 --- a/packages/core/src/models/constants.ts +++ b/packages/core/src/models/constants.ts @@ -25,6 +25,7 @@ export const MODEL_GENERATION_CONFIG_FIELDS = [ 'disableCacheControl', 'schemaCompliance', 'reasoning', + 'defaultHeaders', ] as const satisfies ReadonlyArray; /** diff --git a/packages/core/src/models/modelConfigResolver.ts b/packages/core/src/models/modelConfigResolver.ts index a6c734f72..20f0fa1e4 100644 --- a/packages/core/src/models/modelConfigResolver.ts +++ b/packages/core/src/models/modelConfigResolver.ts @@ -277,7 +277,7 @@ function resolveQwenOAuthConfig( input: ModelConfigSourcesInput, warnings: string[], ): ModelConfigResolutionResult { - const { cli, settings, proxy } = input; + const { cli, settings, proxy, modelProvider } = input; const sources: ConfigSources = {}; // Qwen OAuth only allows specific models @@ -311,10 +311,10 @@ function resolveQwenOAuthConfig( sources['proxy'] = computedSource('Config.getProxy()'); } - // Resolve generation config from settings + // Resolve generation config from settings and modelProvider const generationConfig = resolveGenerationConfig( settings?.generationConfig, - undefined, + modelProvider?.generationConfig, AuthType.QWEN_OAUTH, resolvedModel, sources, @@ -344,7 +344,33 @@ function resolveGenerationConfig( const result: Partial = {}; for (const field of MODEL_GENERATION_CONFIG_FIELDS) { - // ModelProvider config takes priority + // Special handling for defaultHeaders: merge instead of replace + if (field === 'defaultHeaders') { + const settingsHeaders = settingsConfig?.defaultHeaders; + const providerHeaders = modelProviderConfig?.defaultHeaders; + + if (settingsHeaders || providerHeaders) { + // Merge headers: provider headers override settings headers + result.defaultHeaders = { + ...(settingsHeaders || {}), + ...(providerHeaders || {}), + }; + + // Track source for merged headers + if (providerHeaders && authType) { + sources[field] = modelProvidersSource( + authType, + modelId || '', + `generationConfig.${field}`, + ); + } else if (settingsHeaders) { + sources[field] = settingsSource(`model.generationConfig.${field}`); + } + } + continue; + } + + // ModelProvider config takes priority for other fields if (authType && modelProviderConfig && field in modelProviderConfig) { // eslint-disable-next-line @typescript-eslint/no-explicit-any (result as any)[field] = modelProviderConfig[field]; diff --git a/packages/core/src/models/types.ts b/packages/core/src/models/types.ts index b5ce56efa..d429bf563 100644 --- a/packages/core/src/models/types.ts +++ b/packages/core/src/models/types.ts @@ -31,6 +31,7 @@ export type ModelGenerationConfig = Pick< | 'disableCacheControl' | 'schemaCompliance' | 'reasoning' + | 'defaultHeaders' >; /** From 0bd17a2406b9fbc888689de433f6b61396112261 Mon Sep 17 00:00:00 2001 From: xwj02155382 Date: Fri, 9 Jan 2026 16:08:59 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=BB=8E=20model?= =?UTF-8?q?Providers=20=E9=85=8D=E7=BD=AE=E4=B8=AD=E8=AF=BB=E5=8F=96=20def?= =?UTF-8?q?aultHeaders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 ModelConfigSourcesInput 接口,将 modelProvider 类型从 ResolvedModelConfig 改为 ModelProviderConfig - 在 resolveCliGenerationConfig 中添加从 settings.modelProviders 查找 modelProvider 的逻辑 - 使用类型别名避免与 subagents/types.ts 中的 ModelConfig 冲突 - 修复测试文件中的类型错误 - 现在可以通过 modelProviders 配置为特定模型设置 defaultHeaders --- defaultHeaders功能实现文档.md | 435 ++++++++++++++++++ packages/cli/src/utils/modelConfigUtils.ts | 17 + .../src/models/modelConfigResolver.test.ts | 4 - .../core/src/models/modelConfigResolver.ts | 6 +- test-defaultHeaders.cjs | 116 +++++ verify-defaultHeaders.cjs | 114 +++++ 6 files changed, 685 insertions(+), 7 deletions(-) create mode 100644 defaultHeaders功能实现文档.md create mode 100644 test-defaultHeaders.cjs create mode 100644 verify-defaultHeaders.cjs diff --git a/defaultHeaders功能实现文档.md b/defaultHeaders功能实现文档.md new file mode 100644 index 000000000..cc8b5b57c --- /dev/null +++ b/defaultHeaders功能实现文档.md @@ -0,0 +1,435 @@ +# defaultHeaders 功能实现文档 + +## 概述 + +本次修改为 Qwen Code 项目添加了 `model.generationConfig.defaultHeaders` 配置属性,允许用户为 API 请求自定义 HTTP headers。该功能支持 OpenAI、Gemini 和 Anthropic 三种 content generators,并在 ModelProviders 级别提供支持。 + +## 修改文件清单 + +共修改了 8 个文件: + +### 1. 类型定义文件(3个) + +- `packages/core/src/core/contentGenerator.ts` +- `packages/core/src/models/types.ts` +- `packages/core/src/models/constants.ts` + +### 2. 配置解析器(1个) + +- `packages/core/src/models/modelConfigResolver.ts` + +### 3. Content Generators 实现(4个) + +- `packages/core/src/core/openaiContentGenerator/provider/default.ts` +- `packages/core/src/core/openaiContentGenerator/provider/dashscope.ts` +- `packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts` +- `packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts` + +--- + +## 详细修改说明 + +### 1. packages/core/src/core/contentGenerator.ts + +**修改位置:** 第 93 行附近,`ContentGeneratorConfig` 类型定义 + +**修改内容:** + +```typescript +export type ContentGeneratorConfig = { + // ... 其他字段 + schemaCompliance?: 'auto' | 'openapi_30'; + // 新增字段 + defaultHeaders?: Record; +}; +``` + +**修改意图:** + +- 在核心配置类型 `ContentGeneratorConfig` 中添加 `defaultHeaders` 字段 +- 类型为 `Record`,表示键值对形式的 HTTP headers +- 设置为可选字段(`?`),不影响现有代码的兼容性 +- 这是整个功能的基础类型定义,所有 content generators 都会使用这个配置 + +--- + +### 2. packages/core/src/models/types.ts + +**修改位置:** 第 26-34 行,`ModelGenerationConfig` 类型定义 + +**修改内容:** + +```typescript +export type ModelGenerationConfig = Pick< + ContentGeneratorConfig, + | 'samplingParams' + | 'timeout' + | 'maxRetries' + | 'disableCacheControl' + | 'schemaCompliance' + | 'reasoning' + | 'defaultHeaders' // 新增 +>; +``` + +**修改意图:** + +- 将 `defaultHeaders` 添加到 `ModelGenerationConfig` 类型中 +- `ModelGenerationConfig` 是模型级别的配置类型,用于 ModelProviders 配置 +- 这样用户就可以在 `settings.json` 的 `modelProviders` 配置中使用 `defaultHeaders` +- 确保配置可以从 ModelProviders 层级传递到 ContentGeneratorConfig + +--- + +### 3. packages/core/src/models/constants.ts + +**修改位置:** 第 16-23 行,`MODEL_GENERATION_CONFIG_FIELDS` 常量数组 + +**修改内容:** + +```typescript +export const MODEL_GENERATION_CONFIG_FIELDS = [ + 'samplingParams', + 'timeout', + 'maxRetries', + 'disableCacheControl', + 'schemaCompliance', + 'reasoning', + 'defaultHeaders', // 新增 +] as const satisfies ReadonlyArray; +``` + +**修改意图:** + +- 将 `defaultHeaders` 添加到模型生成配置字段列表中 +- 这个常量数组用于配置解析器遍历和处理所有生成配置字段 +- 添加后,配置解析器会自动处理 `defaultHeaders` 的层级解析 +- 确保类型安全,使用 TypeScript 的 `satisfies` 关键字验证字段名正确 + +--- + +### 4. packages/core/src/models/modelConfigResolver.ts + +**修改位置:** 第 338-370 行,`resolveGenerationConfig` 函数 + +**修改内容:** + +```typescript +function resolveGenerationConfig( + settingsConfig: Partial | undefined, + modelProviderConfig: Partial | undefined, + authType: AuthType | undefined, + modelId: string | undefined, + sources: ConfigSources, +): Partial { + const result: Partial = {}; + + for (const field of MODEL_GENERATION_CONFIG_FIELDS) { + // 新增:defaultHeaders 的特殊处理 + if (field === 'defaultHeaders') { + const settingsHeaders = settingsConfig?.defaultHeaders; + const providerHeaders = modelProviderConfig?.defaultHeaders; + + if (settingsHeaders || providerHeaders) { + // 合并 headers:provider headers 覆盖 settings headers + result.defaultHeaders = { + ...(settingsHeaders || {}), + ...(providerHeaders || {}), + }; + + // 跟踪配置来源 + if (providerHeaders && authType) { + sources[field] = modelProvidersSource( + authType, + modelId || '', + `generationConfig.${field}`, + ); + } else if (settingsHeaders) { + sources[field] = settingsSource(`model.generationConfig.${field}`); + } + } + continue; + } + + // 其他字段的处理逻辑保持不变 + // ... + } + + return result; +} +``` + +**修改意图:** + +- 实现 `defaultHeaders` 的多层级配置解析和合并逻辑 +- **合并策略**: + - 从 `settings.model.generationConfig.defaultHeaders` 读取基础 headers + - 从 `modelProviders[authType][].generationConfig.defaultHeaders` 读取覆盖 headers + - 使用对象展开运算符合并,高优先级(modelProvider)的同名 header 会覆盖低优先级(settings) + - 不同名的 headers 会被保留和合并 +- **来源跟踪**:记录最终生效的配置来源,便于调试和 UI 展示 +- **特殊处理原因**:与其他字段不同,`defaultHeaders` 需要合并而不是简单替换 + +--- + +### 5. packages/core/src/core/openaiContentGenerator/provider/default.ts + +**修改位置:** 第 25-32 行,`buildHeaders` 方法 + +**修改内容:** + +```typescript +buildHeaders(): Record { + const version = this.cliConfig.getCliVersion() || 'unknown'; + const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; + const baseHeaders: Record = { + 'User-Agent': userAgent, + }; + + // 新增:合并自定义 defaultHeaders + const customHeaders = this.contentGeneratorConfig.defaultHeaders || {}; + return { + ...baseHeaders, + ...customHeaders, + }; +} +``` + +**修改意图:** + +- 在 DefaultOpenAICompatibleProvider 中实现 `defaultHeaders` 支持 +- 将用户配置的自定义 headers 与系统默认 headers(如 User-Agent)合并 +- 自定义 headers 会覆盖同名的默认 headers(如果用户想自定义 User-Agent) +- 这个修改会自动影响所有继承自 DefaultOpenAICompatibleProvider 的子类: + - ModelScopeOpenAICompatibleProvider + - DeepSeekOpenAICompatibleProvider + - OpenRouterOpenAICompatibleProvider(虽然它 override 了 buildHeaders,但会调用 super.buildHeaders()) + +--- + +### 6. packages/core/src/core/openaiContentGenerator/provider/dashscope.ts + +**修改位置:** 第 45-58 行,`buildHeaders` 方法 + +**修改内容:** + +```typescript +buildHeaders(): Record { + const version = this.cliConfig.getCliVersion() || 'unknown'; + const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; + const { authType } = this.contentGeneratorConfig; + const baseHeaders: Record = { + 'User-Agent': userAgent, + 'X-DashScope-CacheControl': 'enable', + 'X-DashScope-UserAgent': userAgent, + 'X-DashScope-AuthType': authType, + }; + + // 新增:合并自定义 defaultHeaders + const customHeaders = this.contentGeneratorConfig.defaultHeaders || {}; + return { + ...baseHeaders, + ...customHeaders, + }; +} +``` + +**修改意图:** + +- DashScopeOpenAICompatibleProvider 有自己独立的 `buildHeaders` 实现 +- 需要单独添加 `defaultHeaders` 支持 +- 保持与 DefaultOpenAICompatibleProvider 相同的合并逻辑 +- DashScope 特有的 headers(如 X-DashScope-\*)会与自定义 headers 合并 +- 确保 DashScope(阿里云百炼)用户也能使用自定义 headers 功能 + +--- + +### 7. packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts + +**修改位置:** 第 30-48 行,`constructor` 方法 + +**修改内容:** + +```typescript +constructor( + options: { + apiKey?: string; + vertexai?: boolean; + httpOptions?: { headers: Record }; + }, + contentGeneratorConfig?: ContentGeneratorConfig, +) { + // 新增:合并自定义 defaultHeaders 到 httpOptions + const customHeaders = contentGeneratorConfig?.defaultHeaders || {}; + const mergedOptions = { + ...options, + httpOptions: { + headers: { + ...(options.httpOptions?.headers || {}), + ...customHeaders, + }, + }, + }; + + this.googleGenAI = new GoogleGenAI(mergedOptions); + this.contentGeneratorConfig = contentGeneratorConfig; +} +``` + +**修改意图:** + +- Gemini 使用 Google 的 `@google/genai` SDK +- 该 SDK 通过 `httpOptions.headers` 参数接收自定义 headers +- 在构造函数中将 `defaultHeaders` 合并到 `httpOptions.headers` 中 +- 确保自定义 headers 在创建 GoogleGenAI 实例时就被设置 +- 合并逻辑:原有的 httpOptions.headers + 自定义 defaultHeaders + +--- + +### 8. packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts + +**修改位置:** 第 140-158 行,`buildHeaders` 方法 + +**修改内容:** + +```typescript +private buildHeaders(): Record { + const version = this.cliConfig.getCliVersion() || 'unknown'; + const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; + + const betas: string[] = []; + const reasoning = this.contentGeneratorConfig.reasoning; + + // Interleaved thinking 配置 + if (reasoning !== false) { + betas.push('interleaved-thinking-2025-05-14'); + } + + // Effort (beta) 配置 + if (reasoning !== false && reasoning?.effort !== undefined) { + betas.push('effort-2025-11-24'); + } + + const headers: Record = { + 'User-Agent': userAgent, + }; + + if (betas.length) { + headers['anthropic-beta'] = betas.join(','); + } + + // 新增:合并自定义 defaultHeaders + const customHeaders = this.contentGeneratorConfig.defaultHeaders || {}; + return { + ...headers, + ...customHeaders, + }; +} +``` + +**修改意图:** + +- 在 AnthropicContentGenerator 的 `buildHeaders` 方法中添加 `defaultHeaders` 支持 +- Anthropic SDK 在构造函数中通过 `defaultHeaders` 参数接收自定义 headers +- 将用户配置的 headers 与系统 headers(User-Agent、anthropic-beta)合并 +- 保持与 OpenAI providers 相同的合并逻辑 +- 确保 Claude 模型用户也能使用自定义 headers 功能 + +--- + +## 配置层级和优先级 + +### 配置层级 + +1. **L1(最高优先级)**: `modelProviders[authType][].generationConfig.defaultHeaders` +2. **L2(次优先级)**: `settings.model.generationConfig.defaultHeaders` + +### 合并规则 + +- 两个层级的 headers 会被合并 +- 相同名称的 header,高优先级(L1)会覆盖低优先级(L2) +- 不同名称的 headers 会被保留 + +### 示例 + +**Settings 配置:** + +```json +{ + "model": { + "generationConfig": { + "defaultHeaders": { + "X-Custom-Header": "from-settings", + "X-Another-Header": "value1" + } + } + } +} +``` + +**ModelProviders 配置:** + +```json +{ + "modelProviders": { + "openai": [ + { + "id": "qwen3-coder-plus", + "generationConfig": { + "defaultHeaders": { + "X-Custom-Header": "from-provider", + "X-Provider-Header": "value2" + } + } + } + ] + } +} +``` + +**最终生效的 headers:** + +```json +{ + "X-Custom-Header": "from-provider", // 被 provider 覆盖 + "X-Another-Header": "value1", // 保留自 settings + "X-Provider-Header": "value2" // 来自 provider +} +``` + +--- + +## 使用场景 + +1. **添加认证 headers**:为需要额外认证的 API 网关添加自定义认证头 +2. **请求追踪**:添加 `X-Request-ID`、`X-Trace-ID` 等追踪 headers +3. **API 版本控制**:通过 `X-API-Version` 指定 API 版本 +4. **自定义元数据**:添加组织、项目等元数据信息 +5. **调试和监控**:添加调试标识或监控标签 + +--- + +## 技术亮点 + +1. **类型安全**:完整的 TypeScript 类型定义,编译时检查 +2. **配置来源追踪**:记录每个配置项的来源,便于调试 +3. **向后兼容**:所有修改都是可选的,不影响现有代码 +4. **统一实现**:三个主要 content generators 都采用相同的合并逻辑 +5. **继承友好**:OpenAI providers 的继承体系自动获得支持 +6. **灵活合并**:支持多层级配置合并,满足不同场景需求 + +--- + +## 测试验证 + +- ✅ TypeScript 编译通过,无类型错误 +- ✅ 所有 OpenAI providers(Default、DashScope、ModelScope、DeepSeek、OpenRouter)都支持 +- ✅ Gemini 和 Anthropic generators 正确实现 +- ✅ 配置解析器正确处理多层级合并 +- ✅ 向后兼容,不影响未配置 defaultHeaders 的用户 + +--- + +## 总结 + +本次修改通过 8 个文件的协同更新,为 Qwen Code 项目添加了完整的自定义 HTTP headers 支持。修改遵循了项目的架构设计,保持了代码的一致性和可维护性,同时确保了向后兼容性和类型安全。用户现在可以通过简单的配置为 API 请求添加自定义 headers,满足各种企业级和高级使用场景的需求。 diff --git a/packages/cli/src/utils/modelConfigUtils.ts b/packages/cli/src/utils/modelConfigUtils.ts index 9a0ad8978..4a025ed1f 100644 --- a/packages/cli/src/utils/modelConfigUtils.ts +++ b/packages/cli/src/utils/modelConfigUtils.ts @@ -10,6 +10,7 @@ import { type ContentGeneratorConfigSources, resolveModelConfig, type ModelConfigSourcesInput, + type ProviderModelConfig, } from '@qwen-code/qwen-code-core'; import type { Settings } from '../config/settings.js'; @@ -81,6 +82,21 @@ export function resolveCliGenerationConfig( const authType = selectedAuthType; + // Find modelProvider from settings.modelProviders based on authType and model + let modelProvider: ProviderModelConfig | undefined; + if (authType && settings.modelProviders) { + const providers = settings.modelProviders[authType]; + if (providers && Array.isArray(providers)) { + // Try to find by requested model (from CLI or settings) + const requestedModel = argv.model || settings.model?.name; + if (requestedModel) { + modelProvider = providers.find((p) => p.id === requestedModel) as + | ProviderModelConfig + | undefined; + } + } + } + const configSources: ModelConfigSourcesInput = { authType, cli: { @@ -96,6 +112,7 @@ export function resolveCliGenerationConfig( | Partial | undefined, }, + modelProvider, env, }; diff --git a/packages/core/src/models/modelConfigResolver.test.ts b/packages/core/src/models/modelConfigResolver.test.ts index b7aa8c29b..a69ca678e 100644 --- a/packages/core/src/models/modelConfigResolver.test.ts +++ b/packages/core/src/models/modelConfigResolver.test.ts @@ -112,11 +112,9 @@ describe('modelConfigResolver', () => { modelProvider: { id: 'provider-model', name: 'Provider Model', - authType: AuthType.USE_OPENAI, envKey: 'MY_CUSTOM_KEY', baseUrl: 'https://provider.example.com', generationConfig: {}, - capabilities: {}, }, }); @@ -249,13 +247,11 @@ describe('modelConfigResolver', () => { modelProvider: { id: 'model', name: 'Model', - authType: AuthType.USE_OPENAI, envKey: 'MY_KEY', baseUrl: 'https://api.example.com', generationConfig: { timeout: 60000, }, - capabilities: {}, }, }); diff --git a/packages/core/src/models/modelConfigResolver.ts b/packages/core/src/models/modelConfigResolver.ts index 20f0fa1e4..33747e43a 100644 --- a/packages/core/src/models/modelConfigResolver.ts +++ b/packages/core/src/models/modelConfigResolver.ts @@ -41,7 +41,7 @@ import { QWEN_OAUTH_ALLOWED_MODELS, MODEL_GENERATION_CONFIG_FIELDS, } from './constants.js'; -import type { ResolvedModelConfig } from './types.js'; +import type { ModelConfig as ModelProviderConfig } from './types.js'; export { validateModelConfig, type ModelConfigValidationResult, @@ -86,8 +86,8 @@ export interface ModelConfigSourcesInput { /** Environment variables (injected for testability) */ env: Record; - /** Resolved model from ModelProviders (explicit selection, highest priority) */ - modelProvider?: ResolvedModelConfig; + /** Model from ModelProviders (explicit selection, highest priority) */ + modelProvider?: ModelProviderConfig; /** Proxy URL (computed from Config) */ proxy?: string; diff --git a/test-defaultHeaders.cjs b/test-defaultHeaders.cjs new file mode 100644 index 000000000..94e3cf5d4 --- /dev/null +++ b/test-defaultHeaders.cjs @@ -0,0 +1,116 @@ +/** + * defaultHeaders 功能测试脚本 + * + * 这个脚本会模拟配置并输出最终的 headers + */ + +// 模拟配置解析逻辑 +function resolveDefaultHeaders(settingsHeaders, providerHeaders) { + console.log('📋 测试 defaultHeaders 合并逻辑\n'); + + console.log('输入:'); + console.log(' Settings headers:', JSON.stringify(settingsHeaders, null, 2)); + console.log(' Provider headers:', JSON.stringify(providerHeaders, null, 2)); + console.log(''); + + const result = { + ...(settingsHeaders || {}), + ...(providerHeaders || {}), + }; + + console.log('输出(合并后):'); + console.log(' Final headers:', JSON.stringify(result, null, 2)); + console.log(''); + + return result; +} + +// 测试场景 1:只有 settings 配置 +console.log('━'.repeat(60)); +console.log('场景 1: 只配置 settings.model.generationConfig.defaultHeaders'); +console.log('━'.repeat(60)); +resolveDefaultHeaders( + { + 'X-Custom-Header': 'from-settings', + 'X-Request-ID': 'req-123', + }, + undefined +); + +// 测试场景 2:只有 provider 配置 +console.log('━'.repeat(60)); +console.log('场景 2: 只配置 modelProviders[].generationConfig.defaultHeaders'); +console.log('━'.repeat(60)); +resolveDefaultHeaders( + undefined, + { + 'X-Provider-Header': 'from-provider', + 'X-API-Version': 'v2', + } +); + +// 测试场景 3:两者都配置,无冲突 +console.log('━'.repeat(60)); +console.log('场景 3: 两者都配置,header 名称不冲突'); +console.log('━'.repeat(60)); +resolveDefaultHeaders( + { + 'X-Settings-Header': 'from-settings', + 'X-Request-ID': 'req-123', + }, + { + 'X-Provider-Header': 'from-provider', + 'X-API-Version': 'v2', + } +); + +// 测试场景 4:两者都配置,有冲突(provider 优先) +console.log('━'.repeat(60)); +console.log('场景 4: 两者都配置,有同名 header(provider 应覆盖 settings)'); +console.log('━'.repeat(60)); +resolveDefaultHeaders( + { + 'X-Custom-Header': 'from-settings', + 'X-Request-ID': 'req-123', + 'X-Common-Header': 'settings-value', + }, + { + 'X-Custom-Header': 'from-provider', + 'X-API-Version': 'v2', + 'X-Common-Header': 'provider-value', // 这个应该覆盖 settings 的值 + } +); + +// 模拟最终与基础 headers 合并 +console.log('━'.repeat(60)); +console.log('场景 5: 与系统基础 headers 合并(模拟实际使用)'); +console.log('━'.repeat(60)); + +const systemHeaders = { + 'User-Agent': 'QwenCode/0.7.0 (darwin; arm64)', +}; + +const customHeaders = { + 'X-Custom-Header': 'custom-value', + 'X-Request-ID': 'req-456', +}; + +console.log('系统基础 headers:', JSON.stringify(systemHeaders, null, 2)); +console.log('用户自定义 headers:', JSON.stringify(customHeaders, null, 2)); +console.log(''); + +const finalHeaders = { + ...systemHeaders, + ...customHeaders, +}; + +console.log('最终发送的 headers:', JSON.stringify(finalHeaders, null, 2)); +console.log(''); + +console.log('━'.repeat(60)); +console.log('✅ 测试完成!'); +console.log(''); +console.log('💡 提示:'); +console.log(' 1. 在实际代码中,在 buildHeaders() 方法打断点可以看到这些值'); +console.log(' 2. 使用网络抓包工具可以看到实际发送的 HTTP 请求头'); +console.log(' 3. 高优先级(provider)的 headers 会覆盖低优先级(settings)的同名 headers'); diff --git a/verify-defaultHeaders.cjs b/verify-defaultHeaders.cjs new file mode 100644 index 000000000..54b29a817 --- /dev/null +++ b/verify-defaultHeaders.cjs @@ -0,0 +1,114 @@ +/** + * defaultHeaders 功能验证脚本 + * + * 使用方法: + * node verify-defaultHeaders.js + */ + +const fs = require('fs'); +const path = require('path'); + +console.log('🔍 开始验证 defaultHeaders 功能实现...\n'); + +// 验证项目列表 +const verifications = [ + { + name: '1. ContentGeneratorConfig 类型定义', + file: 'packages/core/src/core/contentGenerator.ts', + check: (content) => content.includes('defaultHeaders?: Record'), + description: '检查 ContentGeneratorConfig 是否包含 defaultHeaders 字段' + }, + { + name: '2. ModelGenerationConfig 类型定义', + file: 'packages/core/src/models/types.ts', + check: (content) => content.includes("'defaultHeaders'"), + description: '检查 ModelGenerationConfig 是否包含 defaultHeaders' + }, + { + name: '3. MODEL_GENERATION_CONFIG_FIELDS 常量', + file: 'packages/core/src/models/constants.ts', + check: (content) => content.includes("'defaultHeaders'"), + description: '检查配置字段列表是否包含 defaultHeaders' + }, + { + name: '4. modelConfigResolver 合并逻辑', + file: 'packages/core/src/models/modelConfigResolver.ts', + check: (content) => content.includes("field === 'defaultHeaders'") && content.includes('settingsHeaders'), + description: '检查配置解析器是否实现 defaultHeaders 合并逻辑' + }, + { + name: '5. DefaultOpenAICompatibleProvider', + file: 'packages/core/src/core/openaiContentGenerator/provider/default.ts', + check: (content) => content.includes('this.contentGeneratorConfig.defaultHeaders'), + description: '检查 OpenAI 默认 provider 是否支持 defaultHeaders' + }, + { + name: '6. DashScopeOpenAICompatibleProvider', + file: 'packages/core/src/core/openaiContentGenerator/provider/dashscope.ts', + check: (content) => content.includes('this.contentGeneratorConfig.defaultHeaders'), + description: '检查 DashScope provider 是否支持 defaultHeaders' + }, + { + name: '7. GeminiContentGenerator', + file: 'packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts', + check: (content) => content.includes('contentGeneratorConfig?.defaultHeaders'), + description: '检查 Gemini generator 是否支持 defaultHeaders' + }, + { + name: '8. AnthropicContentGenerator', + file: 'packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts', + check: (content) => content.includes('this.contentGeneratorConfig.defaultHeaders'), + description: '检查 Anthropic generator 是否支持 defaultHeaders' + } +]; + +let passedCount = 0; +let failedCount = 0; + +// 执行验证 +verifications.forEach((verification, index) => { + const filePath = path.join(__dirname, verification.file); + + try { + if (!fs.existsSync(filePath)) { + console.log(`❌ ${verification.name}`); + console.log(` 文件不存在: ${verification.file}\n`); + failedCount++; + return; + } + + const content = fs.readFileSync(filePath, 'utf-8'); + const passed = verification.check(content); + + if (passed) { + console.log(`✅ ${verification.name}`); + console.log(` ${verification.description}`); + console.log(` 文件: ${verification.file}\n`); + passedCount++; + } else { + console.log(`❌ ${verification.name}`); + console.log(` ${verification.description}`); + console.log(` 文件: ${verification.file}`); + console.log(` 状态: 未找到预期的代码\n`); + failedCount++; + } + } catch (error) { + console.log(`❌ ${verification.name}`); + console.log(` 错误: ${error.message}\n`); + failedCount++; + } +}); + +// 输出总结 +console.log('━'.repeat(60)); +console.log(`\n📊 验证结果总结:`); +console.log(` ✅ 通过: ${passedCount}/${verifications.length}`); +console.log(` ❌ 失败: ${failedCount}/${verifications.length}`); + +if (failedCount === 0) { + console.log(`\n🎉 所有验证项都通过!defaultHeaders 功能已正确实现。\n`); + process.exit(0); +} else { + console.log(`\n⚠️ 有 ${failedCount} 项验证失败,请检查相关文件。\n`); + process.exit(1); +} From 1b7418f91f5e29c83e9f2b48e79b7ca7b8783c9a Mon Sep 17 00:00:00 2001 From: xwj02155382 Date: Fri, 9 Jan 2026 17:31:01 +0800 Subject: [PATCH 3/7] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=20defaultHeaders?= =?UTF-8?q?=20=E5=8A=9F=E8=83=BD=E5=AE=8C=E6=95=B4=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 整合当前分支相对于 main 的所有改动(10 个文件) - 包含两个 commit 的完整改动详情 - 删除测试文件 test-defaultHeaders.cjs 和 verify-defaultHeaders.cjs - 删除旧的不完整文档 - 新增完整的功能文档,包含代码改动说明、配置示例、使用指南等 --- defaultHeaders功能实现文档.md | 435 ---------------------------------- test-defaultHeaders.cjs | 116 --------- verify-defaultHeaders.cjs | 114 --------- 3 files changed, 665 deletions(-) delete mode 100644 defaultHeaders功能实现文档.md delete mode 100644 test-defaultHeaders.cjs delete mode 100644 verify-defaultHeaders.cjs diff --git a/defaultHeaders功能实现文档.md b/defaultHeaders功能实现文档.md deleted file mode 100644 index cc8b5b57c..000000000 --- a/defaultHeaders功能实现文档.md +++ /dev/null @@ -1,435 +0,0 @@ -# defaultHeaders 功能实现文档 - -## 概述 - -本次修改为 Qwen Code 项目添加了 `model.generationConfig.defaultHeaders` 配置属性,允许用户为 API 请求自定义 HTTP headers。该功能支持 OpenAI、Gemini 和 Anthropic 三种 content generators,并在 ModelProviders 级别提供支持。 - -## 修改文件清单 - -共修改了 8 个文件: - -### 1. 类型定义文件(3个) - -- `packages/core/src/core/contentGenerator.ts` -- `packages/core/src/models/types.ts` -- `packages/core/src/models/constants.ts` - -### 2. 配置解析器(1个) - -- `packages/core/src/models/modelConfigResolver.ts` - -### 3. Content Generators 实现(4个) - -- `packages/core/src/core/openaiContentGenerator/provider/default.ts` -- `packages/core/src/core/openaiContentGenerator/provider/dashscope.ts` -- `packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts` -- `packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts` - ---- - -## 详细修改说明 - -### 1. packages/core/src/core/contentGenerator.ts - -**修改位置:** 第 93 行附近,`ContentGeneratorConfig` 类型定义 - -**修改内容:** - -```typescript -export type ContentGeneratorConfig = { - // ... 其他字段 - schemaCompliance?: 'auto' | 'openapi_30'; - // 新增字段 - defaultHeaders?: Record; -}; -``` - -**修改意图:** - -- 在核心配置类型 `ContentGeneratorConfig` 中添加 `defaultHeaders` 字段 -- 类型为 `Record`,表示键值对形式的 HTTP headers -- 设置为可选字段(`?`),不影响现有代码的兼容性 -- 这是整个功能的基础类型定义,所有 content generators 都会使用这个配置 - ---- - -### 2. packages/core/src/models/types.ts - -**修改位置:** 第 26-34 行,`ModelGenerationConfig` 类型定义 - -**修改内容:** - -```typescript -export type ModelGenerationConfig = Pick< - ContentGeneratorConfig, - | 'samplingParams' - | 'timeout' - | 'maxRetries' - | 'disableCacheControl' - | 'schemaCompliance' - | 'reasoning' - | 'defaultHeaders' // 新增 ->; -``` - -**修改意图:** - -- 将 `defaultHeaders` 添加到 `ModelGenerationConfig` 类型中 -- `ModelGenerationConfig` 是模型级别的配置类型,用于 ModelProviders 配置 -- 这样用户就可以在 `settings.json` 的 `modelProviders` 配置中使用 `defaultHeaders` -- 确保配置可以从 ModelProviders 层级传递到 ContentGeneratorConfig - ---- - -### 3. packages/core/src/models/constants.ts - -**修改位置:** 第 16-23 行,`MODEL_GENERATION_CONFIG_FIELDS` 常量数组 - -**修改内容:** - -```typescript -export const MODEL_GENERATION_CONFIG_FIELDS = [ - 'samplingParams', - 'timeout', - 'maxRetries', - 'disableCacheControl', - 'schemaCompliance', - 'reasoning', - 'defaultHeaders', // 新增 -] as const satisfies ReadonlyArray; -``` - -**修改意图:** - -- 将 `defaultHeaders` 添加到模型生成配置字段列表中 -- 这个常量数组用于配置解析器遍历和处理所有生成配置字段 -- 添加后,配置解析器会自动处理 `defaultHeaders` 的层级解析 -- 确保类型安全,使用 TypeScript 的 `satisfies` 关键字验证字段名正确 - ---- - -### 4. packages/core/src/models/modelConfigResolver.ts - -**修改位置:** 第 338-370 行,`resolveGenerationConfig` 函数 - -**修改内容:** - -```typescript -function resolveGenerationConfig( - settingsConfig: Partial | undefined, - modelProviderConfig: Partial | undefined, - authType: AuthType | undefined, - modelId: string | undefined, - sources: ConfigSources, -): Partial { - const result: Partial = {}; - - for (const field of MODEL_GENERATION_CONFIG_FIELDS) { - // 新增:defaultHeaders 的特殊处理 - if (field === 'defaultHeaders') { - const settingsHeaders = settingsConfig?.defaultHeaders; - const providerHeaders = modelProviderConfig?.defaultHeaders; - - if (settingsHeaders || providerHeaders) { - // 合并 headers:provider headers 覆盖 settings headers - result.defaultHeaders = { - ...(settingsHeaders || {}), - ...(providerHeaders || {}), - }; - - // 跟踪配置来源 - if (providerHeaders && authType) { - sources[field] = modelProvidersSource( - authType, - modelId || '', - `generationConfig.${field}`, - ); - } else if (settingsHeaders) { - sources[field] = settingsSource(`model.generationConfig.${field}`); - } - } - continue; - } - - // 其他字段的处理逻辑保持不变 - // ... - } - - return result; -} -``` - -**修改意图:** - -- 实现 `defaultHeaders` 的多层级配置解析和合并逻辑 -- **合并策略**: - - 从 `settings.model.generationConfig.defaultHeaders` 读取基础 headers - - 从 `modelProviders[authType][].generationConfig.defaultHeaders` 读取覆盖 headers - - 使用对象展开运算符合并,高优先级(modelProvider)的同名 header 会覆盖低优先级(settings) - - 不同名的 headers 会被保留和合并 -- **来源跟踪**:记录最终生效的配置来源,便于调试和 UI 展示 -- **特殊处理原因**:与其他字段不同,`defaultHeaders` 需要合并而不是简单替换 - ---- - -### 5. packages/core/src/core/openaiContentGenerator/provider/default.ts - -**修改位置:** 第 25-32 行,`buildHeaders` 方法 - -**修改内容:** - -```typescript -buildHeaders(): Record { - const version = this.cliConfig.getCliVersion() || 'unknown'; - const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; - const baseHeaders: Record = { - 'User-Agent': userAgent, - }; - - // 新增:合并自定义 defaultHeaders - const customHeaders = this.contentGeneratorConfig.defaultHeaders || {}; - return { - ...baseHeaders, - ...customHeaders, - }; -} -``` - -**修改意图:** - -- 在 DefaultOpenAICompatibleProvider 中实现 `defaultHeaders` 支持 -- 将用户配置的自定义 headers 与系统默认 headers(如 User-Agent)合并 -- 自定义 headers 会覆盖同名的默认 headers(如果用户想自定义 User-Agent) -- 这个修改会自动影响所有继承自 DefaultOpenAICompatibleProvider 的子类: - - ModelScopeOpenAICompatibleProvider - - DeepSeekOpenAICompatibleProvider - - OpenRouterOpenAICompatibleProvider(虽然它 override 了 buildHeaders,但会调用 super.buildHeaders()) - ---- - -### 6. packages/core/src/core/openaiContentGenerator/provider/dashscope.ts - -**修改位置:** 第 45-58 行,`buildHeaders` 方法 - -**修改内容:** - -```typescript -buildHeaders(): Record { - const version = this.cliConfig.getCliVersion() || 'unknown'; - const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; - const { authType } = this.contentGeneratorConfig; - const baseHeaders: Record = { - 'User-Agent': userAgent, - 'X-DashScope-CacheControl': 'enable', - 'X-DashScope-UserAgent': userAgent, - 'X-DashScope-AuthType': authType, - }; - - // 新增:合并自定义 defaultHeaders - const customHeaders = this.contentGeneratorConfig.defaultHeaders || {}; - return { - ...baseHeaders, - ...customHeaders, - }; -} -``` - -**修改意图:** - -- DashScopeOpenAICompatibleProvider 有自己独立的 `buildHeaders` 实现 -- 需要单独添加 `defaultHeaders` 支持 -- 保持与 DefaultOpenAICompatibleProvider 相同的合并逻辑 -- DashScope 特有的 headers(如 X-DashScope-\*)会与自定义 headers 合并 -- 确保 DashScope(阿里云百炼)用户也能使用自定义 headers 功能 - ---- - -### 7. packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts - -**修改位置:** 第 30-48 行,`constructor` 方法 - -**修改内容:** - -```typescript -constructor( - options: { - apiKey?: string; - vertexai?: boolean; - httpOptions?: { headers: Record }; - }, - contentGeneratorConfig?: ContentGeneratorConfig, -) { - // 新增:合并自定义 defaultHeaders 到 httpOptions - const customHeaders = contentGeneratorConfig?.defaultHeaders || {}; - const mergedOptions = { - ...options, - httpOptions: { - headers: { - ...(options.httpOptions?.headers || {}), - ...customHeaders, - }, - }, - }; - - this.googleGenAI = new GoogleGenAI(mergedOptions); - this.contentGeneratorConfig = contentGeneratorConfig; -} -``` - -**修改意图:** - -- Gemini 使用 Google 的 `@google/genai` SDK -- 该 SDK 通过 `httpOptions.headers` 参数接收自定义 headers -- 在构造函数中将 `defaultHeaders` 合并到 `httpOptions.headers` 中 -- 确保自定义 headers 在创建 GoogleGenAI 实例时就被设置 -- 合并逻辑:原有的 httpOptions.headers + 自定义 defaultHeaders - ---- - -### 8. packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts - -**修改位置:** 第 140-158 行,`buildHeaders` 方法 - -**修改内容:** - -```typescript -private buildHeaders(): Record { - const version = this.cliConfig.getCliVersion() || 'unknown'; - const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; - - const betas: string[] = []; - const reasoning = this.contentGeneratorConfig.reasoning; - - // Interleaved thinking 配置 - if (reasoning !== false) { - betas.push('interleaved-thinking-2025-05-14'); - } - - // Effort (beta) 配置 - if (reasoning !== false && reasoning?.effort !== undefined) { - betas.push('effort-2025-11-24'); - } - - const headers: Record = { - 'User-Agent': userAgent, - }; - - if (betas.length) { - headers['anthropic-beta'] = betas.join(','); - } - - // 新增:合并自定义 defaultHeaders - const customHeaders = this.contentGeneratorConfig.defaultHeaders || {}; - return { - ...headers, - ...customHeaders, - }; -} -``` - -**修改意图:** - -- 在 AnthropicContentGenerator 的 `buildHeaders` 方法中添加 `defaultHeaders` 支持 -- Anthropic SDK 在构造函数中通过 `defaultHeaders` 参数接收自定义 headers -- 将用户配置的 headers 与系统 headers(User-Agent、anthropic-beta)合并 -- 保持与 OpenAI providers 相同的合并逻辑 -- 确保 Claude 模型用户也能使用自定义 headers 功能 - ---- - -## 配置层级和优先级 - -### 配置层级 - -1. **L1(最高优先级)**: `modelProviders[authType][].generationConfig.defaultHeaders` -2. **L2(次优先级)**: `settings.model.generationConfig.defaultHeaders` - -### 合并规则 - -- 两个层级的 headers 会被合并 -- 相同名称的 header,高优先级(L1)会覆盖低优先级(L2) -- 不同名称的 headers 会被保留 - -### 示例 - -**Settings 配置:** - -```json -{ - "model": { - "generationConfig": { - "defaultHeaders": { - "X-Custom-Header": "from-settings", - "X-Another-Header": "value1" - } - } - } -} -``` - -**ModelProviders 配置:** - -```json -{ - "modelProviders": { - "openai": [ - { - "id": "qwen3-coder-plus", - "generationConfig": { - "defaultHeaders": { - "X-Custom-Header": "from-provider", - "X-Provider-Header": "value2" - } - } - } - ] - } -} -``` - -**最终生效的 headers:** - -```json -{ - "X-Custom-Header": "from-provider", // 被 provider 覆盖 - "X-Another-Header": "value1", // 保留自 settings - "X-Provider-Header": "value2" // 来自 provider -} -``` - ---- - -## 使用场景 - -1. **添加认证 headers**:为需要额外认证的 API 网关添加自定义认证头 -2. **请求追踪**:添加 `X-Request-ID`、`X-Trace-ID` 等追踪 headers -3. **API 版本控制**:通过 `X-API-Version` 指定 API 版本 -4. **自定义元数据**:添加组织、项目等元数据信息 -5. **调试和监控**:添加调试标识或监控标签 - ---- - -## 技术亮点 - -1. **类型安全**:完整的 TypeScript 类型定义,编译时检查 -2. **配置来源追踪**:记录每个配置项的来源,便于调试 -3. **向后兼容**:所有修改都是可选的,不影响现有代码 -4. **统一实现**:三个主要 content generators 都采用相同的合并逻辑 -5. **继承友好**:OpenAI providers 的继承体系自动获得支持 -6. **灵活合并**:支持多层级配置合并,满足不同场景需求 - ---- - -## 测试验证 - -- ✅ TypeScript 编译通过,无类型错误 -- ✅ 所有 OpenAI providers(Default、DashScope、ModelScope、DeepSeek、OpenRouter)都支持 -- ✅ Gemini 和 Anthropic generators 正确实现 -- ✅ 配置解析器正确处理多层级合并 -- ✅ 向后兼容,不影响未配置 defaultHeaders 的用户 - ---- - -## 总结 - -本次修改通过 8 个文件的协同更新,为 Qwen Code 项目添加了完整的自定义 HTTP headers 支持。修改遵循了项目的架构设计,保持了代码的一致性和可维护性,同时确保了向后兼容性和类型安全。用户现在可以通过简单的配置为 API 请求添加自定义 headers,满足各种企业级和高级使用场景的需求。 diff --git a/test-defaultHeaders.cjs b/test-defaultHeaders.cjs deleted file mode 100644 index 94e3cf5d4..000000000 --- a/test-defaultHeaders.cjs +++ /dev/null @@ -1,116 +0,0 @@ -/** - * defaultHeaders 功能测试脚本 - * - * 这个脚本会模拟配置并输出最终的 headers - */ - -// 模拟配置解析逻辑 -function resolveDefaultHeaders(settingsHeaders, providerHeaders) { - console.log('📋 测试 defaultHeaders 合并逻辑\n'); - - console.log('输入:'); - console.log(' Settings headers:', JSON.stringify(settingsHeaders, null, 2)); - console.log(' Provider headers:', JSON.stringify(providerHeaders, null, 2)); - console.log(''); - - const result = { - ...(settingsHeaders || {}), - ...(providerHeaders || {}), - }; - - console.log('输出(合并后):'); - console.log(' Final headers:', JSON.stringify(result, null, 2)); - console.log(''); - - return result; -} - -// 测试场景 1:只有 settings 配置 -console.log('━'.repeat(60)); -console.log('场景 1: 只配置 settings.model.generationConfig.defaultHeaders'); -console.log('━'.repeat(60)); -resolveDefaultHeaders( - { - 'X-Custom-Header': 'from-settings', - 'X-Request-ID': 'req-123', - }, - undefined -); - -// 测试场景 2:只有 provider 配置 -console.log('━'.repeat(60)); -console.log('场景 2: 只配置 modelProviders[].generationConfig.defaultHeaders'); -console.log('━'.repeat(60)); -resolveDefaultHeaders( - undefined, - { - 'X-Provider-Header': 'from-provider', - 'X-API-Version': 'v2', - } -); - -// 测试场景 3:两者都配置,无冲突 -console.log('━'.repeat(60)); -console.log('场景 3: 两者都配置,header 名称不冲突'); -console.log('━'.repeat(60)); -resolveDefaultHeaders( - { - 'X-Settings-Header': 'from-settings', - 'X-Request-ID': 'req-123', - }, - { - 'X-Provider-Header': 'from-provider', - 'X-API-Version': 'v2', - } -); - -// 测试场景 4:两者都配置,有冲突(provider 优先) -console.log('━'.repeat(60)); -console.log('场景 4: 两者都配置,有同名 header(provider 应覆盖 settings)'); -console.log('━'.repeat(60)); -resolveDefaultHeaders( - { - 'X-Custom-Header': 'from-settings', - 'X-Request-ID': 'req-123', - 'X-Common-Header': 'settings-value', - }, - { - 'X-Custom-Header': 'from-provider', - 'X-API-Version': 'v2', - 'X-Common-Header': 'provider-value', // 这个应该覆盖 settings 的值 - } -); - -// 模拟最终与基础 headers 合并 -console.log('━'.repeat(60)); -console.log('场景 5: 与系统基础 headers 合并(模拟实际使用)'); -console.log('━'.repeat(60)); - -const systemHeaders = { - 'User-Agent': 'QwenCode/0.7.0 (darwin; arm64)', -}; - -const customHeaders = { - 'X-Custom-Header': 'custom-value', - 'X-Request-ID': 'req-456', -}; - -console.log('系统基础 headers:', JSON.stringify(systemHeaders, null, 2)); -console.log('用户自定义 headers:', JSON.stringify(customHeaders, null, 2)); -console.log(''); - -const finalHeaders = { - ...systemHeaders, - ...customHeaders, -}; - -console.log('最终发送的 headers:', JSON.stringify(finalHeaders, null, 2)); -console.log(''); - -console.log('━'.repeat(60)); -console.log('✅ 测试完成!'); -console.log(''); -console.log('💡 提示:'); -console.log(' 1. 在实际代码中,在 buildHeaders() 方法打断点可以看到这些值'); -console.log(' 2. 使用网络抓包工具可以看到实际发送的 HTTP 请求头'); -console.log(' 3. 高优先级(provider)的 headers 会覆盖低优先级(settings)的同名 headers'); diff --git a/verify-defaultHeaders.cjs b/verify-defaultHeaders.cjs deleted file mode 100644 index 54b29a817..000000000 --- a/verify-defaultHeaders.cjs +++ /dev/null @@ -1,114 +0,0 @@ -/** - * defaultHeaders 功能验证脚本 - * - * 使用方法: - * node verify-defaultHeaders.js - */ - -const fs = require('fs'); -const path = require('path'); - -console.log('🔍 开始验证 defaultHeaders 功能实现...\n'); - -// 验证项目列表 -const verifications = [ - { - name: '1. ContentGeneratorConfig 类型定义', - file: 'packages/core/src/core/contentGenerator.ts', - check: (content) => content.includes('defaultHeaders?: Record'), - description: '检查 ContentGeneratorConfig 是否包含 defaultHeaders 字段' - }, - { - name: '2. ModelGenerationConfig 类型定义', - file: 'packages/core/src/models/types.ts', - check: (content) => content.includes("'defaultHeaders'"), - description: '检查 ModelGenerationConfig 是否包含 defaultHeaders' - }, - { - name: '3. MODEL_GENERATION_CONFIG_FIELDS 常量', - file: 'packages/core/src/models/constants.ts', - check: (content) => content.includes("'defaultHeaders'"), - description: '检查配置字段列表是否包含 defaultHeaders' - }, - { - name: '4. modelConfigResolver 合并逻辑', - file: 'packages/core/src/models/modelConfigResolver.ts', - check: (content) => content.includes("field === 'defaultHeaders'") && content.includes('settingsHeaders'), - description: '检查配置解析器是否实现 defaultHeaders 合并逻辑' - }, - { - name: '5. DefaultOpenAICompatibleProvider', - file: 'packages/core/src/core/openaiContentGenerator/provider/default.ts', - check: (content) => content.includes('this.contentGeneratorConfig.defaultHeaders'), - description: '检查 OpenAI 默认 provider 是否支持 defaultHeaders' - }, - { - name: '6. DashScopeOpenAICompatibleProvider', - file: 'packages/core/src/core/openaiContentGenerator/provider/dashscope.ts', - check: (content) => content.includes('this.contentGeneratorConfig.defaultHeaders'), - description: '检查 DashScope provider 是否支持 defaultHeaders' - }, - { - name: '7. GeminiContentGenerator', - file: 'packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts', - check: (content) => content.includes('contentGeneratorConfig?.defaultHeaders'), - description: '检查 Gemini generator 是否支持 defaultHeaders' - }, - { - name: '8. AnthropicContentGenerator', - file: 'packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts', - check: (content) => content.includes('this.contentGeneratorConfig.defaultHeaders'), - description: '检查 Anthropic generator 是否支持 defaultHeaders' - } -]; - -let passedCount = 0; -let failedCount = 0; - -// 执行验证 -verifications.forEach((verification, index) => { - const filePath = path.join(__dirname, verification.file); - - try { - if (!fs.existsSync(filePath)) { - console.log(`❌ ${verification.name}`); - console.log(` 文件不存在: ${verification.file}\n`); - failedCount++; - return; - } - - const content = fs.readFileSync(filePath, 'utf-8'); - const passed = verification.check(content); - - if (passed) { - console.log(`✅ ${verification.name}`); - console.log(` ${verification.description}`); - console.log(` 文件: ${verification.file}\n`); - passedCount++; - } else { - console.log(`❌ ${verification.name}`); - console.log(` ${verification.description}`); - console.log(` 文件: ${verification.file}`); - console.log(` 状态: 未找到预期的代码\n`); - failedCount++; - } - } catch (error) { - console.log(`❌ ${verification.name}`); - console.log(` 错误: ${error.message}\n`); - failedCount++; - } -}); - -// 输出总结 -console.log('━'.repeat(60)); -console.log(`\n📊 验证结果总结:`); -console.log(` ✅ 通过: ${passedCount}/${verifications.length}`); -console.log(` ❌ 失败: ${failedCount}/${verifications.length}`); - -if (failedCount === 0) { - console.log(`\n🎉 所有验证项都通过!defaultHeaders 功能已正确实现。\n`); - process.exit(0); -} else { - console.log(`\n⚠️ 有 ${failedCount} 项验证失败,请检查相关文件。\n`); - process.exit(1); -} From 2d1934bf2fbe6207a1a660d7103643f981972837 Mon Sep 17 00:00:00 2001 From: xwj02155382 Date: Fri, 9 Jan 2026 18:15:21 +0800 Subject: [PATCH 4/7] docs: add defaultHeaders documentation to settings.md - Add defaultHeaders to model.generationConfig description - Add defaultHeaders example in model.generationConfig - Add defaultHeaders example in modelProviders configuration - Document defaultHeaders merge strategy in generation config layering - Explain use cases: request tracing, monitoring, API gateway routing --- docs/users/configuration/settings.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index 3b3c54533..ea6bd442e 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -104,7 +104,7 @@ Settings are organized into categories. All settings should be placed within the | `model.name` | string | The Qwen model to use for conversations. | `undefined` | | `model.maxSessionTurns` | number | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` | | `model.summarizeToolOutput` | object | Enables or disables the summarization of tool output. You can specify the token budget for the summarization using the `tokenBudget` setting. Note: Currently only the `run_shell_command` tool is supported. For example `{"run_shell_command": {"tokenBudget": 2000}}` | `undefined` | -| `model.generationConfig` | object | Advanced overrides passed to the underlying content generator. Supports request controls such as `timeout`, `maxRetries`, and `disableCacheControl`, along with fine-tuning knobs under `samplingParams` (for example `temperature`, `top_p`, `max_tokens`). Leave unset to rely on provider defaults. | `undefined` | +| `model.generationConfig` | object | Advanced overrides passed to the underlying content generator. Supports request controls such as `timeout`, `maxRetries`, `disableCacheControl`, and `defaultHeaders` (custom HTTP headers for API requests), along with fine-tuning knobs under `samplingParams` (for example `temperature`, `top_p`, `max_tokens`). Leave unset to rely on provider defaults. | `undefined` | | `model.chatCompression.contextPercentageThreshold` | number | Sets the threshold for chat history compression as a percentage of the model's total token limit. This is a value between 0 and 1 that applies to both automatic compression and the manual `/compress` command. For example, a value of `0.6` will trigger compression when the chat history exceeds 60% of the token limit. Use `0` to disable compression entirely. | `0.7` | | `model.skipNextSpeakerCheck` | boolean | Skip the next speaker check. | `false` | | `model.skipLoopDetection` | boolean | Disables loop detection checks. Loop detection prevents infinite loops in AI responses but can generate false positives that interrupt legitimate workflows. Enable this option if you experience frequent false positive loop detection interruptions. | `false` | @@ -114,12 +114,16 @@ Settings are organized into categories. All settings should be placed within the **Example model.generationConfig:** -``` +```json { "model": { "generationConfig": { "timeout": 60000, "disableCacheControl": false, + "defaultHeaders": { + "X-Request-ID": "req-123", + "X-User-ID": "user-456" + }, "samplingParams": { "temperature": 0.2, "top_p": 0.8, @@ -130,6 +134,8 @@ Settings are organized into categories. All settings should be placed within the } ``` +The `defaultHeaders` field allows you to add custom HTTP headers to all API requests. This is useful for request tracing, monitoring, API gateway routing, or when different models require different headers. Headers defined in `modelProviders[].generationConfig.defaultHeaders` will merge with and override headers from `model.generationConfig.defaultHeaders`. + **model.openAILoggingDir examples:** - `"~/qwen-logs"` - Logs to `~/qwen-logs` directory @@ -154,6 +160,10 @@ Use `modelProviders` to declare curated model lists per auth type that the `/mod "generationConfig": { "timeout": 60000, "maxRetries": 3, + "defaultHeaders": { + "X-Model-Version": "v1.0", + "X-Request-Priority": "high" + }, "samplingParams": { "temperature": 0.2 } } } @@ -215,7 +225,7 @@ Per-field precedence for `generationConfig`: 3. `settings.model.generationConfig` 4. Content-generator defaults (`getDefaultGenerationConfig` for OpenAI, `getParameterValue` for Gemini, etc.) -`samplingParams` is treated atomically; provider values replace the entire object. Defaults from the content generator apply last so each provider retains its tuned baseline. +`samplingParams` is treated atomically; provider values replace the entire object. For `defaultHeaders`, a merge strategy is used: headers from `modelProviders[].generationConfig.defaultHeaders` will be merged with headers from `model.generationConfig.defaultHeaders`, with provider-specific headers taking precedence for duplicate keys. Defaults from the content generator apply last so each provider retains its tuned baseline. ##### Selection persistence and recommendations From adb53a6dc6bf17d2f3dd0fee2eed2ef128988f7f Mon Sep 17 00:00:00 2001 From: xwj02155382 Date: Mon, 12 Jan 2026 18:03:02 +0800 Subject: [PATCH 5/7] refactor: change customHeaders to use priority override instead of merge - Remove special merge handling for customHeaders in modelConfigResolver - Update all content generators to use priority override logic - If customHeaders is defined in modelProvider, use it directly - Otherwise, use customHeaders from global config or default headers - Update documentation to reflect the new behavior - Align customHeaders behavior with other config fields (timeout, maxRetries, etc.) --- docs/users/configuration/settings.md | 10 +++---- .../anthropicContentGenerator.ts | 13 +++++---- .../geminiContentGenerator.ts | 23 ++++++++------- .../provider/dashscope.ts | 17 ++++++----- .../provider/default.ts | 14 +++++----- .../core/src/models/modelConfigResolver.ts | 28 +------------------ 6 files changed, 39 insertions(+), 66 deletions(-) diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index ea6bd442e..97ee91598 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -104,7 +104,7 @@ Settings are organized into categories. All settings should be placed within the | `model.name` | string | The Qwen model to use for conversations. | `undefined` | | `model.maxSessionTurns` | number | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` | | `model.summarizeToolOutput` | object | Enables or disables the summarization of tool output. You can specify the token budget for the summarization using the `tokenBudget` setting. Note: Currently only the `run_shell_command` tool is supported. For example `{"run_shell_command": {"tokenBudget": 2000}}` | `undefined` | -| `model.generationConfig` | object | Advanced overrides passed to the underlying content generator. Supports request controls such as `timeout`, `maxRetries`, `disableCacheControl`, and `defaultHeaders` (custom HTTP headers for API requests), along with fine-tuning knobs under `samplingParams` (for example `temperature`, `top_p`, `max_tokens`). Leave unset to rely on provider defaults. | `undefined` | +| `model.generationConfig` | object | Advanced overrides passed to the underlying content generator. Supports request controls such as `timeout`, `maxRetries`, `disableCacheControl`, and `customHeaders` (custom HTTP headers for API requests), along with fine-tuning knobs under `samplingParams` (for example `temperature`, `top_p`, `max_tokens`). Leave unset to rely on provider defaults. | `undefined` | | `model.chatCompression.contextPercentageThreshold` | number | Sets the threshold for chat history compression as a percentage of the model's total token limit. This is a value between 0 and 1 that applies to both automatic compression and the manual `/compress` command. For example, a value of `0.6` will trigger compression when the chat history exceeds 60% of the token limit. Use `0` to disable compression entirely. | `0.7` | | `model.skipNextSpeakerCheck` | boolean | Skip the next speaker check. | `false` | | `model.skipLoopDetection` | boolean | Disables loop detection checks. Loop detection prevents infinite loops in AI responses but can generate false positives that interrupt legitimate workflows. Enable this option if you experience frequent false positive loop detection interruptions. | `false` | @@ -120,7 +120,7 @@ Settings are organized into categories. All settings should be placed within the "generationConfig": { "timeout": 60000, "disableCacheControl": false, - "defaultHeaders": { + "customHeaders": { "X-Request-ID": "req-123", "X-User-ID": "user-456" }, @@ -134,7 +134,7 @@ Settings are organized into categories. All settings should be placed within the } ``` -The `defaultHeaders` field allows you to add custom HTTP headers to all API requests. This is useful for request tracing, monitoring, API gateway routing, or when different models require different headers. Headers defined in `modelProviders[].generationConfig.defaultHeaders` will merge with and override headers from `model.generationConfig.defaultHeaders`. +The `customHeaders` field allows you to add custom HTTP headers to all API requests. This is useful for request tracing, monitoring, API gateway routing, or when different models require different headers. If `customHeaders` is defined in `modelProviders[].generationConfig.customHeaders`, it will be used directly; otherwise, headers from `model.generationConfig.customHeaders` will be used. No merging occurs between the two levels. **model.openAILoggingDir examples:** @@ -160,7 +160,7 @@ Use `modelProviders` to declare curated model lists per auth type that the `/mod "generationConfig": { "timeout": 60000, "maxRetries": 3, - "defaultHeaders": { + "customHeaders": { "X-Model-Version": "v1.0", "X-Request-Priority": "high" }, @@ -225,7 +225,7 @@ Per-field precedence for `generationConfig`: 3. `settings.model.generationConfig` 4. Content-generator defaults (`getDefaultGenerationConfig` for OpenAI, `getParameterValue` for Gemini, etc.) -`samplingParams` is treated atomically; provider values replace the entire object. For `defaultHeaders`, a merge strategy is used: headers from `modelProviders[].generationConfig.defaultHeaders` will be merged with headers from `model.generationConfig.defaultHeaders`, with provider-specific headers taking precedence for duplicate keys. Defaults from the content generator apply last so each provider retains its tuned baseline. +`samplingParams` and `customHeaders` are both treated atomically; provider values replace the entire object. If `modelProviders[].generationConfig` defines these fields, they are used directly; otherwise, values from `model.generationConfig` are used. No merging occurs between provider and global configuration levels. Defaults from the content generator apply last so each provider retains its tuned baseline. ##### Selection persistence and recommendations diff --git a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts index 54818184a..a5d714f3a 100644 --- a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts +++ b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts @@ -141,6 +141,12 @@ export class AnthropicContentGenerator implements ContentGenerator { private buildHeaders(): Record { const version = this.cliConfig.getCliVersion() || 'unknown'; const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; + const { customHeaders } = this.contentGeneratorConfig; + + // If customHeaders is provided, use it directly; otherwise build default headers + if (customHeaders) { + return customHeaders as Record; + } const betas: string[] = []; const reasoning = this.contentGeneratorConfig.reasoning; @@ -163,12 +169,7 @@ export class AnthropicContentGenerator implements ContentGenerator { headers['anthropic-beta'] = betas.join(','); } - // Merge with custom defaultHeaders from config - const customHeaders = this.contentGeneratorConfig.defaultHeaders || {}; - return { - ...headers, - ...customHeaders, - }; + return headers; } private async buildRequest( diff --git a/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts b/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts index bb9206c96..530c456a4 100644 --- a/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts +++ b/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts @@ -35,19 +35,18 @@ export class GeminiContentGenerator implements ContentGenerator { }, contentGeneratorConfig?: ContentGeneratorConfig, ) { - // Merge custom defaultHeaders into httpOptions - const customHeaders = contentGeneratorConfig?.defaultHeaders || {}; - const mergedOptions = { - ...options, - httpOptions: { - headers: { - ...(options.httpOptions?.headers || {}), - ...customHeaders, - }, - }, - }; + // If customHeaders is provided, use it directly; otherwise use options.httpOptions.headers + const customHeaders = contentGeneratorConfig?.customHeaders; + const finalOptions = customHeaders + ? { + ...options, + httpOptions: { + headers: customHeaders as Record, + }, + } + : options; - this.googleGenAI = new GoogleGenAI(mergedOptions); + this.googleGenAI = new GoogleGenAI(finalOptions); this.contentGeneratorConfig = contentGeneratorConfig; } diff --git a/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts b/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts index 6491e7bbf..176ce6b6d 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts @@ -47,20 +47,19 @@ export class DashScopeOpenAICompatibleProvider buildHeaders(): Record { const version = this.cliConfig.getCliVersion() || 'unknown'; const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; - const { authType } = this.contentGeneratorConfig; - const baseHeaders: Record = { + const { authType, customHeaders } = this.contentGeneratorConfig; + + // If customHeaders is provided, use it directly; otherwise use default headers + if (customHeaders) { + return customHeaders; + } + + return { 'User-Agent': userAgent, 'X-DashScope-CacheControl': 'enable', 'X-DashScope-UserAgent': userAgent, 'X-DashScope-AuthType': authType, }; - - // Merge with custom defaultHeaders from config - const customHeaders = this.contentGeneratorConfig.defaultHeaders || {}; - return { - ...baseHeaders, - ...customHeaders, - }; } buildClient(): OpenAI { diff --git a/packages/core/src/core/openaiContentGenerator/provider/default.ts b/packages/core/src/core/openaiContentGenerator/provider/default.ts index 6b493f522..4cda72feb 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/default.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/default.ts @@ -25,15 +25,15 @@ export class DefaultOpenAICompatibleProvider buildHeaders(): Record { const version = this.cliConfig.getCliVersion() || 'unknown'; const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; - const baseHeaders: Record = { - 'User-Agent': userAgent, - }; + const { customHeaders } = this.contentGeneratorConfig; + + // If customHeaders is provided, use it directly; otherwise use default headers + if (customHeaders) { + return customHeaders; + } - // Merge with custom defaultHeaders from config - const customHeaders = this.contentGeneratorConfig.defaultHeaders || {}; return { - ...baseHeaders, - ...customHeaders, + 'User-Agent': userAgent, }; } diff --git a/packages/core/src/models/modelConfigResolver.ts b/packages/core/src/models/modelConfigResolver.ts index 33747e43a..1afad58eb 100644 --- a/packages/core/src/models/modelConfigResolver.ts +++ b/packages/core/src/models/modelConfigResolver.ts @@ -344,33 +344,7 @@ function resolveGenerationConfig( const result: Partial = {}; for (const field of MODEL_GENERATION_CONFIG_FIELDS) { - // Special handling for defaultHeaders: merge instead of replace - if (field === 'defaultHeaders') { - const settingsHeaders = settingsConfig?.defaultHeaders; - const providerHeaders = modelProviderConfig?.defaultHeaders; - - if (settingsHeaders || providerHeaders) { - // Merge headers: provider headers override settings headers - result.defaultHeaders = { - ...(settingsHeaders || {}), - ...(providerHeaders || {}), - }; - - // Track source for merged headers - if (providerHeaders && authType) { - sources[field] = modelProvidersSource( - authType, - modelId || '', - `generationConfig.${field}`, - ); - } else if (settingsHeaders) { - sources[field] = settingsSource(`model.generationConfig.${field}`); - } - } - continue; - } - - // ModelProvider config takes priority for other fields + // ModelProvider config takes priority over settings config if (authType && modelProviderConfig && field in modelProviderConfig) { // eslint-disable-next-line @typescript-eslint/no-explicit-any (result as any)[field] = modelProviderConfig[field]; From a8eb858f99da91f5320be8d90fc6d50a8407637a Mon Sep 17 00:00:00 2001 From: xwj02155382 Date: Tue, 13 Jan 2026 10:14:55 +0800 Subject: [PATCH 6/7] refactor: rename defaultHeaders to customHeaders - Rename defaultHeaders field to customHeaders in ContentGeneratorConfig - Update MODEL_GENERATION_CONFIG_FIELDS constant - Update ModelGenerationConfig type definition - Align naming with documentation and usage across the codebase --- packages/core/src/core/contentGenerator.ts | 2 +- packages/core/src/models/constants.ts | 2 +- packages/core/src/models/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index d229b707f..476776cb6 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -92,7 +92,7 @@ export type ContentGeneratorConfig = { // Schema compliance mode for tool definitions schemaCompliance?: 'auto' | 'openapi_30'; // Custom HTTP headers to be sent with requests - defaultHeaders?: Record; + customHeaders?: Record; }; // Keep the public ContentGeneratorConfigSources API, but reuse the generic diff --git a/packages/core/src/models/constants.ts b/packages/core/src/models/constants.ts index 2c550a5d6..fcb1be985 100644 --- a/packages/core/src/models/constants.ts +++ b/packages/core/src/models/constants.ts @@ -25,7 +25,7 @@ export const MODEL_GENERATION_CONFIG_FIELDS = [ 'disableCacheControl', 'schemaCompliance', 'reasoning', - 'defaultHeaders', + 'customHeaders', ] as const satisfies ReadonlyArray; /** diff --git a/packages/core/src/models/types.ts b/packages/core/src/models/types.ts index d429bf563..c8360e158 100644 --- a/packages/core/src/models/types.ts +++ b/packages/core/src/models/types.ts @@ -31,7 +31,7 @@ export type ModelGenerationConfig = Pick< | 'disableCacheControl' | 'schemaCompliance' | 'reasoning' - | 'defaultHeaders' + | 'customHeaders' >; /** From e4dee3a2b2f3652969fcc90b5d9feb697c689f2b Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Tue, 13 Jan 2026 17:30:54 +0800 Subject: [PATCH 7/7] Implement proper header merging: customHeaders now merge with default headers instead of replacing them in all content generators Co-authored-by: Qwen-Coder --- .../anthropicContentGenerator.test.ts | 27 ++++++++++++++ .../anthropicContentGenerator.ts | 7 +--- .../geminiContentGenerator.test.ts | 35 +++++++++++++++++++ .../geminiContentGenerator.ts | 22 ++++++++---- .../provider/dashscope.test.ts | 21 +++++++++++ .../provider/dashscope.ts | 12 +++---- .../provider/default.test.ts | 20 +++++++++++ .../provider/default.ts | 12 +++---- 8 files changed, 129 insertions(+), 27 deletions(-) diff --git a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts index 483edac1a..cef3d0242 100644 --- a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts +++ b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts @@ -10,6 +10,7 @@ import type { GenerateContentParameters, } from '@google/genai'; import { FinishReason, GenerateContentResponse } from '@google/genai'; +import type { ContentGeneratorConfig } from '../contentGenerator.js'; // Mock the request tokenizer module BEFORE importing the class that uses it. const mockTokenizer = { @@ -127,6 +128,32 @@ describe('AnthropicContentGenerator', () => { ); }); + it('merges customHeaders into defaultHeaders (does not replace defaults)', async () => { + const { AnthropicContentGenerator } = await importGenerator(); + void new AnthropicContentGenerator( + { + model: 'claude-test', + apiKey: 'test-key', + baseUrl: 'https://example.invalid', + timeout: 10_000, + maxRetries: 2, + samplingParams: {}, + schemaCompliance: 'auto', + reasoning: { effort: 'medium' }, + customHeaders: { + 'X-Custom': '1', + }, + } as unknown as Record as ContentGeneratorConfig, + mockConfig, + ); + + const headers = (anthropicState.constructorOptions?.['defaultHeaders'] || + {}) as Record; + expect(headers['User-Agent']).toContain('QwenCode/1.2.3'); + expect(headers['anthropic-beta']).toContain('effort-2025-11-24'); + expect(headers['X-Custom']).toBe('1'); + }); + it('adds the effort beta header when reasoning.effort is set', async () => { const { AnthropicContentGenerator } = await importGenerator(); void new AnthropicContentGenerator( diff --git a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts index a5d714f3a..281c5d9ae 100644 --- a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts +++ b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts @@ -143,11 +143,6 @@ export class AnthropicContentGenerator implements ContentGenerator { const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; const { customHeaders } = this.contentGeneratorConfig; - // If customHeaders is provided, use it directly; otherwise build default headers - if (customHeaders) { - return customHeaders as Record; - } - const betas: string[] = []; const reasoning = this.contentGeneratorConfig.reasoning; @@ -169,7 +164,7 @@ export class AnthropicContentGenerator implements ContentGenerator { headers['anthropic-beta'] = betas.join(','); } - return headers; + return customHeaders ? { ...headers, ...customHeaders } : headers; } private async buildRequest( diff --git a/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.test.ts b/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.test.ts index 82f3b186e..bdf9bfb99 100644 --- a/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.test.ts +++ b/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.test.ts @@ -39,6 +39,41 @@ describe('GeminiContentGenerator', () => { mockGoogleGenAI = vi.mocked(GoogleGenAI).mock.results[0].value; }); + it('should merge customHeaders into existing httpOptions.headers', async () => { + vi.mocked(GoogleGenAI).mockClear(); + + void new GeminiContentGenerator( + { + apiKey: 'test-api-key', + httpOptions: { + headers: { + 'X-Base': 'base', + 'X-Override': 'base', + }, + }, + }, + { + customHeaders: { + 'X-Custom': 'custom', + 'X-Override': 'custom', + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + ); + + expect(vi.mocked(GoogleGenAI)).toHaveBeenCalledTimes(1); + expect(vi.mocked(GoogleGenAI)).toHaveBeenCalledWith({ + apiKey: 'test-api-key', + httpOptions: { + headers: { + 'X-Base': 'base', + 'X-Custom': 'custom', + 'X-Override': 'custom', + }, + }, + }); + }); + it('should call generateContent on the underlying model', async () => { const request = { model: 'gemini-1.5-flash', contents: [] }; const expectedResponse = { responseId: 'test-id' }; diff --git a/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts b/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts index 530c456a4..33819cd7f 100644 --- a/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts +++ b/packages/core/src/core/geminiContentGenerator/geminiContentGenerator.ts @@ -35,15 +35,23 @@ export class GeminiContentGenerator implements ContentGenerator { }, contentGeneratorConfig?: ContentGeneratorConfig, ) { - // If customHeaders is provided, use it directly; otherwise use options.httpOptions.headers const customHeaders = contentGeneratorConfig?.customHeaders; const finalOptions = customHeaders - ? { - ...options, - httpOptions: { - headers: customHeaders as Record, - }, - } + ? (() => { + const baseHttpOptions = options.httpOptions; + const baseHeaders = baseHttpOptions?.headers ?? {}; + + return { + ...options, + httpOptions: { + ...(baseHttpOptions ?? {}), + headers: { + ...baseHeaders, + ...customHeaders, + }, + }, + }; + })() : options; this.googleGenAI = new GoogleGenAI(finalOptions); diff --git a/packages/core/src/core/openaiContentGenerator/provider/dashscope.test.ts b/packages/core/src/core/openaiContentGenerator/provider/dashscope.test.ts index 1dabaf8ab..e7c951fd9 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/dashscope.test.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/dashscope.test.ts @@ -142,6 +142,27 @@ describe('DashScopeOpenAICompatibleProvider', () => { }); }); + it('should merge custom headers with DashScope defaults', () => { + const providerWithCustomHeaders = new DashScopeOpenAICompatibleProvider( + { + ...mockContentGeneratorConfig, + customHeaders: { + 'X-Custom': '1', + 'X-DashScope-CacheControl': 'disable', + }, + } as ContentGeneratorConfig, + mockCliConfig, + ); + + const headers = providerWithCustomHeaders.buildHeaders(); + + expect(headers['User-Agent']).toContain('QwenCode/1.0.0'); + expect(headers['X-DashScope-UserAgent']).toContain('QwenCode/1.0.0'); + expect(headers['X-DashScope-AuthType']).toBe(AuthType.QWEN_OAUTH); + expect(headers['X-Custom']).toBe('1'); + expect(headers['X-DashScope-CacheControl']).toBe('disable'); + }); + it('should handle unknown CLI version', () => { ( mockCliConfig.getCliVersion as MockedFunction< diff --git a/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts b/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts index 176ce6b6d..45b0568a0 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts @@ -48,18 +48,16 @@ export class DashScopeOpenAICompatibleProvider const version = this.cliConfig.getCliVersion() || 'unknown'; const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; const { authType, customHeaders } = this.contentGeneratorConfig; - - // If customHeaders is provided, use it directly; otherwise use default headers - if (customHeaders) { - return customHeaders; - } - - return { + const defaultHeaders = { 'User-Agent': userAgent, 'X-DashScope-CacheControl': 'enable', 'X-DashScope-UserAgent': userAgent, 'X-DashScope-AuthType': authType, }; + + return customHeaders + ? { ...defaultHeaders, ...customHeaders } + : defaultHeaders; } buildClient(): OpenAI { diff --git a/packages/core/src/core/openaiContentGenerator/provider/default.test.ts b/packages/core/src/core/openaiContentGenerator/provider/default.test.ts index 3855d2ccb..23a6887dc 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/default.test.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/default.test.ts @@ -73,6 +73,26 @@ describe('DefaultOpenAICompatibleProvider', () => { }); }); + it('should merge customHeaders with defaults (and allow overrides)', () => { + const providerWithCustomHeaders = new DefaultOpenAICompatibleProvider( + { + ...mockContentGeneratorConfig, + customHeaders: { + 'X-Custom': '1', + 'User-Agent': 'custom-agent', + }, + } as ContentGeneratorConfig, + mockCliConfig, + ); + + const headers = providerWithCustomHeaders.buildHeaders(); + + expect(headers).toEqual({ + 'User-Agent': 'custom-agent', + 'X-Custom': '1', + }); + }); + it('should handle unknown CLI version', () => { ( mockCliConfig.getCliVersion as MockedFunction< diff --git a/packages/core/src/core/openaiContentGenerator/provider/default.ts b/packages/core/src/core/openaiContentGenerator/provider/default.ts index 4cda72feb..6f449badd 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/default.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/default.ts @@ -26,15 +26,13 @@ export class DefaultOpenAICompatibleProvider const version = this.cliConfig.getCliVersion() || 'unknown'; const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; const { customHeaders } = this.contentGeneratorConfig; - - // If customHeaders is provided, use it directly; otherwise use default headers - if (customHeaders) { - return customHeaders; - } - - return { + const defaultHeaders = { 'User-Agent': userAgent, }; + + return customHeaders + ? { ...defaultHeaders, ...customHeaders } + : defaultHeaders; } buildClient(): OpenAI {