feat: add extra_body support for OpenAI-compatible providers

Add extra_body configuration option to model.generationConfig for passing
custom parameters to OpenAI-compatible API request bodies.

- Add extra_body to ContentGeneratorConfig type
- Add extra_body to MODEL_GENERATION_CONFIG_FIELDS and ModelGenerationConfig
- Implement extra_body merging in DefaultOpenAICompatibleProvider
- Implement extra_body merging in DashScopeOpenAICompatibleProvider
- Update documentation with examples and provider compatibility notes
- Note: This feature is only for OpenAI-compatible providers (openai, qwen-oauth)

Resolves #1647
Resolves #1644

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-01-29 12:33:54 +08:00
parent 561be0eb42
commit 532d97670b
8 changed files with 140 additions and 13 deletions

View file

@ -93,6 +93,8 @@ export type ContentGeneratorConfig = {
schemaCompliance?: 'auto' | 'openapi_30';
// Custom HTTP headers to be sent with requests
customHeaders?: Record<string, string>;
// Extra body parameters to be merged into the request body
extra_body?: Record<string, unknown>;
};
// Keep the public ContentGeneratorConfigSources API, but reuse the generic

View file

@ -929,5 +929,71 @@ describe('DashScopeOpenAICompatibleProvider', () => {
expect(result.max_tokens).toBe(65536); // Should be limited
expect(result.stream).toBe(true); // Streaming should be preserved
});
it('should merge extra_body into the request', () => {
const providerWithExtraBody = new DashScopeOpenAICompatibleProvider(
{
...mockContentGeneratorConfig,
extra_body: {
custom_param: 'custom_value',
nested: { key: 'value' },
},
},
mockCliConfig,
);
const request: OpenAI.Chat.ChatCompletionCreateParams = {
model: 'qwen3-coder-plus',
messages: [{ role: 'user', content: 'Hello' }],
};
const result = providerWithExtraBody.buildRequest(
request,
'test-prompt-id',
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((result as any).custom_param).toBe('custom_value');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((result as any).nested).toEqual({ key: 'value' });
});
it('should merge extra_body into vision model requests', () => {
const providerWithExtraBody = new DashScopeOpenAICompatibleProvider(
{
...mockContentGeneratorConfig,
extra_body: {
custom_param: 'custom_value',
},
},
mockCliConfig,
);
const request: OpenAI.Chat.ChatCompletionCreateParams = {
model: 'qwen-vl-max',
messages: [{ role: 'user', content: 'Hello' }],
};
const result = providerWithExtraBody.buildRequest(
request,
'test-prompt-id',
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((result as any).custom_param).toBe('custom_value');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((result as any).vl_high_resolution_images).toBe(true);
});
it('should not include extra_body when not configured', () => {
const request: OpenAI.Chat.ChatCompletionCreateParams = {
model: 'qwen3-coder-plus',
messages: [{ role: 'user', content: 'Hello' }],
};
const result = provider.buildRequest(request, 'test-prompt-id');
expect(result).not.toHaveProperty('custom_param');
});
});
});

View file

@ -124,6 +124,8 @@ export class DashScopeOpenAICompatibleProvider
request.model,
);
const extraBody = this.contentGeneratorConfig.extra_body;
if (this.isVisionModel(request.model)) {
return {
...requestWithTokenLimits,
@ -132,6 +134,7 @@ export class DashScopeOpenAICompatibleProvider
...(this.buildMetadata(userPromptId) || {}),
/* @ts-expect-error dashscope exclusive */
vl_high_resolution_images: true,
...(extraBody ? extraBody : {}),
} as OpenAI.Chat.ChatCompletionCreateParams;
}
@ -140,6 +143,7 @@ export class DashScopeOpenAICompatibleProvider
messages,
...(tools ? { tools } : {}),
...(this.buildMetadata(userPromptId) || {}),
...(extraBody ? extraBody : {}),
} as OpenAI.Chat.ChatCompletionCreateParams;
}

View file

@ -261,5 +261,48 @@ describe('DefaultOpenAICompatibleProvider', () => {
// Result should be a different object
expect(result).not.toBe(originalRequest);
});
it('should merge extra_body into the request', () => {
const providerWithExtraBody = new DefaultOpenAICompatibleProvider(
{
...mockContentGeneratorConfig,
extra_body: {
custom_param: 'custom_value',
nested: { key: 'value' },
},
} as ContentGeneratorConfig,
mockCliConfig,
);
const originalRequest: OpenAI.Chat.ChatCompletionCreateParams = {
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello' }],
temperature: 0.7,
};
const result = providerWithExtraBody.buildRequest(
originalRequest,
'prompt-id',
);
expect(result).toEqual({
...originalRequest,
custom_param: 'custom_value',
nested: { key: 'value' },
});
});
it('should not include extra_body when not configured', () => {
const originalRequest: OpenAI.Chat.ChatCompletionCreateParams = {
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello' }],
temperature: 0.7,
};
const result = provider.buildRequest(originalRequest, 'prompt-id');
expect(result).toEqual(originalRequest);
expect(result).not.toHaveProperty('custom_param');
});
});
});

View file

@ -64,9 +64,11 @@ export class DefaultOpenAICompatibleProvider
request: OpenAI.Chat.ChatCompletionCreateParams,
_userPromptId: string,
): OpenAI.Chat.ChatCompletionCreateParams {
const extraBody = this.contentGeneratorConfig.extra_body;
// Default provider doesn't need special enhancements, just pass through all parameters
return {
...request, // Preserve all original parameters including sampling params
...(extraBody ? extraBody : {}),
};
}

View file

@ -26,6 +26,7 @@ export const MODEL_GENERATION_CONFIG_FIELDS = [
'schemaCompliance',
'reasoning',
'customHeaders',
'extra_body',
] as const satisfies ReadonlyArray<keyof ContentGeneratorConfig>;
/**

View file

@ -32,6 +32,7 @@ export type ModelGenerationConfig = Pick<
| 'schemaCompliance'
| 'reasoning'
| 'customHeaders'
| 'extra_body'
>;
/**