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];