feat(llms): add transformRequestBody hook and improve prompt assembly (#480)
* feat(llms): add transformRequestBody hook and refine prompt handling * docs(website): document transformRequestBody usage * refactor(extension): keep function-valued config handling consistent in useAgent * feat: simplify `transformRequestBody` --------- Co-authored-by: Simon <10131203+gaomeng1900@users.noreply.github.com>
This commit is contained in:
@@ -146,6 +146,7 @@ function CopyButton({ text, label }: { text: string; label: string }) {
|
|||||||
function extractPrompt(rawRequest: unknown, role: 'system' | 'user'): string | null {
|
function extractPrompt(rawRequest: unknown, role: 'system' | 'user'): string | null {
|
||||||
const messages = (rawRequest as { messages?: { role: string; content?: unknown }[] })?.messages
|
const messages = (rawRequest as { messages?: { role: string; content?: unknown }[] })?.messages
|
||||||
if (!messages) return null
|
if (!messages) return null
|
||||||
|
if (!Array.isArray(messages)) return null
|
||||||
const msg =
|
const msg =
|
||||||
role === 'system'
|
role === 'system'
|
||||||
? messages.find((m) => m.role === role)
|
? messages.find((m) => m.role === role)
|
||||||
|
|||||||
@@ -45,6 +45,17 @@ export class OpenAIClient implements LLMClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
modelPatch(requestBody)
|
modelPatch(requestBody)
|
||||||
|
let transformedBody: Record<string, unknown> | undefined
|
||||||
|
try {
|
||||||
|
transformedBody = this.config.transformRequestBody(requestBody)
|
||||||
|
} catch (error) {
|
||||||
|
throw new InvokeError(
|
||||||
|
InvokeErrorType.CONFIG_ERROR,
|
||||||
|
`transformRequestBody failed: ${(error as Error).message}`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const finalRequestBody = transformedBody ?? requestBody
|
||||||
|
|
||||||
// 2. Call API
|
// 2. Call API
|
||||||
let response: Response
|
let response: Response
|
||||||
@@ -55,7 +66,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }),
|
...(this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestBody),
|
body: JSON.stringify(finalRequestBody),
|
||||||
signal: abortSignal,
|
signal: abortSignal,
|
||||||
})
|
})
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@@ -225,7 +236,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens,
|
reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens,
|
||||||
},
|
},
|
||||||
rawResponse: data,
|
rawResponse: data,
|
||||||
rawRequest: requestBody,
|
rawRequest: finalRequestBody,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const InvokeErrorType = {
|
|||||||
UNKNOWN: 'unknown',
|
UNKNOWN: 'unknown',
|
||||||
|
|
||||||
// Non-retryable
|
// Non-retryable
|
||||||
|
CONFIG_ERROR: 'config_error', // Invalid local configuration or hook
|
||||||
AUTH_ERROR: 'auth_error', // Authentication failed
|
AUTH_ERROR: 'auth_error', // Authentication failed
|
||||||
CONTEXT_LENGTH: 'context_length', // Prompt too long
|
CONTEXT_LENGTH: 'context_length', // Prompt too long
|
||||||
CONTENT_FILTER: 'content_filter', // Content filtered
|
CONTENT_FILTER: 'content_filter', // Content filtered
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
|||||||
apiKey: config.apiKey || '',
|
apiKey: config.apiKey || '',
|
||||||
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
|
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
|
||||||
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
|
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
|
||||||
|
transformRequestBody: config.transformRequestBody ?? ((requestBody) => requestBody),
|
||||||
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
|
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
|
||||||
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
|
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,16 @@ export interface LLMConfig {
|
|||||||
temperature?: number
|
temperature?: number
|
||||||
maxRetries?: number
|
maxRetries?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the final request body before sending it to the provider.
|
||||||
|
* Use this to implement provider-specific request tweaks such as caching hints or custom flags.
|
||||||
|
*
|
||||||
|
* Return a new object, or mutate the input object and return undefined.
|
||||||
|
*/
|
||||||
|
transformRequestBody?: (
|
||||||
|
requestBody: Record<string, unknown>
|
||||||
|
) => Record<string, unknown> | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* remove the tool_choice field from the request.
|
* remove the tool_choice field from the request.
|
||||||
* @note fix "Invalid tool_choice type: 'object'" for some LLMs.
|
* @note fix "Invalid tool_choice type: 'object'" for some LLMs.
|
||||||
|
|||||||
@@ -156,6 +156,13 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
defaultValue: '3',
|
defaultValue: '3',
|
||||||
description: isZh ? 'API 调用失败时的最大重试次数' : 'Maximum retries on API failure',
|
description: isZh ? 'API 调用失败时的最大重试次数' : 'Maximum retries on API failure',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'transformRequestBody',
|
||||||
|
type: '(requestBody) => Record<string, unknown> | undefined',
|
||||||
|
description: isZh
|
||||||
|
? '在请求发送前转换最终 request body。可用于处理供应商特定的缓存提示或私有参数。'
|
||||||
|
: 'Transform the final request body before sending it. Useful for provider-specific cache hints or private request parameters.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'disableNamedToolChoice',
|
name: 'disableNamedToolChoice',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@@ -174,6 +181,42 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-800 dark:text-gray-200">
|
||||||
|
{isZh ? 'transformRequestBody 示例' : 'transformRequestBody Example'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? '如果某个供应商需要私有字段或缓存提示,可以通过 transformRequestBody 在发送前透传请求体,而不需要把供应商逻辑写进 PageAgent 内部。'
|
||||||
|
: 'If a provider needs private fields or cache hints, use transformRequestBody to tweak the request body before sending it instead of baking provider-specific logic into PageAgent.'}
|
||||||
|
</p>
|
||||||
|
<CodeEditor
|
||||||
|
language="typescript"
|
||||||
|
code={`const agent = new PageAgentCore({
|
||||||
|
pageController: new PageController({ enableMask: true }),
|
||||||
|
baseURL: 'https://your-provider.example/v1',
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
model: 'qwen3.5-plus',
|
||||||
|
transformRequestBody: (requestBody) => {
|
||||||
|
const messages = requestBody.messages
|
||||||
|
if (!Array.isArray(messages)) return
|
||||||
|
|
||||||
|
const systemMessage = messages.find(
|
||||||
|
(message) => message && typeof message === 'object' && message.role === 'system'
|
||||||
|
) as { role: string; content?: unknown } | undefined
|
||||||
|
|
||||||
|
if (!systemMessage || typeof systemMessage.content !== 'string') return
|
||||||
|
|
||||||
|
systemMessage.content = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: systemMessage.content,
|
||||||
|
cache_control: { type: 'ephemeral' },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})`}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Agent Config */}
|
{/* Agent Config */}
|
||||||
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-800 dark:text-gray-200">
|
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-800 dark:text-gray-200">
|
||||||
{isZh ? 'Agent 配置' : 'Agent Config'}
|
{isZh ? 'Agent 配置' : 'Agent Config'}
|
||||||
|
|||||||
Reference in New Issue
Block a user