ccccccc / src /openai-handler.ts
bingn's picture
Upload 12 files
5844451 verified
/**
* openai-handler.ts - OpenAI Chat Completions API 兼容处理器
*
* 将 OpenAI 格式请求转换为内部 Anthropic 格式,复用现有 Cursor 交互管道
* 支持流式和非流式响应、工具调用、Cursor IDE Agent 模式
*/
import type { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
import type {
OpenAIChatRequest,
OpenAIMessage,
OpenAIChatCompletion,
OpenAIChatCompletionChunk,
OpenAIToolCall,
OpenAIContentPart,
OpenAITool,
} from './openai-types.js';
import type {
AnthropicRequest,
AnthropicMessage,
AnthropicContentBlock,
AnthropicTool,
CursorChatRequest,
CursorSSEEvent,
} from './types.js';
import { convertToCursorRequest, parseToolCalls, hasToolCalls } from './converter.js';
import { sendCursorRequest, sendCursorRequestFull } from './cursor-client.js';
import { getConfig } from './config.js';
import { extractThinking } from './thinking.js';
import {
isRefusal,
sanitizeResponse,
isIdentityProbe,
isToolCapabilityQuestion,
buildRetryRequest,
CLAUDE_IDENTITY_RESPONSE,
CLAUDE_TOOLS_RESPONSE,
MAX_REFUSAL_RETRIES,
estimateInputTokens,
} from './handler.js';
function chatId(): string {
return 'chatcmpl-' + uuidv4().replace(/-/g, '').substring(0, 24);
}
function toolCallId(): string {
return 'call_' + uuidv4().replace(/-/g, '').substring(0, 24);
}
// ==================== 请求转换:OpenAI → Anthropic ====================
/**
* 将 OpenAI Chat Completions 请求转换为内部 Anthropic 格式
* 这样可以完全复用现有的 convertToCursorRequest 管道
*/
function convertToAnthropicRequest(body: OpenAIChatRequest): AnthropicRequest {
const rawMessages: AnthropicMessage[] = [];
let systemPrompt: string | undefined;
for (const msg of body.messages) {
switch (msg.role) {
case 'system':
systemPrompt = (systemPrompt ? systemPrompt + '\n\n' : '') + extractOpenAIContent(msg);
break;
case 'user': {
// 检查 content 数组中是否有 tool_result 类型的块(Anthropic 风格)
const contentBlocks = extractOpenAIContentBlocks(msg);
if (Array.isArray(contentBlocks)) {
rawMessages.push({ role: 'user', content: contentBlocks });
} else {
rawMessages.push({ role: 'user', content: contentBlocks || '' });
}
break;
}
case 'assistant': {
const blocks: AnthropicContentBlock[] = [];
const contentBlocks = extractOpenAIContentBlocks(msg);
if (typeof contentBlocks === 'string' && contentBlocks) {
blocks.push({ type: 'text', text: contentBlocks });
} else if (Array.isArray(contentBlocks)) {
blocks.push(...contentBlocks);
}
if (msg.tool_calls && msg.tool_calls.length > 0) {
for (const tc of msg.tool_calls) {
let args: Record<string, unknown> = {};
try {
args = JSON.parse(tc.function.arguments);
} catch {
args = { input: tc.function.arguments };
}
blocks.push({
type: 'tool_use',
id: tc.id,
name: tc.function.name,
input: args,
});
}
}
rawMessages.push({
role: 'assistant',
content: blocks.length > 0 ? blocks : (typeof contentBlocks === 'string' ? contentBlocks : ''),
});
break;
}
case 'tool': {
rawMessages.push({
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: msg.tool_call_id,
content: extractOpenAIContent(msg),
}] as AnthropicContentBlock[],
});
break;
}
}
}
// 合并连续同角色消息(Anthropic API 要求 user/assistant 严格交替)
const messages = mergeConsecutiveRoles(rawMessages);
// 转换工具定义:支持 OpenAI 标准格式和 Cursor 扁平格式
const tools: AnthropicTool[] | undefined = body.tools?.map((t: OpenAITool | Record<string, unknown>) => {
// Cursor IDE 可能发送扁平格式:{ name, description, input_schema }
if ('function' in t && t.function) {
const fn = (t as OpenAITool).function;
return {
name: fn.name,
description: fn.description,
input_schema: fn.parameters || { type: 'object', properties: {} },
};
}
// Cursor 扁平格式
const flat = t as Record<string, unknown>;
return {
name: (flat.name as string) || '',
description: flat.description as string | undefined,
input_schema: (flat.input_schema as Record<string, unknown>) || { type: 'object', properties: {} },
};
});
return {
model: body.model,
messages,
max_tokens: Math.max(body.max_tokens || body.max_completion_tokens || 8192, 8192),
stream: body.stream,
system: systemPrompt,
tools,
temperature: body.temperature,
top_p: body.top_p,
stop_sequences: body.stop
? (Array.isArray(body.stop) ? body.stop : [body.stop])
: undefined,
};
}
/**
* 合并连续同角色的消息(Anthropic API 要求角色严格交替)
*/
function mergeConsecutiveRoles(messages: AnthropicMessage[]): AnthropicMessage[] {
if (messages.length <= 1) return messages;
const merged: AnthropicMessage[] = [];
for (const msg of messages) {
const last = merged[merged.length - 1];
if (last && last.role === msg.role) {
// 合并 content
const lastBlocks = toBlocks(last.content);
const newBlocks = toBlocks(msg.content);
last.content = [...lastBlocks, ...newBlocks];
} else {
merged.push({ ...msg });
}
}
return merged;
}
/**
* 将 content 统一转为 AnthropicContentBlock 数组
*/
function toBlocks(content: string | AnthropicContentBlock[]): AnthropicContentBlock[] {
if (typeof content === 'string') {
return content ? [{ type: 'text', text: content }] : [];
}
return content || [];
}
/**
* 从 OpenAI 消息中提取文本或多模态内容块
*/
function extractOpenAIContentBlocks(msg: OpenAIMessage): string | AnthropicContentBlock[] {
if (msg.content === null || msg.content === undefined) return '';
if (typeof msg.content === 'string') return msg.content;
if (Array.isArray(msg.content)) {
const blocks: AnthropicContentBlock[] = [];
for (const p of msg.content as (OpenAIContentPart | Record<string, unknown>)[]) {
if (p.type === 'text' && (p as OpenAIContentPart).text) {
blocks.push({ type: 'text', text: (p as OpenAIContentPart).text! });
} else if (p.type === 'image_url' && (p as OpenAIContentPart).image_url?.url) {
const url = (p as OpenAIContentPart).image_url!.url;
if (url.startsWith('data:')) {
const match = url.match(/^data:([^;]+);base64,(.+)$/);
if (match) {
blocks.push({
type: 'image',
source: { type: 'base64', media_type: match[1], data: match[2] }
});
}
} else {
blocks.push({
type: 'image',
source: { type: 'url', media_type: 'image/jpeg', data: url }
});
}
} else if (p.type === 'tool_use') {
// Anthropic 风格 tool_use 块直接透传
blocks.push(p as unknown as AnthropicContentBlock);
} else if (p.type === 'tool_result') {
// Anthropic 风格 tool_result 块直接透传
blocks.push(p as unknown as AnthropicContentBlock);
}
}
return blocks.length > 0 ? blocks : '';
}
return String(msg.content);
}
/**
* 仅提取纯文本(用于系统提示词和旧行为)
*/
function extractOpenAIContent(msg: OpenAIMessage): string {
const blocks = extractOpenAIContentBlocks(msg);
if (typeof blocks === 'string') return blocks;
return blocks.filter(b => b.type === 'text').map(b => b.text).join('\n');
}
// ==================== 主处理入口 ====================
export async function handleOpenAIChatCompletions(req: Request, res: Response): Promise<void> {
const body = req.body as OpenAIChatRequest;
console.log(`[OpenAI] 收到请求: model=${body.model}, messages=${body.messages?.length}, stream=${body.stream}, tools=${body.tools?.length ?? 0}`);
try {
// Step 1: OpenAI → Anthropic 格式
const anthropicReq = convertToAnthropicRequest(body);
// 注意:图片预处理已移入 convertToCursorRequest → preprocessImages() 统一处理
// Step 1.6: 身份探针拦截(复用 Anthropic handler 的逻辑)
if (isIdentityProbe(anthropicReq)) {
console.log(`[OpenAI] 拦截到身份探针,返回模拟响应`);
const mockText = "I am Claude, an advanced AI programming assistant created by Anthropic. I am ready to help you write code, debug, and answer your technical questions. Please let me know what we should work on!";
if (body.stream) {
return handleOpenAIMockStream(res, body, mockText);
} else {
return handleOpenAIMockNonStream(res, body, mockText);
}
}
// Step 2: Anthropic → Cursor 格式(复用现有管道)
const cursorReq = await convertToCursorRequest(anthropicReq);
if (body.stream) {
await handleOpenAIStream(res, cursorReq, body, anthropicReq);
} else {
await handleOpenAINonStream(res, cursorReq, body, anthropicReq);
}
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.error(`[OpenAI] 请求处理失败:`, message);
res.status(500).json({
error: {
message,
type: 'server_error',
code: 'internal_error',
},
});
}
}
// ==================== 身份探针模拟响应 ====================
function handleOpenAIMockStream(res: Response, body: OpenAIChatRequest, mockText: string): void {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no',
});
const id = chatId();
const created = Math.floor(Date.now() / 1000);
writeOpenAISSE(res, {
id, object: 'chat.completion.chunk', created, model: body.model,
choices: [{ index: 0, delta: { role: 'assistant', content: mockText }, finish_reason: null }],
});
writeOpenAISSE(res, {
id, object: 'chat.completion.chunk', created, model: body.model,
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
});
res.write('data: [DONE]\n\n');
res.end();
}
function handleOpenAIMockNonStream(res: Response, body: OpenAIChatRequest, mockText: string): void {
res.json({
id: chatId(),
object: 'chat.completion',
created: Math.floor(Date.now() / 1000),
model: body.model,
choices: [{
index: 0,
message: { role: 'assistant', content: mockText },
finish_reason: 'stop',
}],
usage: { prompt_tokens: 15, completion_tokens: 35, total_tokens: 50 },
});
}
// ==================== 流式处理(OpenAI SSE 格式) ====================
async function handleOpenAIStream(
res: Response,
cursorReq: CursorChatRequest,
body: OpenAIChatRequest,
anthropicReq: AnthropicRequest,
): Promise<void> {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no',
});
const id = chatId();
const created = Math.floor(Date.now() / 1000);
const model = body.model;
const hasTools = (body.tools?.length ?? 0) > 0;
// 发送 role delta
writeOpenAISSE(res, {
id, object: 'chat.completion.chunk', created, model,
choices: [{
index: 0,
delta: { role: 'assistant', content: '' },
finish_reason: null,
}],
});
let fullResponse = '';
let sentText = '';
let activeCursorReq = cursorReq;
let retryCount = 0;
// 统一缓冲模式:先缓冲全部响应,再检测拒绝和处理
const executeStream = async () => {
fullResponse = '';
await sendCursorRequest(activeCursorReq, (event: CursorSSEEvent) => {
if (event.type !== 'text-delta' || !event.delta) return;
fullResponse += event.delta;
});
};
try {
await executeStream();
console.log(`[OpenAI] 原始响应 (${fullResponse.length} chars, tools=${hasTools}): ${fullResponse.substring(0, 200)}${fullResponse.length > 200 ? '...' : ''}`);
// 拒绝检测 + 自动重试(工具模式和非工具模式均生效)
const shouldRetryRefusal = () => {
if (!isRefusal(fullResponse)) return false;
if (hasTools && hasToolCalls(fullResponse)) return false;
return true;
};
while (shouldRetryRefusal() && retryCount < MAX_REFUSAL_RETRIES) {
retryCount++;
console.log(`[OpenAI] 检测到拒绝(第${retryCount}次),自动重试...原始: ${fullResponse.substring(0, 100)}`);
const retryBody = buildRetryRequest(anthropicReq, retryCount - 1);
activeCursorReq = await convertToCursorRequest(retryBody);
await executeStream();
}
if (shouldRetryRefusal()) {
if (!hasTools) {
if (isToolCapabilityQuestion(anthropicReq)) {
console.log(`[OpenAI] 工具能力询问被拒绝,返回 Claude 能力描述`);
fullResponse = CLAUDE_TOOLS_RESPONSE;
} else {
console.log(`[OpenAI] 重试${MAX_REFUSAL_RETRIES}次后仍被拒绝,返回 Claude 身份回复`);
fullResponse = CLAUDE_IDENTITY_RESPONSE;
}
} else {
console.log(`[OpenAI] 工具模式下拒绝且无工具调用,引导模型输出`);
fullResponse = 'I understand the request. Let me analyze the information and proceed with the appropriate action.';
}
}
// 极短响应重试
if (hasTools && fullResponse.trim().length < 10 && retryCount < MAX_REFUSAL_RETRIES) {
retryCount++;
console.log(`[OpenAI] 响应过短 (${fullResponse.length} chars),重试第${retryCount}次`);
activeCursorReq = await convertToCursorRequest(anthropicReq);
await executeStream();
}
let finishReason: 'stop' | 'tool_calls' = 'stop';
// ★ Thinking 提取:OpenAI 流式模式下提取 <thinking> 块并作为 reasoning_content 发送
const config = getConfig();
if (config.enableThinking && fullResponse.includes('<thinking>')) {
const extracted = extractThinking(fullResponse);
if (extracted.thinkingBlocks.length > 0) {
const reasoningContent = extracted.thinkingBlocks.map(b => b.thinking).join('\n\n');
fullResponse = extracted.cleanText;
// 发送 reasoning_content delta
writeOpenAISSE(res, {
id, object: 'chat.completion.chunk', created, model,
choices: [{
index: 0,
delta: { reasoning_content: reasoningContent },
finish_reason: null,
}],
});
}
}
if (hasTools && hasToolCalls(fullResponse)) {
const { toolCalls, cleanText } = parseToolCalls(fullResponse);
if (toolCalls.length > 0) {
finishReason = 'tool_calls';
// 发送工具调用前的残余文本(清洗后)
let cleanOutput = isRefusal(cleanText) ? '' : cleanText;
cleanOutput = sanitizeResponse(cleanOutput);
if (cleanOutput) {
writeOpenAISSE(res, {
id, object: 'chat.completion.chunk', created, model,
choices: [{
index: 0,
delta: { content: cleanOutput },
finish_reason: null,
}],
});
}
// 增量流式发送工具调用:先发 name+id,再分块发 arguments
for (let i = 0; i < toolCalls.length; i++) {
const tc = toolCalls[i];
const tcId = toolCallId();
const argsStr = JSON.stringify(tc.arguments);
// 第一帧:发送 name + id, arguments 为空
writeOpenAISSE(res, {
id, object: 'chat.completion.chunk', created, model,
choices: [{
index: 0,
delta: {
...(i === 0 ? { content: null } : {}),
tool_calls: [{
index: i,
id: tcId,
type: 'function',
function: { name: tc.name, arguments: '' },
}],
},
finish_reason: null,
}],
});
// 后续帧:分块发送 arguments (128 字节/帧)
const CHUNK_SIZE = 128;
for (let j = 0; j < argsStr.length; j += CHUNK_SIZE) {
writeOpenAISSE(res, {
id, object: 'chat.completion.chunk', created, model,
choices: [{
index: 0,
delta: {
tool_calls: [{
index: i,
function: { arguments: argsStr.slice(j, j + CHUNK_SIZE) },
}],
},
finish_reason: null,
}],
});
}
}
} else {
// 误报:发送清洗后的文本
let textToSend = fullResponse;
if (isRefusal(fullResponse)) {
textToSend = 'The previous action is unavailable. Continue using other available actions to complete the task.';
} else {
textToSend = sanitizeResponse(fullResponse);
}
writeOpenAISSE(res, {
id, object: 'chat.completion.chunk', created, model,
choices: [{
index: 0,
delta: { content: textToSend },
finish_reason: null,
}],
});
}
} else {
// 无工具模式或无工具调用 — 统一清洗后发送
const sanitized = sanitizeResponse(fullResponse);
if (sanitized) {
writeOpenAISSE(res, {
id, object: 'chat.completion.chunk', created, model,
choices: [{
index: 0,
delta: { content: sanitized },
finish_reason: null,
}],
});
}
}
// 发送完成 chunk
writeOpenAISSE(res, {
id, object: 'chat.completion.chunk', created, model,
choices: [{
index: 0,
delta: {},
finish_reason: finishReason,
}],
});
res.write('data: [DONE]\n\n');
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
writeOpenAISSE(res, {
id, object: 'chat.completion.chunk', created, model,
choices: [{
index: 0,
delta: { content: `\n\n[Error: ${message}]` },
finish_reason: 'stop',
}],
});
res.write('data: [DONE]\n\n');
}
res.end();
}
// ==================== 非流式处理 ====================
async function handleOpenAINonStream(
res: Response,
cursorReq: CursorChatRequest,
body: OpenAIChatRequest,
anthropicReq: AnthropicRequest,
): Promise<void> {
let fullText = await sendCursorRequestFull(cursorReq);
const hasTools = (body.tools?.length ?? 0) > 0;
console.log(`[OpenAI] 非流式原始响应 (${fullText.length} chars, tools=${hasTools}): ${fullText.substring(0, 300)}${fullText.length > 300 ? '...' : ''}`);
// 拒绝检测 + 自动重试(工具模式和非工具模式均生效)
const shouldRetry = () => isRefusal(fullText) && !(hasTools && hasToolCalls(fullText));
if (shouldRetry()) {
for (let attempt = 0; attempt < MAX_REFUSAL_RETRIES; attempt++) {
console.log(`[OpenAI] 非流式:检测到拒绝(第${attempt + 1}次重试)...原始: ${fullText.substring(0, 100)}`);
const retryBody = buildRetryRequest(anthropicReq, attempt);
const retryCursorReq = await convertToCursorRequest(retryBody);
fullText = await sendCursorRequestFull(retryCursorReq);
if (!shouldRetry()) break;
}
if (shouldRetry()) {
if (hasTools) {
console.log(`[OpenAI] 非流式:工具模式下拒绝,引导模型输出`);
fullText = 'I understand the request. Let me analyze the information and proceed with the appropriate action.';
} else if (isToolCapabilityQuestion(anthropicReq)) {
console.log(`[OpenAI] 非流式:工具能力询问被拒绝,返回 Claude 能力描述`);
fullText = CLAUDE_TOOLS_RESPONSE;
} else {
console.log(`[OpenAI] 非流式:重试${MAX_REFUSAL_RETRIES}次后仍被拒绝,返回 Claude 身份回复`);
fullText = CLAUDE_IDENTITY_RESPONSE;
}
}
}
let content: string | null = fullText;
let toolCalls: OpenAIToolCall[] | undefined;
let finishReason: 'stop' | 'tool_calls' = 'stop';
let reasoningContent: string | undefined;
// ★ Thinking 提取:OpenAI 非流式模式下提取 <thinking> 块
const config = getConfig();
if (config.enableThinking && fullText.includes('<thinking>')) {
const extracted = extractThinking(fullText);
if (extracted.thinkingBlocks.length > 0) {
reasoningContent = extracted.thinkingBlocks.map(b => b.thinking).join('\n\n');
fullText = extracted.cleanText;
}
}
if (hasTools) {
const parsed = parseToolCalls(fullText);
if (parsed.toolCalls.length > 0) {
finishReason = 'tool_calls';
// 清洗拒绝文本
let cleanText = parsed.cleanText;
if (isRefusal(cleanText)) {
console.log(`[OpenAI] 抑制工具模式下的拒绝文本: ${cleanText.substring(0, 100)}...`);
cleanText = '';
}
content = sanitizeResponse(cleanText) || null;
toolCalls = parsed.toolCalls.map(tc => ({
id: toolCallId(),
type: 'function' as const,
function: {
name: tc.name,
arguments: JSON.stringify(tc.arguments),
},
}));
} else {
// 无工具调用,检查拒绝
if (isRefusal(fullText)) {
content = 'The previous action is unavailable. Continue using other available actions to complete the task.';
} else {
content = sanitizeResponse(fullText);
}
}
} else {
// 无工具模式:清洗响应
content = sanitizeResponse(fullText);
}
const response: OpenAIChatCompletion = {
id: chatId(),
object: 'chat.completion',
created: Math.floor(Date.now() / 1000),
model: body.model,
choices: [{
index: 0,
message: {
role: 'assistant',
content,
...(reasoningContent ? { reasoning_content: reasoningContent } : {}),
...(toolCalls ? { tool_calls: toolCalls } : {}),
},
finish_reason: finishReason,
}],
usage: {
prompt_tokens: estimateInputTokens(anthropicReq).input_tokens,
completion_tokens: Math.ceil(fullText.length / 3),
total_tokens: estimateInputTokens(anthropicReq).input_tokens + Math.ceil(fullText.length / 3),
...estimateInputTokens(anthropicReq) // Merge anthropic cache metrics for compatibility
},
};
res.json(response);
}
// ==================== 工具函数 ====================
function writeOpenAISSE(res: Response, data: OpenAIChatCompletionChunk): void {
res.write(`data: ${JSON.stringify(data)}\n\n`);
if (typeof (res as unknown as { flush: () => void }).flush === 'function') {
(res as unknown as { flush: () => void }).flush();
}
}
// ==================== /v1/responses 支持 ====================
/**
* 处理 Cursor IDE Agent 模式的 /v1/responses 请求
*
* Cursor IDE 对 GPT 模型发送 OpenAI Responses API 格式请求,
* 这里将其转换为 Chat Completions 格式后复用现有管道
*/
export async function handleOpenAIResponses(req: Request, res: Response): Promise<void> {
try {
const body = req.body;
console.log(`[OpenAI] 收到 /v1/responses 请求: model=${body.model}`);
// 将 Responses API 格式转换为 Chat Completions 格式
const chatBody = responsesToChatCompletions(body);
// 此后复用现有管道
req.body = chatBody;
return handleOpenAIChatCompletions(req, res);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.error(`[OpenAI] /v1/responses 处理失败:`, message);
res.status(500).json({
error: { message, type: 'server_error', code: 'internal_error' },
});
}
}
/**
* 将 OpenAI Responses API 格式转换为 Chat Completions 格式
*
* Responses API 使用 `input` 而非 `messages`,格式与 Chat Completions 不同
*/
export function responsesToChatCompletions(body: Record<string, unknown>): OpenAIChatRequest {
const messages: OpenAIMessage[] = [];
// 系统指令
if (body.instructions && typeof body.instructions === 'string') {
messages.push({ role: 'system', content: body.instructions });
}
// 转换 input
const input = body.input;
if (typeof input === 'string') {
messages.push({ role: 'user', content: input });
} else if (Array.isArray(input)) {
for (const item of input as Record<string, unknown>[]) {
// function_call_output 没有 role 字段,必须先检查 type
if (item.type === 'function_call_output') {
messages.push({
role: 'tool',
content: (item.output as string) || '',
tool_call_id: (item.call_id as string) || '',
});
continue;
}
const role = (item.role as string) || 'user';
if (role === 'system' || role === 'developer') {
const text = typeof item.content === 'string'
? item.content
: Array.isArray(item.content)
? (item.content as Array<Record<string, unknown>>).filter(b => b.type === 'input_text').map(b => b.text as string).join('\n')
: String(item.content || '');
messages.push({ role: 'system', content: text });
} else if (role === 'user') {
const content = typeof item.content === 'string'
? item.content
: Array.isArray(item.content)
? (item.content as Array<Record<string, unknown>>).filter(b => b.type === 'input_text').map(b => b.text as string).join('\n')
: String(item.content || '');
messages.push({ role: 'user', content });
} else if (role === 'assistant') {
const blocks = Array.isArray(item.content) ? item.content as Array<Record<string, unknown>> : [];
const text = blocks.filter(b => b.type === 'output_text').map(b => b.text as string).join('\n');
// 检查是否有工具调用
const toolCallBlocks = blocks.filter(b => b.type === 'function_call');
const toolCalls: OpenAIToolCall[] = toolCallBlocks.map(b => ({
id: (b.call_id as string) || toolCallId(),
type: 'function' as const,
function: {
name: (b.name as string) || '',
arguments: (b.arguments as string) || '{}',
},
}));
messages.push({
role: 'assistant',
content: text || null,
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
});
}
}
}
// 转换工具定义
const tools: OpenAITool[] | undefined = Array.isArray(body.tools)
? (body.tools as Array<Record<string, unknown>>).map(t => {
if (t.type === 'function') {
return {
type: 'function' as const,
function: {
name: (t.name as string) || '',
description: t.description as string | undefined,
parameters: t.parameters as Record<string, unknown> | undefined,
},
};
}
return {
type: 'function' as const,
function: {
name: (t.name as string) || '',
description: t.description as string | undefined,
parameters: t.parameters as Record<string, unknown> | undefined,
},
};
})
: undefined;
return {
model: (body.model as string) || 'gpt-4',
messages,
stream: (body.stream as boolean) ?? true,
temperature: body.temperature as number | undefined,
max_tokens: (body.max_output_tokens as number) || 8192,
tools,
};
}