diff --git a/packages/extension/src/components/cards.tsx b/packages/extension/src/components/cards.tsx index 40c3c87..cf1008b 100644 --- a/packages/extension/src/components/cards.tsx +++ b/packages/extension/src/components/cards.tsx @@ -146,6 +146,7 @@ function CopyButton({ text, label }: { text: string; label: string }) { function extractPrompt(rawRequest: unknown, role: 'system' | 'user'): string | null { const messages = (rawRequest as { messages?: { role: string; content?: unknown }[] })?.messages if (!messages) return null + if (!Array.isArray(messages)) return null const msg = role === 'system' ? messages.find((m) => m.role === role) diff --git a/packages/llms/src/OpenAIClient.ts b/packages/llms/src/OpenAIClient.ts index 784d2e3..47f1904 100644 --- a/packages/llms/src/OpenAIClient.ts +++ b/packages/llms/src/OpenAIClient.ts @@ -45,6 +45,17 @@ export class OpenAIClient implements LLMClient { } modelPatch(requestBody) + let transformedBody: Record | 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 let response: Response @@ -55,7 +66,7 @@ export class OpenAIClient implements LLMClient { 'Content-Type': 'application/json', ...(this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }), }, - body: JSON.stringify(requestBody), + body: JSON.stringify(finalRequestBody), signal: abortSignal, }) } catch (error: unknown) { @@ -225,7 +236,7 @@ export class OpenAIClient implements LLMClient { reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens, }, rawResponse: data, - rawRequest: requestBody, + rawRequest: finalRequestBody, } } } diff --git a/packages/llms/src/errors.ts b/packages/llms/src/errors.ts index 378af75..9a9dd48 100644 --- a/packages/llms/src/errors.ts +++ b/packages/llms/src/errors.ts @@ -14,6 +14,7 @@ export const InvokeErrorType = { UNKNOWN: 'unknown', // Non-retryable + CONFIG_ERROR: 'config_error', // Invalid local configuration or hook AUTH_ERROR: 'auth_error', // Authentication failed CONTEXT_LENGTH: 'context_length', // Prompt too long CONTENT_FILTER: 'content_filter', // Content filtered diff --git a/packages/llms/src/index.ts b/packages/llms/src/index.ts index 8a5af66..bc778a0 100644 --- a/packages/llms/src/index.ts +++ b/packages/llms/src/index.ts @@ -21,6 +21,7 @@ export function parseLLMConfig(config: LLMConfig): Required { apiKey: config.apiKey || '', temperature: config.temperature ?? DEFAULT_TEMPERATURE, maxRetries: config.maxRetries ?? LLM_MAX_RETRIES, + transformRequestBody: config.transformRequestBody ?? ((requestBody) => requestBody), disableNamedToolChoice: config.disableNamedToolChoice ?? false, customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound } diff --git a/packages/llms/src/types.ts b/packages/llms/src/types.ts index 84295dd..f548da8 100644 --- a/packages/llms/src/types.ts +++ b/packages/llms/src/types.ts @@ -95,6 +95,16 @@ export interface LLMConfig { temperature?: 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 + ) => Record | undefined + /** * remove the tool_choice field from the request. * @note fix "Invalid tool_choice type: 'object'" for some LLMs. diff --git a/packages/website/src/pages/docs/advanced/page-agent-core/page.tsx b/packages/website/src/pages/docs/advanced/page-agent-core/page.tsx index 7046a9d..84b16fa 100644 --- a/packages/website/src/pages/docs/advanced/page-agent-core/page.tsx +++ b/packages/website/src/pages/docs/advanced/page-agent-core/page.tsx @@ -156,6 +156,13 @@ const result = await agent.execute('Fill in the form with test data')`} defaultValue: '3', description: isZh ? 'API 调用失败时的最大重试次数' : 'Maximum retries on API failure', }, + { + name: 'transformRequestBody', + type: '(requestBody) => Record | 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', type: 'boolean', @@ -174,6 +181,42 @@ const result = await agent.execute('Fill in the form with test data')`} ]} /> +

+ {isZh ? 'transformRequestBody 示例' : 'transformRequestBody Example'} +

+

+ {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.'} +

+ { + 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 */}

{isZh ? 'Agent 配置' : 'Agent Config'}