refactor(converters): 重构协议转换器架构并迁移至策略模式
- 创建新的转换器基础架构,包括BaseConverter、ConverterFactory和策略模式实现 - 迁移OpenAI、Claude、Gemini和OpenAI Responses(仅对话,无工具)转换器到新的策略模式 - 移除旧的ensureRolesInContents函数,优化内容处理逻辑 - 添加注册转换器机制,支持动态协议转换 - 更新API服务器以使用新的转换器工厂 - 优化流式响应处理和协议映射逻辑 - 添加详细的转换器工具和实用函数 - 改进错误处理和调试日志记录
This commit is contained in:
parent
abf7b15781
commit
a212a71385
31 changed files with 7517 additions and 2539 deletions
|
|
@ -111,6 +111,7 @@ import { promises as pfs } from 'fs';
|
|||
import 'dotenv/config'; // Import dotenv and configure it
|
||||
|
||||
import deepmerge from 'deepmerge';
|
||||
import './converters/register-converters.js'; // 注册所有转换器
|
||||
import { getServiceAdapter, serviceInstances } from './adapter.js';
|
||||
import { ProviderPoolManager } from './provider-pool-manager.js';
|
||||
import {
|
||||
|
|
@ -449,8 +450,7 @@ async function initApiService(config) {
|
|||
if (config.providerPools && Object.keys(config.providerPools).length > 0) {
|
||||
providerPoolManager = new ProviderPoolManager(config.providerPools, { globalConfig: config });
|
||||
console.log('[Initialization] ProviderPoolManager initialized with configured pools.');
|
||||
// 可以选择在这里触发一次健康检查
|
||||
providerPoolManager.performHealthChecks(true);
|
||||
// 健康检查将在服务器完全启动后执行
|
||||
} else {
|
||||
console.log('[Initialization] No provider pools configured. Using single provider mode.');
|
||||
}
|
||||
|
|
@ -505,7 +505,7 @@ function logProviderSpecificDetails(provider, config) {
|
|||
} else {
|
||||
console.log(` [gemini-cli-oauth] OAuth Creds: Default discovery`);
|
||||
}
|
||||
console.log(` [gemini-cli-oauth] Project ID: ${config.PROJECT_ID || 'Auto-discovered'}`);
|
||||
// console.log(` [gemini-cli-oauth] Project ID: ${config.PROJECT_ID || 'Auto-discovered'}`);
|
||||
break;
|
||||
case MODEL_PROVIDER.KIRO_API:
|
||||
if (config.KIRO_OAUTH_CREDS_FILE_PATH) {
|
||||
|
|
@ -743,6 +743,11 @@ async function startServer() {
|
|||
// 每 CRON_NEAR_MINUTES 分钟执行一次心跳日志和令牌刷新
|
||||
setInterval(heartbeatAndRefreshToken, CONFIG.CRON_NEAR_MINUTES * 60 * 1000);
|
||||
}
|
||||
// 服务器完全启动后,执行初始健康检查
|
||||
if (providerPoolManager) {
|
||||
console.log('[Initialization] Performing initial health checks for provider pools...');
|
||||
providerPoolManager.performHealthChecks(true);
|
||||
}
|
||||
});
|
||||
return server; // Return the server instance for testing purposes
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,33 +68,6 @@ export function formatExpiryTime(expiryTimestamp) {
|
|||
return `${pad(hours)}h ${pad(minutes)}m ${pad(seconds)}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that all content parts in a request body have a 'role' property.
|
||||
* If 'systemInstruction' is present and lacks a role, it defaults to 'user'.
|
||||
* If any 'contents' entry lacks a role, it defaults to 'user'.
|
||||
* @param {Object} requestBody - The request body object.
|
||||
* @returns {Object} The modified request body with roles ensured.
|
||||
*/
|
||||
export function ensureRolesInContents(requestBody) {
|
||||
if (requestBody.system_instruction) {
|
||||
requestBody.systemInstruction = requestBody.system_instruction;
|
||||
delete requestBody.system_instruction;
|
||||
}
|
||||
|
||||
if (requestBody.systemInstruction && !requestBody.systemInstruction.role) {
|
||||
requestBody.systemInstruction.role = 'user';
|
||||
}
|
||||
|
||||
if (requestBody.contents && Array.isArray(requestBody.contents)) {
|
||||
requestBody.contents.forEach(content => {
|
||||
if (!content.role) {
|
||||
content.role = 'user';
|
||||
}
|
||||
});
|
||||
}
|
||||
return requestBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the entire request body from an HTTP request.
|
||||
* @param {http.IncomingMessage} req - The HTTP request object.
|
||||
|
|
@ -213,29 +186,23 @@ export async function handleStreamRequest(res, service, model, requestBody, from
|
|||
|
||||
// fs.writeFile('request'+Date.now()+'.json', JSON.stringify(requestBody));
|
||||
// The service returns a stream in its native format (toProvider).
|
||||
const nativeStream = await service.generateContentStream(model, requestBody);
|
||||
const needsConversion = getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider);
|
||||
requestBody.model = model;
|
||||
const nativeStream = await service.generateContentStream(model, requestBody);
|
||||
const addEvent = getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.CLAUDE || getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES;
|
||||
const openStop = getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.OPENAI ;
|
||||
const openResponses = getProtocolPrefix(fromProvider) === MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES ;
|
||||
|
||||
try {
|
||||
if (openResponses && needsConversion) {
|
||||
const beginChunks = getOpenAIResponsesStreamChunkBegin(model);
|
||||
for (const chunk of beginChunks) {
|
||||
res.write(`event: ${chunk.type}\n`);
|
||||
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
||||
}
|
||||
}
|
||||
for await (const nativeChunk of nativeStream) {
|
||||
// Convert chunk to the client's format (fromProvider), if necessary.
|
||||
// Extract text for logging purposes
|
||||
const chunkText = extractResponseText(nativeChunk, toProvider);
|
||||
if (chunkText && !Array.isArray(chunkText)) {
|
||||
fullResponseText += chunkText;
|
||||
}
|
||||
|
||||
// Convert the complete chunk object to the client's format (fromProvider), if necessary.
|
||||
const chunkToSend = needsConversion
|
||||
? convertData(chunkText, 'streamChunk', toProvider, fromProvider, model)
|
||||
? convertData(nativeChunk, 'streamChunk', toProvider, fromProvider, model)
|
||||
: nativeChunk;
|
||||
|
||||
if (!chunkToSend) {
|
||||
|
|
@ -247,6 +214,7 @@ export async function handleStreamRequest(res, service, model, requestBody, from
|
|||
|
||||
for (const chunk of chunksToSend) {
|
||||
if (addEvent) {
|
||||
// fullOldResponseJson += chunk.type+"\n";
|
||||
// fullResponseJson += chunk.type+"\n";
|
||||
res.write(`event: ${chunk.type}\n`);
|
||||
// console.log(`event: ${chunk.type}\n`);
|
||||
|
|
@ -258,13 +226,6 @@ export async function handleStreamRequest(res, service, model, requestBody, from
|
|||
// console.log(`data: ${JSON.stringify(chunk)}\n`);
|
||||
}
|
||||
}
|
||||
if (openResponses && needsConversion) {
|
||||
const endChunks = getOpenAIResponsesStreamChunkEnd(model);
|
||||
for (const chunk of endChunks) {
|
||||
res.write(`event: ${chunk.type}\n`);
|
||||
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
||||
}
|
||||
}
|
||||
if (openStop && needsConversion) {
|
||||
res.write(`data: ${JSON.stringify(getOpenAIStreamChunkStop(model))}\n\n`);
|
||||
// console.log(`data: ${JSON.stringify(getOpenAIStreamChunkStop(model))}\n`);
|
||||
|
|
@ -291,20 +252,23 @@ export async function handleStreamRequest(res, service, model, requestBody, from
|
|||
res.end();
|
||||
}
|
||||
await logConversation('output', fullResponseText, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME);
|
||||
// fs.writeFile('oldResponse'+Date.now()+'.json', fullOldResponseJson);
|
||||
// fs.writeFile('response'+Date.now()+'.json', fullResponseJson);
|
||||
// fs.writeFile('oldResponseChunk'+Date.now()+'.json', fullOldResponseJson);
|
||||
// fs.writeFile('responseChunk'+Date.now()+'.json', fullResponseJson);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleUnaryRequest(res, service, model, requestBody, fromProvider, toProvider, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, pooluuid) {
|
||||
try{
|
||||
// The service returns the response in its native format (toProvider).
|
||||
const needsConversion = getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider);
|
||||
requestBody.model = model;
|
||||
// fs.writeFile('oldRequest'+Date.now()+'.json', JSON.stringify(requestBody));
|
||||
const nativeResponse = await service.generateContent(model, requestBody);
|
||||
const responseText = extractResponseText(nativeResponse, toProvider);
|
||||
|
||||
// Convert the response back to the client's format (fromProvider), if necessary.
|
||||
let clientResponse = nativeResponse;
|
||||
if (getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider)) {
|
||||
if (needsConversion) {
|
||||
console.log(`[Response Convert] Converting response from ${toProvider} to ${fromProvider}`);
|
||||
clientResponse = convertData(nativeResponse, 'response', toProvider, fromProvider, model);
|
||||
}
|
||||
|
|
@ -312,6 +276,7 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
|
|||
//console.log(`[Response] Sending response to client: ${JSON.stringify(clientResponse)}`);
|
||||
await handleUnifiedResponse(res, JSON.stringify(clientResponse), false);
|
||||
await logConversation('output', responseText, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME);
|
||||
// fs.writeFile('oldResponse'+Date.now()+'.json', JSON.stringify(clientResponse));
|
||||
} catch (error) {
|
||||
console.error('\n[Server] Error during unary processing:', error.stack);
|
||||
if (providerPoolManager) {
|
||||
|
|
|
|||
2571
src/convert-old.js
Normal file
2571
src/convert-old.js
Normal file
File diff suppressed because it is too large
Load diff
2739
src/convert.js
2739
src/convert.js
File diff suppressed because it is too large
Load diff
115
src/converters/BaseConverter.js
Normal file
115
src/converters/BaseConverter.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* 转换器基类
|
||||
* 使用策略模式定义转换器的通用接口
|
||||
*/
|
||||
|
||||
/**
|
||||
* 抽象转换器基类
|
||||
* 所有具体的协议转换器都应继承此类
|
||||
*/
|
||||
export class BaseConverter {
|
||||
constructor(protocolName) {
|
||||
if (new.target === BaseConverter) {
|
||||
throw new Error('BaseConverter是抽象类,不能直接实例化');
|
||||
}
|
||||
this.protocolName = protocolName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换请求
|
||||
* @param {Object} data - 请求数据
|
||||
* @param {string} targetProtocol - 目标协议
|
||||
* @returns {Object} 转换后的请求
|
||||
*/
|
||||
convertRequest(data, targetProtocol) {
|
||||
throw new Error('convertRequest方法必须被子类实现');
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换响应
|
||||
* @param {Object} data - 响应数据
|
||||
* @param {string} targetProtocol - 目标协议
|
||||
* @param {string} model - 模型名称
|
||||
* @returns {Object} 转换后的响应
|
||||
*/
|
||||
convertResponse(data, targetProtocol, model) {
|
||||
throw new Error('convertResponse方法必须被子类实现');
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换流式响应块
|
||||
* @param {Object} chunk - 流式响应块
|
||||
* @param {string} targetProtocol - 目标协议
|
||||
* @param {string} model - 模型名称
|
||||
* @returns {Object} 转换后的流式响应块
|
||||
*/
|
||||
convertStreamChunk(chunk, targetProtocol, model) {
|
||||
throw new Error('convertStreamChunk方法必须被子类实现');
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换模型列表
|
||||
* @param {Object} data - 模型列表数据
|
||||
* @param {string} targetProtocol - 目标协议
|
||||
* @returns {Object} 转换后的模型列表
|
||||
*/
|
||||
convertModelList(data, targetProtocol) {
|
||||
throw new Error('convertModelList方法必须被子类实现');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取协议名称
|
||||
* @returns {string} 协议名称
|
||||
*/
|
||||
getProtocolName() {
|
||||
return this.protocolName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容处理器接口
|
||||
* 用于处理不同类型的内容(文本、图片、音频等)
|
||||
*/
|
||||
export class ContentProcessor {
|
||||
/**
|
||||
* 处理内容
|
||||
* @param {*} content - 内容数据
|
||||
* @returns {*} 处理后的内容
|
||||
*/
|
||||
process(content) {
|
||||
throw new Error('process方法必须被子类实现');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具处理器接口
|
||||
* 用于处理工具调用相关的转换
|
||||
*/
|
||||
export class ToolProcessor {
|
||||
/**
|
||||
* 处理工具定义
|
||||
* @param {Array} tools - 工具定义数组
|
||||
* @returns {Array} 处理后的工具定义
|
||||
*/
|
||||
processToolDefinitions(tools) {
|
||||
throw new Error('processToolDefinitions方法必须被子类实现');
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理工具调用
|
||||
* @param {Object} toolCall - 工具调用数据
|
||||
* @returns {Object} 处理后的工具调用
|
||||
*/
|
||||
processToolCall(toolCall) {
|
||||
throw new Error('processToolCall方法必须被子类实现');
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理工具结果
|
||||
* @param {Object} toolResult - 工具结果数据
|
||||
* @returns {Object} 处理后的工具结果
|
||||
*/
|
||||
processToolResult(toolResult) {
|
||||
throw new Error('processToolResult方法必须被子类实现');
|
||||
}
|
||||
}
|
||||
182
src/converters/ConverterFactory.js
Normal file
182
src/converters/ConverterFactory.js
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
/**
|
||||
* 转换器工厂类
|
||||
* 使用工厂模式管理转换器实例的创建和缓存
|
||||
*/
|
||||
|
||||
import { MODEL_PROTOCOL_PREFIX } from '../common.js';
|
||||
|
||||
/**
|
||||
* 转换器工厂(单例模式 + 工厂模式)
|
||||
*/
|
||||
export class ConverterFactory {
|
||||
// 私有静态属性:存储转换器实例
|
||||
static #converters = new Map();
|
||||
|
||||
// 私有静态属性:存储转换器类
|
||||
static #converterClasses = new Map();
|
||||
|
||||
/**
|
||||
* 注册转换器类
|
||||
* @param {string} protocolPrefix - 协议前缀
|
||||
* @param {Class} ConverterClass - 转换器类
|
||||
*/
|
||||
static registerConverter(protocolPrefix, ConverterClass) {
|
||||
this.#converterClasses.set(protocolPrefix, ConverterClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取转换器实例(带缓存)
|
||||
* @param {string} protocolPrefix - 协议前缀
|
||||
* @returns {BaseConverter} 转换器实例
|
||||
*/
|
||||
static getConverter(protocolPrefix) {
|
||||
// 检查缓存
|
||||
if (this.#converters.has(protocolPrefix)) {
|
||||
return this.#converters.get(protocolPrefix);
|
||||
}
|
||||
|
||||
// 创建新实例
|
||||
const converter = this.createConverter(protocolPrefix);
|
||||
|
||||
// 缓存实例
|
||||
if (converter) {
|
||||
this.#converters.set(protocolPrefix, converter);
|
||||
}
|
||||
|
||||
return converter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建转换器实例
|
||||
* @param {string} protocolPrefix - 协议前缀
|
||||
* @returns {BaseConverter} 转换器实例
|
||||
*/
|
||||
static createConverter(protocolPrefix) {
|
||||
const ConverterClass = this.#converterClasses.get(protocolPrefix);
|
||||
|
||||
if (!ConverterClass) {
|
||||
throw new Error(`No converter registered for protocol: ${protocolPrefix}`);
|
||||
}
|
||||
|
||||
return new ConverterClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缓存的转换器
|
||||
*/
|
||||
static clearCache() {
|
||||
this.#converters.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除特定协议的转换器缓存
|
||||
* @param {string} protocolPrefix - 协议前缀
|
||||
*/
|
||||
static clearConverterCache(protocolPrefix) {
|
||||
this.#converters.delete(protocolPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的协议
|
||||
* @returns {Array<string>} 协议前缀数组
|
||||
*/
|
||||
static getRegisteredProtocols() {
|
||||
return Array.from(this.#converterClasses.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查协议是否已注册
|
||||
* @param {string} protocolPrefix - 协议前缀
|
||||
* @returns {boolean} 是否已注册
|
||||
*/
|
||||
static isProtocolRegistered(protocolPrefix) {
|
||||
return this.#converterClasses.has(protocolPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容处理器工厂
|
||||
*/
|
||||
export class ContentProcessorFactory {
|
||||
static #processors = new Map();
|
||||
|
||||
/**
|
||||
* 获取内容处理器
|
||||
* @param {string} sourceFormat - 源格式
|
||||
* @param {string} targetFormat - 目标格式
|
||||
* @returns {ContentProcessor} 内容处理器实例
|
||||
*/
|
||||
static getProcessor(sourceFormat, targetFormat) {
|
||||
const key = `${sourceFormat}_to_${targetFormat}`;
|
||||
|
||||
if (!this.#processors.has(key)) {
|
||||
this.#processors.set(key, this.createProcessor(sourceFormat, targetFormat));
|
||||
}
|
||||
|
||||
return this.#processors.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建内容处理器
|
||||
* @param {string} sourceFormat - 源格式
|
||||
* @param {string} targetFormat - 目标格式
|
||||
* @returns {ContentProcessor} 内容处理器实例
|
||||
*/
|
||||
static createProcessor(sourceFormat, targetFormat) {
|
||||
// 这里返回null,实际使用时需要导入具体的处理器类
|
||||
// 为了避免循环依赖,处理器类应该在使用时动态导入
|
||||
console.warn(`Content processor for ${sourceFormat} to ${targetFormat} not yet implemented`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缓存的处理器
|
||||
*/
|
||||
static clearCache() {
|
||||
this.#processors.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具处理器工厂
|
||||
*/
|
||||
export class ToolProcessorFactory {
|
||||
static #processors = new Map();
|
||||
|
||||
/**
|
||||
* 获取工具处理器
|
||||
* @param {string} sourceFormat - 源格式
|
||||
* @param {string} targetFormat - 目标格式
|
||||
* @returns {ToolProcessor} 工具处理器实例
|
||||
*/
|
||||
static getProcessor(sourceFormat, targetFormat) {
|
||||
const key = `${sourceFormat}_to_${targetFormat}`;
|
||||
|
||||
if (!this.#processors.has(key)) {
|
||||
this.#processors.set(key, this.createProcessor(sourceFormat, targetFormat));
|
||||
}
|
||||
|
||||
return this.#processors.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工具处理器
|
||||
* @param {string} sourceFormat - 源格式
|
||||
* @param {string} targetFormat - 目标格式
|
||||
* @returns {ToolProcessor} 工具处理器实例
|
||||
*/
|
||||
static createProcessor(sourceFormat, targetFormat) {
|
||||
console.warn(`Tool processor for ${sourceFormat} to ${targetFormat} not yet implemented`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缓存的处理器
|
||||
*/
|
||||
static clearCache() {
|
||||
this.#processors.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 导出工厂类
|
||||
export default ConverterFactory;
|
||||
25
src/converters/register-converters.js
Normal file
25
src/converters/register-converters.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* 转换器注册模块
|
||||
* 用于注册所有转换器到工厂,避免循环依赖问题
|
||||
*/
|
||||
|
||||
import { MODEL_PROTOCOL_PREFIX } from '../common.js';
|
||||
import { ConverterFactory } from './ConverterFactory.js';
|
||||
import { OpenAIConverter } from './strategies/OpenAIConverter.js';
|
||||
import { OpenAIResponsesConverter } from './strategies/OpenAIResponsesConverter.js';
|
||||
import { ClaudeConverter } from './strategies/ClaudeConverter.js';
|
||||
import { GeminiConverter } from './strategies/GeminiConverter.js';
|
||||
|
||||
/**
|
||||
* 注册所有转换器到工厂
|
||||
* 此函数应在应用启动时调用一次
|
||||
*/
|
||||
export function registerAllConverters() {
|
||||
ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.OPENAI, OpenAIConverter);
|
||||
ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES, OpenAIResponsesConverter);
|
||||
ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.CLAUDE, ClaudeConverter);
|
||||
ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.GEMINI, GeminiConverter);
|
||||
}
|
||||
|
||||
// 自动注册所有转换器
|
||||
registerAllConverters();
|
||||
1290
src/converters/strategies/ClaudeConverter.js
Normal file
1290
src/converters/strategies/ClaudeConverter.js
Normal file
File diff suppressed because it is too large
Load diff
820
src/converters/strategies/GeminiConverter.js
Normal file
820
src/converters/strategies/GeminiConverter.js
Normal file
|
|
@ -0,0 +1,820 @@
|
|||
/**
|
||||
* Gemini转换器
|
||||
* 处理Gemini(Google)协议与其他协议之间的转换
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { BaseConverter } from '../BaseConverter.js';
|
||||
import {
|
||||
checkAndAssignOrDefault
|
||||
} from '../utils.js';
|
||||
import { MODEL_PROTOCOL_PREFIX } from '../../common.js';
|
||||
import {
|
||||
generateResponseCreated,
|
||||
generateResponseInProgress,
|
||||
generateOutputItemAdded,
|
||||
generateContentPartAdded,
|
||||
generateOutputTextDone,
|
||||
generateContentPartDone,
|
||||
generateOutputItemDone,
|
||||
generateResponseCompleted
|
||||
} from '../../openai/openai-responses-core.mjs';
|
||||
|
||||
/**
|
||||
* Gemini转换器类
|
||||
* 实现Gemini协议到其他协议的转换
|
||||
*/
|
||||
export class GeminiConverter extends BaseConverter {
|
||||
constructor() {
|
||||
super('gemini');
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换请求
|
||||
*/
|
||||
convertRequest(data, targetProtocol) {
|
||||
switch (targetProtocol) {
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
||||
return this.toOpenAIRequest(data);
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
return this.toClaudeRequest(data);
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
||||
return this.toOpenAIResponsesRequest(data);
|
||||
default:
|
||||
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换响应
|
||||
*/
|
||||
convertResponse(data, targetProtocol, model) {
|
||||
switch (targetProtocol) {
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
||||
return this.toOpenAIResponse(data, model);
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
return this.toClaudeResponse(data, model);
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
||||
return this.toOpenAIResponsesResponse(data, model);
|
||||
default:
|
||||
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换流式响应块
|
||||
*/
|
||||
convertStreamChunk(chunk, targetProtocol, model) {
|
||||
switch (targetProtocol) {
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
||||
return this.toOpenAIStreamChunk(chunk, model);
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
return this.toClaudeStreamChunk(chunk, model);
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
||||
return this.toOpenAIResponsesStreamChunk(chunk, model);
|
||||
default:
|
||||
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换模型列表
|
||||
*/
|
||||
convertModelList(data, targetProtocol) {
|
||||
switch (targetProtocol) {
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
||||
return this.toOpenAIModelList(data);
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
return this.toClaudeModelList(data);
|
||||
default:
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Gemini -> OpenAI 转换
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Gemini请求 -> OpenAI请求
|
||||
*/
|
||||
toOpenAIRequest(geminiRequest) {
|
||||
const openaiRequest = {
|
||||
messages: [],
|
||||
model: geminiRequest.model,
|
||||
max_tokens: checkAndAssignOrDefault(geminiRequest.max_tokens, 8192),
|
||||
temperature: checkAndAssignOrDefault(geminiRequest.temperature, 1),
|
||||
top_p: checkAndAssignOrDefault(geminiRequest.top_p, 0.95),
|
||||
};
|
||||
|
||||
// 处理系统指令
|
||||
if (geminiRequest.systemInstruction && Array.isArray(geminiRequest.systemInstruction.parts)) {
|
||||
const systemContent = this.processGeminiPartsToOpenAIContent(geminiRequest.systemInstruction.parts);
|
||||
if (systemContent) {
|
||||
openaiRequest.messages.push({
|
||||
role: 'system',
|
||||
content: systemContent
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理内容
|
||||
if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) {
|
||||
geminiRequest.contents.forEach(content => {
|
||||
if (content && Array.isArray(content.parts)) {
|
||||
const openaiContent = this.processGeminiPartsToOpenAIContent(content.parts);
|
||||
if (openaiContent && openaiContent.length > 0) {
|
||||
const openaiRole = content.role === 'model' ? 'assistant' : content.role;
|
||||
openaiRequest.messages.push({
|
||||
role: openaiRole,
|
||||
content: openaiContent
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return openaiRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini响应 -> OpenAI响应
|
||||
*/
|
||||
toOpenAIResponse(geminiResponse, model) {
|
||||
const content = this.processGeminiResponseContent(geminiResponse);
|
||||
|
||||
return {
|
||||
id: `chatcmpl-${uuidv4()}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: content
|
||||
},
|
||||
finish_reason: "stop",
|
||||
}],
|
||||
usage: geminiResponse.usageMetadata ? {
|
||||
prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0,
|
||||
completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0,
|
||||
total_tokens: geminiResponse.usageMetadata.totalTokenCount || 0,
|
||||
} : {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini流式响应 -> OpenAI流式响应
|
||||
*/
|
||||
toOpenAIStreamChunk(geminiChunk, model) {
|
||||
if (!geminiChunk) return null;
|
||||
|
||||
// 处理完整的Gemini chunk对象
|
||||
if (typeof geminiChunk === 'object' && !Array.isArray(geminiChunk)) {
|
||||
const candidate = geminiChunk.candidates?.[0];
|
||||
|
||||
// 提取文本内容
|
||||
let content = '';
|
||||
let finishReason = null;
|
||||
|
||||
if (candidate) {
|
||||
// 从parts中提取文本
|
||||
const parts = candidate.content?.parts;
|
||||
if (parts && Array.isArray(parts)) {
|
||||
content = parts
|
||||
.filter(part => part && typeof part.text === 'string')
|
||||
.map(part => part.text)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// 处理finishReason
|
||||
if (candidate.finishReason) {
|
||||
finishReason = candidate.finishReason === 'STOP' ? 'stop' :
|
||||
candidate.finishReason === 'MAX_TOKENS' ? 'length' :
|
||||
candidate.finishReason.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: `chatcmpl-${uuidv4()}`,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: content ? { content: content } : {},
|
||||
finish_reason: finishReason,
|
||||
}],
|
||||
usage: geminiChunk.usageMetadata ? {
|
||||
prompt_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
||||
completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
|
||||
total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0,
|
||||
} : {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 向后兼容:处理字符串格式
|
||||
if (typeof geminiChunk === 'string') {
|
||||
return {
|
||||
id: `chatcmpl-${uuidv4()}`,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: { content: geminiChunk },
|
||||
finish_reason: null,
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini模型列表 -> OpenAI模型列表
|
||||
*/
|
||||
toOpenAIModelList(geminiModels) {
|
||||
return {
|
||||
object: "list",
|
||||
data: geminiModels.models.map(m => ({
|
||||
id: m.name.startsWith('models/') ? m.name.substring(7) : m.name,
|
||||
object: "model",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
owned_by: "google",
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Gemini parts到OpenAI内容
|
||||
*/
|
||||
processGeminiPartsToOpenAIContent(parts) {
|
||||
if (!parts || !Array.isArray(parts)) return '';
|
||||
|
||||
const contentArray = [];
|
||||
|
||||
parts.forEach(part => {
|
||||
if (!part) return;
|
||||
|
||||
if (typeof part.text === 'string') {
|
||||
contentArray.push({
|
||||
type: 'text',
|
||||
text: part.text
|
||||
});
|
||||
}
|
||||
|
||||
if (part.inlineData) {
|
||||
const { mimeType, data } = part.inlineData;
|
||||
if (mimeType && data) {
|
||||
contentArray.push({
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: `data:${mimeType};base64,${data}`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (part.fileData) {
|
||||
const { mimeType, fileUri } = part.fileData;
|
||||
if (mimeType && fileUri) {
|
||||
if (mimeType.startsWith('image/')) {
|
||||
contentArray.push({
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: fileUri
|
||||
}
|
||||
});
|
||||
} else if (mimeType.startsWith('audio/')) {
|
||||
contentArray.push({
|
||||
type: 'text',
|
||||
text: `[Audio file: ${fileUri}]`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return contentArray.length === 1 && contentArray[0].type === 'text'
|
||||
? contentArray[0].text
|
||||
: contentArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Gemini响应内容
|
||||
*/
|
||||
processGeminiResponseContent(geminiResponse) {
|
||||
if (!geminiResponse || !geminiResponse.candidates) return '';
|
||||
|
||||
const contents = [];
|
||||
|
||||
geminiResponse.candidates.forEach(candidate => {
|
||||
if (candidate.content && candidate.content.parts) {
|
||||
candidate.content.parts.forEach(part => {
|
||||
if (part.text) {
|
||||
contents.push(part.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return contents.join('\n');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Gemini -> Claude 转换
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Gemini请求 -> Claude请求
|
||||
*/
|
||||
toClaudeRequest(geminiRequest) {
|
||||
const claudeRequest = {
|
||||
model: geminiRequest.model || 'claude-3-opus',
|
||||
messages: [],
|
||||
max_tokens: checkAndAssignOrDefault(geminiRequest.generationConfig?.maxOutputTokens, 8192),
|
||||
temperature: checkAndAssignOrDefault(geminiRequest.generationConfig?.temperature, 1),
|
||||
top_p: checkAndAssignOrDefault(geminiRequest.generationConfig?.topP, 0.95),
|
||||
};
|
||||
|
||||
// 处理系统指令
|
||||
if (geminiRequest.systemInstruction && geminiRequest.systemInstruction.parts) {
|
||||
const systemText = geminiRequest.systemInstruction.parts
|
||||
.filter(p => p.text)
|
||||
.map(p => p.text)
|
||||
.join('\n');
|
||||
if (systemText) {
|
||||
claudeRequest.system = systemText;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理内容
|
||||
if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) {
|
||||
geminiRequest.contents.forEach(content => {
|
||||
if (!content || !content.parts) return;
|
||||
|
||||
const role = content.role === 'model' ? 'assistant' : 'user';
|
||||
const claudeContent = this.processGeminiPartsToClaudeContent(content.parts);
|
||||
|
||||
if (claudeContent.length > 0) {
|
||||
claudeRequest.messages.push({
|
||||
role: role,
|
||||
content: claudeContent
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理工具
|
||||
if (geminiRequest.tools && geminiRequest.tools[0]?.functionDeclarations) {
|
||||
claudeRequest.tools = geminiRequest.tools[0].functionDeclarations.map(func => ({
|
||||
name: func.name,
|
||||
description: func.description || '',
|
||||
input_schema: func.parameters || { type: 'object', properties: {} }
|
||||
}));
|
||||
}
|
||||
|
||||
return claudeRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini响应 -> Claude响应
|
||||
*/
|
||||
toClaudeResponse(geminiResponse, model) {
|
||||
if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) {
|
||||
return {
|
||||
id: `msg_${uuidv4()}`,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [],
|
||||
model: model,
|
||||
stop_reason: "end_turn",
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: geminiResponse?.usageMetadata?.promptTokenCount || 0,
|
||||
output_tokens: geminiResponse?.usageMetadata?.candidatesTokenCount || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const candidate = geminiResponse.candidates[0];
|
||||
const content = this.processGeminiResponseToClaudeContent(geminiResponse);
|
||||
const finishReason = candidate.finishReason;
|
||||
let stopReason = "end_turn";
|
||||
|
||||
if (finishReason) {
|
||||
switch (finishReason) {
|
||||
case 'STOP':
|
||||
stopReason = 'end_turn';
|
||||
break;
|
||||
case 'MAX_TOKENS':
|
||||
stopReason = 'max_tokens';
|
||||
break;
|
||||
case 'SAFETY':
|
||||
stopReason = 'safety';
|
||||
break;
|
||||
case 'RECITATION':
|
||||
stopReason = 'recitation';
|
||||
break;
|
||||
case 'OTHER':
|
||||
stopReason = 'other';
|
||||
break;
|
||||
default:
|
||||
stopReason = 'end_turn';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: `msg_${uuidv4()}`,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: content,
|
||||
model: model,
|
||||
stop_reason: stopReason,
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
|
||||
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini流式响应 -> Claude流式响应
|
||||
*/
|
||||
toClaudeStreamChunk(geminiChunk, model) {
|
||||
if (!geminiChunk) return null;
|
||||
|
||||
// 处理完整的Gemini chunk对象
|
||||
if (typeof geminiChunk === 'object' && !Array.isArray(geminiChunk)) {
|
||||
const candidate = geminiChunk.candidates?.[0];
|
||||
|
||||
if (candidate) {
|
||||
const parts = candidate.content?.parts;
|
||||
|
||||
// 提取文本内容
|
||||
if (parts && Array.isArray(parts)) {
|
||||
const textParts = parts.filter(part => part && typeof part.text === 'string');
|
||||
if (textParts.length > 0) {
|
||||
const text = textParts.map(part => part.text).join('');
|
||||
return {
|
||||
type: "content_block_delta",
|
||||
index: 0,
|
||||
delta: {
|
||||
type: "text_delta",
|
||||
text: text
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 处理finishReason
|
||||
if (candidate.finishReason) {
|
||||
return {
|
||||
type: "message_delta",
|
||||
delta: {
|
||||
stop_reason: candidate.finishReason === 'STOP' ? 'end_turn' :
|
||||
candidate.finishReason === 'MAX_TOKENS' ? 'max_tokens' :
|
||||
candidate.finishReason.toLowerCase()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 向后兼容:处理字符串格式
|
||||
if (typeof geminiChunk === 'string') {
|
||||
return {
|
||||
type: "content_block_delta",
|
||||
index: 0,
|
||||
delta: {
|
||||
type: "text_delta",
|
||||
text: geminiChunk
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini模型列表 -> Claude模型列表
|
||||
*/
|
||||
toClaudeModelList(geminiModels) {
|
||||
return {
|
||||
models: geminiModels.models.map(m => ({
|
||||
name: m.name.startsWith('models/') ? m.name.substring(7) : m.name,
|
||||
description: "",
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Gemini parts到Claude内容
|
||||
*/
|
||||
processGeminiPartsToClaudeContent(parts) {
|
||||
if (!parts || !Array.isArray(parts)) return [];
|
||||
|
||||
const content = [];
|
||||
|
||||
parts.forEach(part => {
|
||||
if (!part) return;
|
||||
|
||||
if (part.text) {
|
||||
content.push({
|
||||
type: 'text',
|
||||
text: part.text
|
||||
});
|
||||
}
|
||||
|
||||
if (part.inlineData) {
|
||||
content.push({
|
||||
type: 'image',
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: part.inlineData.mimeType,
|
||||
data: part.inlineData.data
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (part.functionCall) {
|
||||
content.push({
|
||||
type: 'tool_use',
|
||||
id: uuidv4(),
|
||||
name: part.functionCall.name,
|
||||
input: part.functionCall.args || {}
|
||||
});
|
||||
}
|
||||
|
||||
if (part.functionResponse) {
|
||||
content.push({
|
||||
type: 'tool_result',
|
||||
tool_use_id: part.functionResponse.name,
|
||||
content: part.functionResponse.response
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Gemini响应到Claude内容
|
||||
*/
|
||||
processGeminiResponseToClaudeContent(geminiResponse) {
|
||||
if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) return [];
|
||||
|
||||
const content = [];
|
||||
|
||||
for (const candidate of geminiResponse.candidates) {
|
||||
if (candidate.finishReason && candidate.finishReason !== 'STOP') {
|
||||
if (candidate.finishMessage) {
|
||||
content.push({
|
||||
type: 'text',
|
||||
text: `Error: ${candidate.finishMessage}`
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (candidate.content && candidate.content.parts) {
|
||||
for (const part of candidate.content.parts) {
|
||||
if (part.text) {
|
||||
content.push({
|
||||
type: 'text',
|
||||
text: part.text
|
||||
});
|
||||
} else if (part.inlineData) {
|
||||
content.push({
|
||||
type: 'image',
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: part.inlineData.mimeType,
|
||||
data: part.inlineData.data
|
||||
}
|
||||
});
|
||||
} else if (part.functionCall) {
|
||||
content.push({
|
||||
type: 'tool_use',
|
||||
id: uuidv4(),
|
||||
name: part.functionCall.name,
|
||||
input: part.functionCall.args || {}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Gemini -> OpenAI Responses 转换
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Gemini请求 -> OpenAI Responses请求
|
||||
*/
|
||||
toOpenAIResponsesRequest(geminiRequest) {
|
||||
const responsesRequest = {
|
||||
model: geminiRequest.model,
|
||||
max_tokens: checkAndAssignOrDefault(geminiRequest.generationConfig?.maxOutputTokens, 8192),
|
||||
temperature: checkAndAssignOrDefault(geminiRequest.generationConfig?.temperature, 1),
|
||||
top_p: checkAndAssignOrDefault(geminiRequest.generationConfig?.topP, 0.95),
|
||||
};
|
||||
|
||||
// 处理系统指令
|
||||
if (geminiRequest.systemInstruction && geminiRequest.systemInstruction.parts) {
|
||||
const instructionsText = geminiRequest.systemInstruction.parts
|
||||
.filter(p => p.text)
|
||||
.map(p => p.text)
|
||||
.join('\n');
|
||||
if (instructionsText) {
|
||||
responsesRequest.instructions = instructionsText;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理输入
|
||||
if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) {
|
||||
const lastContent = geminiRequest.contents[geminiRequest.contents.length - 1];
|
||||
if (lastContent && lastContent.parts) {
|
||||
const inputText = lastContent.parts
|
||||
.filter(p => p.text)
|
||||
.map(p => p.text)
|
||||
.join(' ');
|
||||
if (inputText) {
|
||||
responsesRequest.input = inputText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return responsesRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini响应 -> OpenAI Responses响应
|
||||
*/
|
||||
toOpenAIResponsesResponse(geminiResponse, model) {
|
||||
const content = this.processGeminiResponseContent(geminiResponse);
|
||||
const textContent = typeof content === 'string' ? content : JSON.stringify(content);
|
||||
|
||||
let output = [];
|
||||
output.push({
|
||||
id: `msg_${uuidv4().replace(/-/g, '')}`,
|
||||
summary: [],
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{
|
||||
annotations: [],
|
||||
logprobs: [],
|
||||
text: textContent,
|
||||
type: "output_text"
|
||||
}]
|
||||
});
|
||||
|
||||
return {
|
||||
background: false,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
error: null,
|
||||
id: `resp_${uuidv4().replace(/-/g, '')}`,
|
||||
incomplete_details: null,
|
||||
max_output_tokens: null,
|
||||
max_tool_calls: null,
|
||||
metadata: {},
|
||||
model: model,
|
||||
object: "response",
|
||||
output: output,
|
||||
parallel_tool_calls: true,
|
||||
previous_response_id: null,
|
||||
prompt_cache_key: null,
|
||||
reasoning: {},
|
||||
safety_identifier: "user-" + uuidv4().replace(/-/g, ''),
|
||||
service_tier: "default",
|
||||
status: "completed",
|
||||
store: false,
|
||||
temperature: 1,
|
||||
text: {
|
||||
format: { type: "text" },
|
||||
},
|
||||
tool_choice: "auto",
|
||||
tools: [],
|
||||
top_logprobs: 0,
|
||||
top_p: 1,
|
||||
truncation: "disabled",
|
||||
usage: {
|
||||
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
|
||||
input_tokens_details: {
|
||||
cached_tokens: geminiResponse.usageMetadata?.cachedTokens || 0,
|
||||
},
|
||||
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0,
|
||||
output_tokens_details: {
|
||||
reasoning_tokens: 0
|
||||
},
|
||||
total_tokens: geminiResponse.usageMetadata?.totalTokenCount || 0,
|
||||
},
|
||||
user: null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini流式响应 -> OpenAI Responses流式响应
|
||||
*/
|
||||
toOpenAIResponsesStreamChunk(geminiChunk, model, requestId = null) {
|
||||
if (!geminiChunk) return [];
|
||||
|
||||
const responseId = requestId || `resp_${uuidv4().replace(/-/g, '')}`;
|
||||
const events = [];
|
||||
|
||||
// 处理完整的Gemini chunk对象
|
||||
if (typeof geminiChunk === 'object' && !Array.isArray(geminiChunk)) {
|
||||
const candidate = geminiChunk.candidates?.[0];
|
||||
|
||||
if (candidate) {
|
||||
const parts = candidate.content?.parts;
|
||||
|
||||
// 第一个chunk - 检测是否是开始(有role)
|
||||
if (candidate.content?.role === 'model' && parts && parts.length > 0) {
|
||||
// 只在第一次有内容时发送开始事件
|
||||
const hasContent = parts.some(part => part && typeof part.text === 'string' && part.text.length > 0);
|
||||
if (hasContent) {
|
||||
events.push(
|
||||
generateResponseCreated(responseId, model || 'unknown'),
|
||||
generateResponseInProgress(responseId),
|
||||
generateOutputItemAdded(responseId),
|
||||
generateContentPartAdded(responseId)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 提取文本内容
|
||||
if (parts && Array.isArray(parts)) {
|
||||
const textParts = parts.filter(part => part && typeof part.text === 'string');
|
||||
if (textParts.length > 0) {
|
||||
const text = textParts.map(part => part.text).join('');
|
||||
events.push({
|
||||
delta: text,
|
||||
item_id: `msg_${uuidv4().replace(/-/g, '')}`,
|
||||
output_index: 0,
|
||||
sequence_number: 3,
|
||||
type: "response.output_text.delta"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理finishReason
|
||||
if (candidate.finishReason) {
|
||||
events.push(
|
||||
generateOutputTextDone(responseId),
|
||||
generateContentPartDone(responseId),
|
||||
generateOutputItemDone(responseId),
|
||||
generateResponseCompleted(responseId)
|
||||
);
|
||||
|
||||
// 如果有 usage 信息,更新最后一个事件
|
||||
if (geminiChunk.usageMetadata && events.length > 0) {
|
||||
const lastEvent = events[events.length - 1];
|
||||
if (lastEvent.response) {
|
||||
lastEvent.response.usage = {
|
||||
input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
||||
output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
|
||||
total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 向后兼容:处理字符串格式
|
||||
if (typeof geminiChunk === 'string') {
|
||||
events.push({
|
||||
delta: geminiChunk,
|
||||
item_id: `msg_${uuidv4().replace(/-/g, '')}`,
|
||||
output_index: 0,
|
||||
sequence_number: 3,
|
||||
type: "response.output_text.delta"
|
||||
});
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
}
|
||||
|
||||
export default GeminiConverter;
|
||||
997
src/converters/strategies/OpenAIConverter.js
Normal file
997
src/converters/strategies/OpenAIConverter.js
Normal file
|
|
@ -0,0 +1,997 @@
|
|||
/**
|
||||
* OpenAI转换器
|
||||
* 处理OpenAI协议与其他协议之间的转换
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { BaseConverter } from '../BaseConverter.js';
|
||||
import {
|
||||
extractAndProcessSystemMessages as extractSystemMessages,
|
||||
extractTextFromMessageContent as extractText,
|
||||
safeParseJSON,
|
||||
checkAndAssignOrDefault,
|
||||
extractThinkingFromOpenAIText,
|
||||
mapFinishReason,
|
||||
cleanJsonSchemaProperties as cleanJsonSchema
|
||||
} from '../utils.js';
|
||||
import { MODEL_PROTOCOL_PREFIX } from '../../common.js';
|
||||
import {
|
||||
generateResponseCreated,
|
||||
generateResponseInProgress,
|
||||
generateOutputItemAdded,
|
||||
generateContentPartAdded,
|
||||
generateOutputTextDone,
|
||||
generateContentPartDone,
|
||||
generateOutputItemDone,
|
||||
generateResponseCompleted
|
||||
} from '../../openai/openai-responses-core.mjs';
|
||||
|
||||
/**
|
||||
* OpenAI转换器类
|
||||
* 实现OpenAI协议到其他协议的转换
|
||||
*/
|
||||
export class OpenAIConverter extends BaseConverter {
|
||||
constructor() {
|
||||
super('openai');
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换请求
|
||||
*/
|
||||
convertRequest(data, targetProtocol) {
|
||||
switch (targetProtocol) {
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
return this.toClaudeRequest(data);
|
||||
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
||||
return this.toGeminiRequest(data);
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
||||
return this.toOpenAIResponsesRequest(data);
|
||||
default:
|
||||
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换响应
|
||||
*/
|
||||
convertResponse(data, targetProtocol, model) {
|
||||
// OpenAI作为源格式时,通常不需要转换响应
|
||||
// 因为其他协议会转换到OpenAI格式
|
||||
switch (targetProtocol) {
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
return this.toClaudeResponse(data, model);
|
||||
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
||||
return this.toGeminiResponse(data, model);
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
||||
return this.toOpenAIResponsesResponse(data, model);
|
||||
default:
|
||||
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换流式响应块
|
||||
*/
|
||||
convertStreamChunk(chunk, targetProtocol, model) {
|
||||
switch (targetProtocol) {
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
return this.toClaudeStreamChunk(chunk, model);
|
||||
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
||||
return this.toGeminiStreamChunk(chunk, model);
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
||||
return this.toOpenAIResponsesStreamChunk(chunk, model);
|
||||
default:
|
||||
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换模型列表
|
||||
*/
|
||||
convertModelList(data, targetProtocol) {
|
||||
switch (targetProtocol) {
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
return this.toClaudeModelList(data);
|
||||
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
||||
return this.toGeminiModelList(data);
|
||||
default:
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// OpenAI -> Claude 转换
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* OpenAI请求 -> Claude请求
|
||||
*/
|
||||
toClaudeRequest(openaiRequest) {
|
||||
const messages = openaiRequest.messages || [];
|
||||
const { systemInstruction, nonSystemMessages } = extractSystemMessages(messages);
|
||||
|
||||
const claudeMessages = [];
|
||||
|
||||
for (const message of nonSystemMessages) {
|
||||
const role = message.role === 'assistant' ? 'assistant' : 'user';
|
||||
let content = [];
|
||||
|
||||
if (message.role === 'tool') {
|
||||
// 工具结果消息
|
||||
content.push({
|
||||
type: 'tool_result',
|
||||
tool_use_id: message.tool_call_id,
|
||||
content: safeParseJSON(message.content)
|
||||
});
|
||||
claudeMessages.push({ role: 'user', content: content });
|
||||
} else if (message.role === 'assistant' && (message.tool_calls?.length || message.function_calls?.length)) {
|
||||
// 助手工具调用消息 - 支持tool_calls和function_calls
|
||||
const calls = message.tool_calls || message.function_calls || [];
|
||||
const toolUseBlocks = calls.map(tc => ({
|
||||
type: 'tool_use',
|
||||
id: tc.id,
|
||||
name: tc.function.name,
|
||||
input: safeParseJSON(tc.function.arguments)
|
||||
}));
|
||||
claudeMessages.push({ role: 'assistant', content: toolUseBlocks });
|
||||
} else {
|
||||
// 普通消息
|
||||
if (typeof message.content === 'string') {
|
||||
if (message.content) {
|
||||
content.push({ type: 'text', text: message.content });
|
||||
}
|
||||
} else if (Array.isArray(message.content)) {
|
||||
message.content.forEach(item => {
|
||||
if (!item) return;
|
||||
switch (item.type) {
|
||||
case 'text':
|
||||
if (item.text) {
|
||||
content.push({ type: 'text', text: item.text });
|
||||
}
|
||||
break;
|
||||
case 'image_url':
|
||||
if (item.image_url) {
|
||||
const imageUrl = typeof item.image_url === 'string'
|
||||
? item.image_url
|
||||
: item.image_url.url;
|
||||
if (imageUrl.startsWith('data:')) {
|
||||
const [header, data] = imageUrl.split(',');
|
||||
const mediaType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg';
|
||||
content.push({
|
||||
type: 'image',
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: mediaType,
|
||||
data: data
|
||||
}
|
||||
});
|
||||
} else {
|
||||
content.push({ type: 'text', text: `[Image: ${imageUrl}]` });
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'audio':
|
||||
if (item.audio_url) {
|
||||
const audioUrl = typeof item.audio_url === 'string'
|
||||
? item.audio_url
|
||||
: item.audio_url.url;
|
||||
content.push({ type: 'text', text: `[Audio: ${audioUrl}]` });
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (content.length > 0) {
|
||||
claudeMessages.push({ role: role, content: content });
|
||||
}
|
||||
}
|
||||
}
|
||||
// 合并相邻相同 role 的消息
|
||||
const mergedClaudeMessages = [];
|
||||
for (let i = 0; i < claudeMessages.length; i++) {
|
||||
const currentMessage = claudeMessages[i];
|
||||
|
||||
if (mergedClaudeMessages.length === 0) {
|
||||
mergedClaudeMessages.push(currentMessage);
|
||||
} else {
|
||||
const lastMessage = mergedClaudeMessages[mergedClaudeMessages.length - 1];
|
||||
|
||||
// 如果当前消息的 role 与上一条消息的 role 相同,则合并 content 数组
|
||||
if (lastMessage.role === currentMessage.role) {
|
||||
lastMessage.content = lastMessage.content.concat(currentMessage.content);
|
||||
} else {
|
||||
mergedClaudeMessages.push(currentMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const claudeRequest = {
|
||||
model: openaiRequest.model,
|
||||
messages: mergedClaudeMessages,
|
||||
max_tokens: checkAndAssignOrDefault(openaiRequest.max_tokens, 8192),
|
||||
temperature: checkAndAssignOrDefault(openaiRequest.temperature, 1),
|
||||
top_p: checkAndAssignOrDefault(openaiRequest.top_p, 0.95),
|
||||
};
|
||||
|
||||
if (systemInstruction) {
|
||||
claudeRequest.system = extractText(systemInstruction.parts[0].text);
|
||||
}
|
||||
|
||||
if (openaiRequest.tools?.length) {
|
||||
claudeRequest.tools = openaiRequest.tools.map(t => ({
|
||||
name: t.function.name,
|
||||
description: t.function.description || '',
|
||||
input_schema: t.function.parameters || { type: 'object', properties: {} }
|
||||
}));
|
||||
claudeRequest.tool_choice = this.buildClaudeToolChoice(openaiRequest.tool_choice);
|
||||
}
|
||||
|
||||
return claudeRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI响应 -> Claude响应
|
||||
*/
|
||||
toClaudeResponse(openaiResponse, model) {
|
||||
if (!openaiResponse || !openaiResponse.choices || openaiResponse.choices.length === 0) {
|
||||
return {
|
||||
id: `msg_${uuidv4()}`,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [],
|
||||
model: model,
|
||||
stop_reason: "end_turn",
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: openaiResponse?.usage?.prompt_tokens || 0,
|
||||
output_tokens: openaiResponse?.usage?.completion_tokens || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const choice = openaiResponse.choices[0];
|
||||
const contentList = [];
|
||||
|
||||
// 处理工具调用 - 支持tool_calls和function_calls
|
||||
const toolCalls = choice.message?.tool_calls || choice.message?.function_calls || [];
|
||||
for (const toolCall of toolCalls.filter(tc => tc && typeof tc === 'object')) {
|
||||
if (toolCall.function) {
|
||||
const func = toolCall.function;
|
||||
const argStr = func.arguments || "{}";
|
||||
let argObj;
|
||||
try {
|
||||
argObj = typeof argStr === 'string' ? JSON.parse(argStr) : argStr;
|
||||
} catch (e) {
|
||||
argObj = {};
|
||||
}
|
||||
contentList.push({
|
||||
type: "tool_use",
|
||||
id: toolCall.id || "",
|
||||
name: func.name || "",
|
||||
input: argObj,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理reasoning_content(推理内容)
|
||||
const reasoningContent = choice.message?.reasoning_content || "";
|
||||
if (reasoningContent) {
|
||||
contentList.push({
|
||||
type: "thinking",
|
||||
thinking: reasoningContent
|
||||
});
|
||||
}
|
||||
|
||||
// 处理文本内容
|
||||
const contentText = choice.message?.content || "";
|
||||
if (contentText) {
|
||||
const extractedContent = extractThinkingFromOpenAIText(contentText);
|
||||
if (Array.isArray(extractedContent)) {
|
||||
contentList.push(...extractedContent);
|
||||
} else {
|
||||
contentList.push({ type: "text", text: extractedContent });
|
||||
}
|
||||
}
|
||||
|
||||
// 映射结束原因
|
||||
const stopReason = mapFinishReason(
|
||||
choice.finish_reason || "stop",
|
||||
"openai",
|
||||
"anthropic"
|
||||
);
|
||||
|
||||
return {
|
||||
id: `msg_${uuidv4()}`,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: contentList,
|
||||
model: model,
|
||||
stop_reason: stopReason,
|
||||
stop_sequence: null,
|
||||
usage: {
|
||||
input_tokens: openaiResponse.usage?.prompt_tokens || 0,
|
||||
output_tokens: openaiResponse.usage?.completion_tokens || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI流式响应 -> Claude流式响应
|
||||
*
|
||||
* 这个方法实现了与 ClaudeConverter.toOpenAIStreamChunk 相反的转换逻辑
|
||||
* 将 OpenAI 的流式 chunk 转换为 Claude 的流式事件
|
||||
*/
|
||||
toClaudeStreamChunk(openaiChunk, model) {
|
||||
if (!openaiChunk) return null;
|
||||
|
||||
// 处理 OpenAI chunk 对象
|
||||
if (typeof openaiChunk === 'object' && !Array.isArray(openaiChunk)) {
|
||||
const choice = openaiChunk.choices?.[0];
|
||||
if (!choice){
|
||||
return null;
|
||||
}
|
||||
|
||||
const delta = choice.delta;
|
||||
const finishReason = choice.finish_reason;
|
||||
const events = [];
|
||||
|
||||
// 注释部分是为了兼容claude code,但是不兼容cherry studio
|
||||
// 1. 处理 role (对应 message_start)
|
||||
// if (delta?.role === "assistant") {
|
||||
// events.push({
|
||||
// type: "message_start",
|
||||
// message: {
|
||||
// id: openaiChunk.id || `msg_${uuidv4()}`,
|
||||
// type: "message",
|
||||
// role: "assistant",
|
||||
// content: [],
|
||||
// model: model || openaiChunk.model || "unknown",
|
||||
// stop_reason: null,
|
||||
// stop_sequence: null,
|
||||
// usage: {
|
||||
// input_tokens: openaiChunk.usage?.prompt_tokens || 0,
|
||||
// output_tokens: 0
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// events.push({
|
||||
// type: "content_block_start",
|
||||
// index: 0,
|
||||
// content_block: {
|
||||
// type: "text",
|
||||
// text: ""
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// 2. 处理 tool_calls (对应 content_block_start 和 content_block_delta)
|
||||
// if (delta?.tool_calls) {
|
||||
// const toolCalls = delta.tool_calls;
|
||||
// for (const toolCall of toolCalls) {
|
||||
// // 如果有 function.name,说明是工具调用开始
|
||||
// if (toolCall.function?.name) {
|
||||
// events.push({
|
||||
// type: "content_block_start",
|
||||
// index: toolCall.index || 0,
|
||||
// content_block: {
|
||||
// type: "tool_use",
|
||||
// id: toolCall.id || `tool_${uuidv4()}`,
|
||||
// name: toolCall.function.name,
|
||||
// input: {}
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// // 如果有 function.arguments,说明是参数增量
|
||||
// if (toolCall.function?.arguments) {
|
||||
// events.push({
|
||||
// type: "content_block_delta",
|
||||
// index: toolCall.index || 0,
|
||||
// delta: {
|
||||
// type: "input_json_delta",
|
||||
// partial_json: toolCall.function.arguments
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// 3. 处理 reasoning_content (对应 thinking 类型的 content_block)
|
||||
if (delta?.reasoning_content) {
|
||||
// 注意:这里可能需要先发送 content_block_start,但由于状态管理复杂,
|
||||
// 我们假设调用方会处理这个逻辑
|
||||
events.push({
|
||||
type: "content_block_delta",
|
||||
index: 0,
|
||||
delta: {
|
||||
type: "thinking_delta",
|
||||
thinking: delta.reasoning_content
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 处理普通文本 content (对应 text 类型的 content_block)
|
||||
if (delta?.content) {
|
||||
events.push({
|
||||
type: "content_block_delta",
|
||||
index: 0,
|
||||
delta: {
|
||||
type: "text_delta",
|
||||
text: delta.content
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 5. 处理 finish_reason (对应 message_delta 和 message_stop)
|
||||
if (finishReason) {
|
||||
// 映射 finish_reason
|
||||
const stopReason = finishReason === "stop" ? "end_turn" :
|
||||
finishReason === "length" ? "max_tokens" :
|
||||
"end_turn";
|
||||
|
||||
events.push({
|
||||
type: "content_block_stop",
|
||||
index: 0
|
||||
});
|
||||
// 发送 message_delta
|
||||
events.push({
|
||||
type: "message_delta",
|
||||
delta: {
|
||||
stop_reason: stopReason,
|
||||
stop_sequence: null
|
||||
},
|
||||
usage: {
|
||||
output_tokens: openaiChunk.usage?.completion_tokens || 0,
|
||||
input_tokens: openaiChunk.usage?.prompt_tokens || 0,
|
||||
}
|
||||
});
|
||||
|
||||
// 发送 message_stop
|
||||
events.push({
|
||||
type: "message_stop"
|
||||
});
|
||||
}
|
||||
|
||||
return events.length > 0 ? events : null;
|
||||
}
|
||||
|
||||
// 向后兼容:处理字符串格式
|
||||
if (typeof openaiChunk === 'string') {
|
||||
return {
|
||||
type: "content_block_delta",
|
||||
index: 0,
|
||||
delta: {
|
||||
type: "text_delta",
|
||||
text: openaiChunk
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI模型列表 -> Claude模型列表
|
||||
*/
|
||||
toClaudeModelList(openaiModels) {
|
||||
return {
|
||||
models: openaiModels.data.map(m => ({
|
||||
name: m.id,
|
||||
description: "",
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OpenAI 模型列表转换为 Gemini 模型列表
|
||||
*/
|
||||
toGeminiModelList(openaiModels) {
|
||||
const models = openaiModels.data || [];
|
||||
return {
|
||||
models: models.map(m => ({
|
||||
name: `models/${m.id}`,
|
||||
version: m.version || "1.0.0",
|
||||
displayName: m.displayName || m.id,
|
||||
description: m.description || `A generative model for text and chat generation. ID: ${m.id}`,
|
||||
inputTokenLimit: m.inputTokenLimit || 32768,
|
||||
outputTokenLimit: m.outputTokenLimit || 8192,
|
||||
supportedGenerationMethods: m.supportedGenerationMethods || ["generateContent", "streamGenerateContent"]
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建Claude工具选择
|
||||
*/
|
||||
buildClaudeToolChoice(toolChoice) {
|
||||
if (typeof toolChoice === 'string') {
|
||||
const mapping = { auto: 'auto', none: 'none', required: 'any' };
|
||||
return { type: mapping[toolChoice] };
|
||||
}
|
||||
if (typeof toolChoice === 'object' && toolChoice.function) {
|
||||
return { type: 'tool', name: toolChoice.function.name };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// OpenAI -> Gemini 转换
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* OpenAI请求 -> Gemini请求
|
||||
*/
|
||||
toGeminiRequest(openaiRequest) {
|
||||
const messages = openaiRequest.messages || [];
|
||||
const { systemInstruction, nonSystemMessages } = extractSystemMessages(messages);
|
||||
|
||||
const processedMessages = [];
|
||||
let lastMessage = null;
|
||||
|
||||
for (const message of nonSystemMessages) {
|
||||
const geminiRole = message.role === 'assistant' ? 'model' : message.role;
|
||||
|
||||
if (geminiRole === 'tool') {
|
||||
if (lastMessage) processedMessages.push(lastMessage);
|
||||
processedMessages.push({
|
||||
role: 'function',
|
||||
parts: [{
|
||||
functionResponse: {
|
||||
name: message.name,
|
||||
response: { content: safeParseJSON(message.content) }
|
||||
}
|
||||
}]
|
||||
});
|
||||
lastMessage = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
const processedContent = this.processOpenAIContentToGeminiParts(message.content);
|
||||
|
||||
if (lastMessage && lastMessage.role === geminiRole && !message.tool_calls &&
|
||||
Array.isArray(processedContent) && processedContent.every(p => p.text) &&
|
||||
Array.isArray(lastMessage.parts) && lastMessage.parts.every(p => p.text)) {
|
||||
lastMessage.parts.push(...processedContent);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastMessage) processedMessages.push(lastMessage);
|
||||
lastMessage = { role: geminiRole, parts: processedContent };
|
||||
}
|
||||
if (lastMessage) processedMessages.push(lastMessage);
|
||||
|
||||
const geminiRequest = {
|
||||
contents: processedMessages.filter(item => item.parts && item.parts.length > 0)
|
||||
};
|
||||
|
||||
if (systemInstruction) geminiRequest.systemInstruction = systemInstruction;
|
||||
|
||||
if (openaiRequest.tools?.length) {
|
||||
geminiRequest.tools = [{
|
||||
functionDeclarations: openaiRequest.tools.map(t => {
|
||||
if (!t || typeof t !== 'object' || !t.function) return null;
|
||||
const func = t.function;
|
||||
const parameters = cleanJsonSchema(func.parameters || {});
|
||||
return {
|
||||
name: String(func.name || ''),
|
||||
description: String(func.description || ''),
|
||||
parameters: parameters
|
||||
};
|
||||
}).filter(Boolean)
|
||||
}];
|
||||
if (geminiRequest.tools[0].functionDeclarations.length === 0) {
|
||||
delete geminiRequest.tools;
|
||||
}
|
||||
}
|
||||
|
||||
if (openaiRequest.tool_choice) {
|
||||
geminiRequest.toolConfig = this.buildGeminiToolConfig(openaiRequest.tool_choice);
|
||||
}
|
||||
|
||||
const config = this.buildGeminiGenerationConfig(openaiRequest);
|
||||
if (Object.keys(config).length) geminiRequest.generationConfig = config;
|
||||
|
||||
return geminiRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理OpenAI内容到Gemini parts
|
||||
*/
|
||||
processOpenAIContentToGeminiParts(content) {
|
||||
if (!content) return [];
|
||||
if (typeof content === 'string') return [{ text: content }];
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
const parts = [];
|
||||
|
||||
for (const item of content) {
|
||||
if (!item) continue;
|
||||
|
||||
if (item.type === 'text' && item.text) {
|
||||
parts.push({ text: item.text });
|
||||
} else if (item.type === 'image_url' && item.image_url) {
|
||||
const imageUrl = typeof item.image_url === 'string'
|
||||
? item.image_url
|
||||
: item.image_url.url;
|
||||
|
||||
if (imageUrl.startsWith('data:')) {
|
||||
const [header, data] = imageUrl.split(',');
|
||||
const mimeType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg';
|
||||
parts.push({ inlineData: { mimeType, data } });
|
||||
} else {
|
||||
parts.push({
|
||||
fileData: { mimeType: 'image/jpeg', fileUri: imageUrl }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建Gemini工具配置
|
||||
*/
|
||||
buildGeminiToolConfig(toolChoice) {
|
||||
if (typeof toolChoice === 'string' && ['none', 'auto'].includes(toolChoice)) {
|
||||
return { functionCallingConfig: { mode: toolChoice.toUpperCase() } };
|
||||
}
|
||||
if (typeof toolChoice === 'object' && toolChoice.function) {
|
||||
return { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [toolChoice.function.name] } };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建Gemini生成配置
|
||||
*/
|
||||
buildGeminiGenerationConfig({ temperature, max_tokens, top_p, stop }) {
|
||||
const config = {};
|
||||
config.temperature = checkAndAssignOrDefault(temperature, 1);
|
||||
config.maxOutputTokens = checkAndAssignOrDefault(max_tokens, 65535);
|
||||
config.topP = checkAndAssignOrDefault(top_p, 0.95);
|
||||
if (stop !== undefined) config.stopSequences = Array.isArray(stop) ? stop : [stop];
|
||||
return config;
|
||||
}
|
||||
/**
|
||||
* 将OpenAI响应转换为Gemini响应格式
|
||||
*/
|
||||
toGeminiResponse(openaiResponse, model) {
|
||||
if (!openaiResponse || !openaiResponse.choices || !openaiResponse.choices[0]) {
|
||||
return { candidates: [], usageMetadata: {} };
|
||||
}
|
||||
|
||||
const choice = openaiResponse.choices[0];
|
||||
const message = choice.message || {};
|
||||
const parts = [];
|
||||
|
||||
// 处理文本内容
|
||||
if (message.content) {
|
||||
parts.push({ text: message.content });
|
||||
}
|
||||
|
||||
// 处理工具调用
|
||||
if (message.tool_calls && message.tool_calls.length > 0) {
|
||||
for (const toolCall of message.tool_calls) {
|
||||
if (toolCall.type === 'function') {
|
||||
parts.push({
|
||||
functionCall: {
|
||||
name: toolCall.function.name,
|
||||
args: typeof toolCall.function.arguments === 'string'
|
||||
? JSON.parse(toolCall.function.arguments)
|
||||
: toolCall.function.arguments
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 映射finish_reason
|
||||
const finishReasonMap = {
|
||||
'stop': 'STOP',
|
||||
'length': 'MAX_TOKENS',
|
||||
'tool_calls': 'STOP',
|
||||
'content_filter': 'SAFETY'
|
||||
};
|
||||
|
||||
return {
|
||||
candidates: [{
|
||||
content: {
|
||||
role: 'model',
|
||||
parts: parts
|
||||
},
|
||||
finishReason: finishReasonMap[choice.finish_reason] || 'STOP'
|
||||
}],
|
||||
usageMetadata: openaiResponse.usage ? {
|
||||
promptTokenCount: openaiResponse.usage.prompt_tokens || 0,
|
||||
candidatesTokenCount: openaiResponse.usage.completion_tokens || 0,
|
||||
totalTokenCount: openaiResponse.usage.total_tokens || 0
|
||||
} : {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将OpenAI流式响应块转换为Gemini流式响应格式
|
||||
*/
|
||||
toGeminiStreamChunk(openaiChunk, model) {
|
||||
if (!openaiChunk || !openaiChunk.choices || !openaiChunk.choices[0]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const choice = openaiChunk.choices[0];
|
||||
const delta = choice.delta || {};
|
||||
const parts = [];
|
||||
|
||||
// 处理文本内容
|
||||
if (delta.content) {
|
||||
parts.push({ text: delta.content });
|
||||
}
|
||||
|
||||
// 处理工具调用
|
||||
if (delta.tool_calls && delta.tool_calls.length > 0) {
|
||||
for (const toolCall of delta.tool_calls) {
|
||||
if (toolCall.function) {
|
||||
const functionCall = {
|
||||
name: toolCall.function.name || '',
|
||||
args: {}
|
||||
};
|
||||
|
||||
if (toolCall.function.arguments) {
|
||||
try {
|
||||
functionCall.args = typeof toolCall.function.arguments === 'string'
|
||||
? JSON.parse(toolCall.function.arguments)
|
||||
: toolCall.function.arguments;
|
||||
} catch (e) {
|
||||
// 部分参数,保持为字符串
|
||||
functionCall.args = { partial: toolCall.function.arguments };
|
||||
}
|
||||
}
|
||||
|
||||
parts.push({ functionCall });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
candidates: [{
|
||||
content: {
|
||||
role: 'model',
|
||||
parts: parts
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
// 添加finish_reason(如果存在)
|
||||
if (choice.finish_reason) {
|
||||
const finishReasonMap = {
|
||||
'stop': 'STOP',
|
||||
'length': 'MAX_TOKENS',
|
||||
'tool_calls': 'STOP',
|
||||
'content_filter': 'SAFETY'
|
||||
};
|
||||
result.candidates[0].finishReason = finishReasonMap[choice.finish_reason] || 'STOP';
|
||||
}
|
||||
|
||||
// 添加usage信息(如果存在)
|
||||
if (openaiChunk.usage) {
|
||||
result.usageMetadata = {
|
||||
promptTokenCount: openaiChunk.usage.prompt_tokens || 0,
|
||||
candidatesTokenCount: openaiChunk.usage.completion_tokens || 0,
|
||||
totalTokenCount: openaiChunk.usage.total_tokens || 0
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将OpenAI请求转换为OpenAI Responses格式
|
||||
*/
|
||||
toOpenAIResponsesRequest(openaiRequest) {
|
||||
const responsesRequest = {
|
||||
model: openaiRequest.model,
|
||||
messages: []
|
||||
};
|
||||
|
||||
// 转换messages
|
||||
if (openaiRequest.messages && openaiRequest.messages.length > 0) {
|
||||
responsesRequest.messages = openaiRequest.messages.map(msg => ({
|
||||
role: msg.role,
|
||||
content: typeof msg.content === 'string'
|
||||
? [{ type: 'input_text', text: msg.content }]
|
||||
: msg.content
|
||||
}));
|
||||
}
|
||||
|
||||
// 转换其他参数
|
||||
if (openaiRequest.temperature !== undefined) {
|
||||
responsesRequest.temperature = openaiRequest.temperature;
|
||||
}
|
||||
if (openaiRequest.max_tokens !== undefined) {
|
||||
responsesRequest.max_output_tokens = openaiRequest.max_tokens;
|
||||
}
|
||||
if (openaiRequest.top_p !== undefined) {
|
||||
responsesRequest.top_p = openaiRequest.top_p;
|
||||
}
|
||||
if (openaiRequest.tools) {
|
||||
responsesRequest.tools = openaiRequest.tools;
|
||||
}
|
||||
if (openaiRequest.tool_choice) {
|
||||
responsesRequest.tool_choice = openaiRequest.tool_choice;
|
||||
}
|
||||
|
||||
return responsesRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将OpenAI响应转换为OpenAI Responses格式
|
||||
*/
|
||||
toOpenAIResponsesResponse(openaiResponse, model) {
|
||||
if (!openaiResponse || !openaiResponse.choices || !openaiResponse.choices[0]) {
|
||||
return {
|
||||
id: `resp_${Date.now()}`,
|
||||
object: 'response',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
status: 'completed',
|
||||
model: model || 'unknown',
|
||||
output: [],
|
||||
usage: {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
total_tokens: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const choice = openaiResponse.choices[0];
|
||||
const message = choice.message || {};
|
||||
const output = [];
|
||||
|
||||
// 构建message输出
|
||||
const messageContent = [];
|
||||
if (message.content) {
|
||||
messageContent.push({
|
||||
type: 'output_text',
|
||||
text: message.content
|
||||
});
|
||||
}
|
||||
|
||||
output.push({
|
||||
type: 'message',
|
||||
id: `msg_${Date.now()}`,
|
||||
status: 'completed',
|
||||
role: 'assistant',
|
||||
content: messageContent
|
||||
});
|
||||
|
||||
return {
|
||||
id: openaiResponse.id || `resp_${Date.now()}`,
|
||||
object: 'response',
|
||||
created_at: openaiResponse.created || Math.floor(Date.now() / 1000),
|
||||
status: choice.finish_reason === 'stop' ? 'completed' : 'in_progress',
|
||||
model: model || openaiResponse.model || 'unknown',
|
||||
output: output,
|
||||
usage: openaiResponse.usage ? {
|
||||
input_tokens: openaiResponse.usage.prompt_tokens || 0,
|
||||
output_tokens: openaiResponse.usage.completion_tokens || 0,
|
||||
total_tokens: openaiResponse.usage.total_tokens || 0
|
||||
} : {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
total_tokens: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将OpenAI流式响应转换为OpenAI Responses流式格式
|
||||
* 参考 ClaudeConverter.toOpenAIResponsesStreamChunk 的实现逻辑
|
||||
*/
|
||||
toOpenAIResponsesStreamChunk(openaiChunk, model, requestId = null) {
|
||||
if (!openaiChunk || !openaiChunk.choices || !openaiChunk.choices[0]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const responseId = requestId || `resp_${uuidv4().replace(/-/g, '')}`;
|
||||
const choice = openaiChunk.choices[0];
|
||||
const delta = choice.delta || {};
|
||||
const events = [];
|
||||
|
||||
// 第一个chunk - role为assistant时调用 getOpenAIResponsesStreamChunkBegin
|
||||
if (delta.role === 'assistant') {
|
||||
events.push(
|
||||
generateResponseCreated(responseId, model || openaiChunk.model || 'unknown'),
|
||||
generateResponseInProgress(responseId),
|
||||
generateOutputItemAdded(responseId),
|
||||
generateContentPartAdded(responseId)
|
||||
);
|
||||
}
|
||||
|
||||
// 处理 reasoning_content(推理内容)
|
||||
if (delta.reasoning_content) {
|
||||
events.push({
|
||||
delta: delta.reasoning_content,
|
||||
item_id: `thinking_${uuidv4().replace(/-/g, '')}`,
|
||||
output_index: 0,
|
||||
sequence_number: 3,
|
||||
type: "response.reasoning_summary_text.delta"
|
||||
});
|
||||
}
|
||||
|
||||
// 处理 tool_calls(工具调用)
|
||||
if (delta.tool_calls && delta.tool_calls.length > 0) {
|
||||
for (const toolCall of delta.tool_calls) {
|
||||
const outputIndex = toolCall.index || 0;
|
||||
|
||||
// 如果有 function.name,说明是工具调用开始
|
||||
if (toolCall.function && toolCall.function.name) {
|
||||
events.push({
|
||||
item: {
|
||||
id: toolCall.id || `call_${uuidv4().replace(/-/g, '')}`,
|
||||
type: "function_call",
|
||||
name: toolCall.function.name,
|
||||
arguments: "",
|
||||
status: "in_progress"
|
||||
},
|
||||
output_index: outputIndex,
|
||||
sequence_number: 2,
|
||||
type: "response.output_item.added"
|
||||
});
|
||||
}
|
||||
|
||||
// 如果有 function.arguments,说明是参数增量
|
||||
if (toolCall.function && toolCall.function.arguments) {
|
||||
events.push({
|
||||
delta: toolCall.function.arguments,
|
||||
item_id: toolCall.id || `call_${uuidv4().replace(/-/g, '')}`,
|
||||
output_index: outputIndex,
|
||||
sequence_number: 3,
|
||||
type: "response.custom_tool_call_input.delta"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理普通文本内容
|
||||
if (delta.content) {
|
||||
events.push({
|
||||
delta: delta.content,
|
||||
item_id: `msg_${uuidv4().replace(/-/g, '')}`,
|
||||
output_index: 0,
|
||||
sequence_number: 3,
|
||||
type: "response.output_text.delta"
|
||||
});
|
||||
}
|
||||
|
||||
// 处理完成状态 - 调用 getOpenAIResponsesStreamChunkEnd
|
||||
if (choice.finish_reason) {
|
||||
events.push(
|
||||
generateOutputTextDone(responseId),
|
||||
generateContentPartDone(responseId),
|
||||
generateOutputItemDone(responseId),
|
||||
generateResponseCompleted(responseId)
|
||||
);
|
||||
|
||||
// 如果有 usage 信息,更新最后一个事件
|
||||
if (openaiChunk.usage && events.length > 0) {
|
||||
const lastEvent = events[events.length - 1];
|
||||
if (lastEvent.response) {
|
||||
lastEvent.response.usage = {
|
||||
input_tokens: openaiChunk.usage.prompt_tokens || 0,
|
||||
output_tokens: openaiChunk.usage.completion_tokens || 0,
|
||||
total_tokens: openaiChunk.usage.total_tokens || 0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default OpenAIConverter;
|
||||
532
src/converters/strategies/OpenAIResponsesConverter.js
Normal file
532
src/converters/strategies/OpenAIResponsesConverter.js
Normal file
|
|
@ -0,0 +1,532 @@
|
|||
/**
|
||||
* OpenAI Responses API 转换器
|
||||
* 处理 OpenAI Responses API 格式与其他协议之间的转换
|
||||
*/
|
||||
|
||||
import { BaseConverter } from '../BaseConverter.js';
|
||||
import { MODEL_PROTOCOL_PREFIX } from '../../common.js';
|
||||
import {
|
||||
extractAndProcessSystemMessages as extractSystemMessages,
|
||||
extractTextFromMessageContent as extractText
|
||||
} from '../utils.js';
|
||||
|
||||
/**
|
||||
* OpenAI Responses API 转换器类
|
||||
* 支持 OpenAI Responses 格式与 OpenAI、Claude、Gemini 之间的转换
|
||||
*/
|
||||
export class OpenAIResponsesConverter extends BaseConverter {
|
||||
constructor() {
|
||||
super(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 请求转换
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* 转换请求到目标协议
|
||||
*/
|
||||
convertRequest(data, toProtocol) {
|
||||
switch (toProtocol) {
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
||||
return this.toOpenAIRequest(data);
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
return this.toClaudeRequest(data);
|
||||
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
||||
return this.toGeminiRequest(data);
|
||||
default:
|
||||
throw new Error(`Unsupported target protocol: ${toProtocol}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换响应到目标协议
|
||||
*/
|
||||
convertResponse(data, toProtocol, model) {
|
||||
switch (toProtocol) {
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
||||
return this.toOpenAIResponse(data, model);
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
return this.toClaudeResponse(data, model);
|
||||
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
||||
return this.toGeminiResponse(data, model);
|
||||
default:
|
||||
throw new Error(`Unsupported target protocol: ${toProtocol}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换流式响应块到目标协议
|
||||
*/
|
||||
convertStreamChunk(chunk, toProtocol, model) {
|
||||
switch (toProtocol) {
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
||||
return this.toOpenAIStreamChunk(chunk, model);
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
return this.toClaudeStreamChunk(chunk, model);
|
||||
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
||||
return this.toGeminiStreamChunk(chunk, model);
|
||||
default:
|
||||
throw new Error(`Unsupported target protocol: ${toProtocol}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换模型列表到目标协议
|
||||
*/
|
||||
convertModelList(data, targetProtocol) {
|
||||
switch (targetProtocol) {
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
||||
return this.toOpenAIModelList(data);
|
||||
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
||||
return this.toClaudeModelList(data);
|
||||
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
||||
return this.toGeminiModelList(data);
|
||||
default:
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 转换到 OpenAI 格式
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* 将 OpenAI Responses 请求转换为标准 OpenAI 请求
|
||||
*/
|
||||
toOpenAIRequest(responsesRequest) {
|
||||
const openaiRequest = {
|
||||
model: responsesRequest.model,
|
||||
messages: [],
|
||||
stream: responsesRequest.stream || false
|
||||
};
|
||||
|
||||
// OpenAI Responses API 使用 instructions 和 input 字段
|
||||
// 需要转换为标准的 messages 格式
|
||||
if (responsesRequest.instructions) {
|
||||
// instructions 作为系统消息
|
||||
openaiRequest.messages.push({
|
||||
role: 'system',
|
||||
content: responsesRequest.instructions
|
||||
});
|
||||
}
|
||||
|
||||
// input 包含用户消息和历史对话
|
||||
if (responsesRequest.input && Array.isArray(responsesRequest.input)) {
|
||||
responsesRequest.input.forEach(item => {
|
||||
if (item.type === 'message') {
|
||||
// 提取消息内容
|
||||
const content = item.content
|
||||
.filter(c => c.type === 'input_text')
|
||||
.map(c => c.text)
|
||||
.join('\n');
|
||||
|
||||
if (content) {
|
||||
openaiRequest.messages.push({
|
||||
role: item.role,
|
||||
content: content
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 如果有标准的 messages 字段,也支持
|
||||
if (responsesRequest.messages && Array.isArray(responsesRequest.messages)) {
|
||||
responsesRequest.messages.forEach(msg => {
|
||||
openaiRequest.messages.push({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 复制其他参数
|
||||
if (responsesRequest.temperature !== undefined) {
|
||||
openaiRequest.temperature = responsesRequest.temperature;
|
||||
}
|
||||
if (responsesRequest.max_tokens !== undefined) {
|
||||
openaiRequest.max_tokens = responsesRequest.max_tokens;
|
||||
}
|
||||
if (responsesRequest.top_p !== undefined) {
|
||||
openaiRequest.top_p = responsesRequest.top_p;
|
||||
}
|
||||
|
||||
return openaiRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OpenAI Responses 响应转换为标准 OpenAI 响应
|
||||
*/
|
||||
toOpenAIResponse(responsesResponse, model) {
|
||||
// OpenAI Responses 格式已经很接近标准 OpenAI 格式
|
||||
return {
|
||||
id: responsesResponse.id || `chatcmpl-${Date.now()}`,
|
||||
object: 'chat.completion',
|
||||
created: responsesResponse.created || Math.floor(Date.now() / 1000),
|
||||
model: model || responsesResponse.model,
|
||||
choices: responsesResponse.choices || [{
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: responsesResponse.content || ''
|
||||
},
|
||||
finish_reason: responsesResponse.finish_reason || 'stop'
|
||||
}],
|
||||
usage: responsesResponse.usage || {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OpenAI Responses 流式块转换为标准 OpenAI 流式块
|
||||
*/
|
||||
toOpenAIStreamChunk(responsesChunk, model) {
|
||||
return {
|
||||
id: responsesChunk.id || `chatcmpl-${Date.now()}`,
|
||||
object: 'chat.completion.chunk',
|
||||
created: responsesChunk.created || Math.floor(Date.now() / 1000),
|
||||
model: model || responsesChunk.model,
|
||||
choices: responsesChunk.choices || [{
|
||||
index: 0,
|
||||
delta: {
|
||||
content: responsesChunk.delta?.content || ''
|
||||
},
|
||||
finish_reason: responsesChunk.finish_reason || null
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 转换到 Claude 格式
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* 将 OpenAI Responses 请求转换为 Claude 请求
|
||||
*/
|
||||
toClaudeRequest(responsesRequest) {
|
||||
const claudeRequest = {
|
||||
model: responsesRequest.model,
|
||||
messages: [],
|
||||
max_tokens: responsesRequest.max_tokens || 4096,
|
||||
stream: responsesRequest.stream || false
|
||||
};
|
||||
|
||||
// 处理 instructions 作为系统消息
|
||||
if (responsesRequest.instructions) {
|
||||
claudeRequest.system = responsesRequest.instructions;
|
||||
}
|
||||
|
||||
// 处理 input 数组中的消息
|
||||
if (responsesRequest.input && Array.isArray(responsesRequest.input)) {
|
||||
responsesRequest.input.forEach(item => {
|
||||
if (item.type === 'message') {
|
||||
const content = item.content
|
||||
.filter(c => c.type === 'input_text')
|
||||
.map(c => c.text)
|
||||
.join('\n');
|
||||
|
||||
if (content) {
|
||||
claudeRequest.messages.push({
|
||||
role: item.role === 'assistant' ? 'assistant' : 'user',
|
||||
content: content
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 如果有标准的 messages 字段,也支持
|
||||
if (responsesRequest.messages && Array.isArray(responsesRequest.messages)) {
|
||||
if (!claudeRequest.system && systemMessages.length > 0) {
|
||||
const systemTexts = systemMessages.map(msg => extractText(msg.content));
|
||||
claudeRequest.system = systemTexts.join('\n');
|
||||
}
|
||||
|
||||
otherMessages.forEach(msg => {
|
||||
claudeRequest.messages.push({
|
||||
role: msg.role === 'assistant' ? 'assistant' : 'user',
|
||||
content: typeof msg.content === 'string' ? msg.content : extractText(msg.content)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 复制其他参数
|
||||
if (responsesRequest.temperature !== undefined) {
|
||||
claudeRequest.temperature = responsesRequest.temperature;
|
||||
}
|
||||
if (responsesRequest.top_p !== undefined) {
|
||||
claudeRequest.top_p = responsesRequest.top_p;
|
||||
}
|
||||
|
||||
return claudeRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OpenAI Responses 响应转换为 Claude 响应
|
||||
*/
|
||||
toClaudeResponse(responsesResponse, model) {
|
||||
const content = responsesResponse.choices?.[0]?.message?.content ||
|
||||
responsesResponse.content || '';
|
||||
|
||||
return {
|
||||
id: responsesResponse.id || `msg_${Date.now()}`,
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: content
|
||||
}],
|
||||
model: model || responsesResponse.model,
|
||||
stop_reason: responsesResponse.choices?.[0]?.finish_reason || 'end_turn',
|
||||
usage: {
|
||||
input_tokens: responsesResponse.usage?.prompt_tokens || 0,
|
||||
output_tokens: responsesResponse.usage?.completion_tokens || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OpenAI Responses 流式块转换为 Claude 流式块
|
||||
*/
|
||||
toClaudeStreamChunk(responsesChunk, model) {
|
||||
const delta = responsesChunk.choices?.[0]?.delta || responsesChunk.delta || {};
|
||||
const finishReason = responsesChunk.choices?.[0]?.finish_reason ||
|
||||
responsesChunk.finish_reason;
|
||||
|
||||
if (finishReason) {
|
||||
return {
|
||||
type: 'message_stop'
|
||||
};
|
||||
}
|
||||
|
||||
if (delta.content) {
|
||||
return {
|
||||
type: 'content_block_delta',
|
||||
index: 0,
|
||||
delta: {
|
||||
type: 'text_delta',
|
||||
text: delta.content
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'message_start',
|
||||
message: {
|
||||
id: responsesChunk.id || `msg_${Date.now()}`,
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
model: model || responsesChunk.model
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 转换到 Gemini 格式
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* 将 OpenAI Responses 请求转换为 Gemini 请求
|
||||
*/
|
||||
toGeminiRequest(responsesRequest) {
|
||||
const geminiRequest = {
|
||||
contents: [],
|
||||
generationConfig: {}
|
||||
};
|
||||
|
||||
// 处理 instructions 作为系统指令
|
||||
if (responsesRequest.instructions) {
|
||||
geminiRequest.systemInstruction = {
|
||||
parts: [{
|
||||
text: responsesRequest.instructions
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// 处理 input 数组中的消息
|
||||
if (responsesRequest.input && Array.isArray(responsesRequest.input)) {
|
||||
responsesRequest.input.forEach(item => {
|
||||
if (item.type === 'message') {
|
||||
const content = item.content
|
||||
.filter(c => c.type === 'input_text')
|
||||
.map(c => c.text)
|
||||
.join('\n');
|
||||
|
||||
if (content) {
|
||||
geminiRequest.contents.push({
|
||||
role: item.role === 'assistant' ? 'model' : 'user',
|
||||
parts: [{
|
||||
text: content
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 如果有标准的 messages 字段,也支持
|
||||
if (responsesRequest.messages && Array.isArray(responsesRequest.messages)) {
|
||||
const { systemMessages, otherMessages } = extractSystemMessages(
|
||||
responsesRequest.messages
|
||||
);
|
||||
|
||||
if (!geminiRequest.systemInstruction && systemMessages.length > 0) {
|
||||
const systemTexts = systemMessages.map(msg => extractText(msg.content));
|
||||
geminiRequest.systemInstruction = {
|
||||
parts: [{
|
||||
text: systemTexts.join('\n')
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
otherMessages.forEach(msg => {
|
||||
geminiRequest.contents.push({
|
||||
role: msg.role === 'assistant' ? 'model' : 'user',
|
||||
parts: [{
|
||||
text: typeof msg.content === 'string' ? msg.content : extractText(msg.content)
|
||||
}]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 设置生成配置
|
||||
if (responsesRequest.temperature !== undefined) {
|
||||
geminiRequest.generationConfig.temperature = responsesRequest.temperature;
|
||||
}
|
||||
if (responsesRequest.max_tokens !== undefined) {
|
||||
geminiRequest.generationConfig.maxOutputTokens = responsesRequest.max_tokens;
|
||||
}
|
||||
if (responsesRequest.top_p !== undefined) {
|
||||
geminiRequest.generationConfig.topP = responsesRequest.top_p;
|
||||
}
|
||||
|
||||
return geminiRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OpenAI Responses 响应转换为 Gemini 响应
|
||||
*/
|
||||
toGeminiResponse(responsesResponse, model) {
|
||||
const content = responsesResponse.choices?.[0]?.message?.content ||
|
||||
responsesResponse.content || '';
|
||||
|
||||
return {
|
||||
candidates: [{
|
||||
content: {
|
||||
parts: [{
|
||||
text: content
|
||||
}],
|
||||
role: 'model'
|
||||
},
|
||||
finishReason: this.mapFinishReason(
|
||||
responsesResponse.choices?.[0]?.finish_reason || 'STOP'
|
||||
),
|
||||
index: 0
|
||||
}],
|
||||
usageMetadata: {
|
||||
promptTokenCount: responsesResponse.usage?.prompt_tokens || 0,
|
||||
candidatesTokenCount: responsesResponse.usage?.completion_tokens || 0,
|
||||
totalTokenCount: responsesResponse.usage?.total_tokens || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OpenAI Responses 流式块转换为 Gemini 流式块
|
||||
*/
|
||||
toGeminiStreamChunk(responsesChunk, model) {
|
||||
const delta = responsesChunk.choices?.[0]?.delta || responsesChunk.delta || {};
|
||||
const finishReason = responsesChunk.choices?.[0]?.finish_reason ||
|
||||
responsesChunk.finish_reason;
|
||||
|
||||
return {
|
||||
candidates: [{
|
||||
content: {
|
||||
parts: delta.content ? [{
|
||||
text: delta.content
|
||||
}] : [],
|
||||
role: 'model'
|
||||
},
|
||||
finishReason: finishReason ? this.mapFinishReason(finishReason) : null,
|
||||
index: 0
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 辅助方法
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* 映射完成原因
|
||||
*/
|
||||
mapFinishReason(reason) {
|
||||
const reasonMap = {
|
||||
'stop': 'STOP',
|
||||
'length': 'MAX_TOKENS',
|
||||
'content_filter': 'SAFETY',
|
||||
'end_turn': 'STOP'
|
||||
};
|
||||
return reasonMap[reason] || 'STOP';
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OpenAI Responses 模型列表转换为标准 OpenAI 模型列表
|
||||
*/
|
||||
toOpenAIModelList(responsesModels) {
|
||||
// OpenAI Responses 格式的模型列表已经是标准 OpenAI 格式
|
||||
// 如果输入已经是标准格式,直接返回
|
||||
if (responsesModels.object === 'list' && responsesModels.data) {
|
||||
return responsesModels;
|
||||
}
|
||||
|
||||
// 如果是其他格式,转换为标准格式
|
||||
return {
|
||||
object: "list",
|
||||
data: (responsesModels.models || responsesModels.data || []).map(m => ({
|
||||
id: m.id || m.name,
|
||||
object: "model",
|
||||
created: m.created || Math.floor(Date.now() / 1000),
|
||||
owned_by: m.owned_by || "openai",
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OpenAI Responses 模型列表转换为 Claude 模型列表
|
||||
*/
|
||||
toClaudeModelList(responsesModels) {
|
||||
const models = responsesModels.data || responsesModels.models || [];
|
||||
return {
|
||||
models: models.map(m => ({
|
||||
name: m.id || m.name,
|
||||
description: m.description || "",
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OpenAI Responses 模型列表转换为 Gemini 模型列表
|
||||
*/
|
||||
toGeminiModelList(responsesModels) {
|
||||
const models = responsesModels.data || responsesModels.models || [];
|
||||
return {
|
||||
models: models.map(m => ({
|
||||
name: `models/${m.id || m.name}`,
|
||||
version: m.version || "1.0.0",
|
||||
displayName: m.displayName || m.id || m.name,
|
||||
description: m.description || `A generative model for text and chat generation. ID: ${m.id || m.name}`,
|
||||
inputTokenLimit: m.inputTokenLimit || 32768,
|
||||
outputTokenLimit: m.outputTokenLimit || 8192,
|
||||
supportedGenerationMethods: m.supportedGenerationMethods || ["generateContent", "streamGenerateContent"]
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
296
src/converters/utils.js
Normal file
296
src/converters/utils.js
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
/**
|
||||
* 转换器公共工具函数模块
|
||||
* 提供各种协议转换所需的通用辅助函数
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// =============================================================================
|
||||
// 常量定义
|
||||
// =============================================================================
|
||||
|
||||
export const DEFAULT_MAX_TOKENS = 8192;
|
||||
export const DEFAULT_GEMINI_MAX_TOKENS = 65535;
|
||||
export const DEFAULT_TEMPERATURE = 1;
|
||||
export const DEFAULT_TOP_P = 0.95;
|
||||
|
||||
// =============================================================================
|
||||
// 通用辅助函数
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* 判断值是否为 undefined 或 0,并返回默认值
|
||||
* @param {*} value - 要检查的值
|
||||
* @param {*} defaultValue - 默认值
|
||||
* @returns {*} 处理后的值
|
||||
*/
|
||||
export function checkAndAssignOrDefault(value, defaultValue) {
|
||||
if (value !== undefined && value !== 0) {
|
||||
return value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
* @param {string} prefix - ID前缀
|
||||
* @returns {string} 生成的ID
|
||||
*/
|
||||
export function generateId(prefix = '') {
|
||||
return prefix ? `${prefix}_${uuidv4()}` : uuidv4();
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全解析JSON字符串
|
||||
* @param {string} str - JSON字符串
|
||||
* @returns {*} 解析后的对象或原始字符串
|
||||
*/
|
||||
export function safeParseJSON(str) {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
let cleanedStr = str;
|
||||
|
||||
// 处理可能被截断的转义序列
|
||||
if (cleanedStr.endsWith('\\') && !cleanedStr.endsWith('\\\\')) {
|
||||
cleanedStr = cleanedStr.substring(0, cleanedStr.length - 1);
|
||||
} else if (cleanedStr.endsWith('\\u') || cleanedStr.endsWith('\\u0') || cleanedStr.endsWith('\\u00')) {
|
||||
const idx = cleanedStr.lastIndexOf('\\u');
|
||||
cleanedStr = cleanedStr.substring(0, idx);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(cleanedStr || '{}');
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取消息内容中的文本
|
||||
* @param {string|Array} content - 消息内容
|
||||
* @returns {string} 提取的文本
|
||||
*/
|
||||
export function extractTextFromMessageContent(content) {
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.filter(part => part.type === 'text' && part.text)
|
||||
.map(part => part.text)
|
||||
.join('\n');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取并处理系统消息
|
||||
* @param {Array} messages - 消息数组
|
||||
* @returns {{systemInstruction: Object|null, nonSystemMessages: Array}}
|
||||
*/
|
||||
export function extractAndProcessSystemMessages(messages) {
|
||||
const systemContents = [];
|
||||
const nonSystemMessages = [];
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.role === 'system') {
|
||||
systemContents.push(extractTextFromMessageContent(message.content));
|
||||
} else {
|
||||
nonSystemMessages.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
let systemInstruction = null;
|
||||
if (systemContents.length > 0) {
|
||||
systemInstruction = {
|
||||
parts: [{
|
||||
text: systemContents.join('\n')
|
||||
}]
|
||||
};
|
||||
}
|
||||
return { systemInstruction, nonSystemMessages };
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理JSON Schema属性(移除Gemini不支持的属性)
|
||||
* @param {Object} schema - JSON Schema
|
||||
* @returns {Object} 清理后的JSON Schema
|
||||
*/
|
||||
export function cleanJsonSchemaProperties(schema) {
|
||||
if (!schema || typeof schema !== 'object') {
|
||||
return schema;
|
||||
}
|
||||
|
||||
const sanitized = {};
|
||||
for (const [key, value] of Object.entries(schema)) {
|
||||
if (["type", "description", "properties", "required", "enum", "items"].includes(key)) {
|
||||
sanitized[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (sanitized.properties && typeof sanitized.properties === 'object') {
|
||||
const cleanProperties = {};
|
||||
for (const [propName, propSchema] of Object.entries(sanitized.properties)) {
|
||||
cleanProperties[propName] = cleanJsonSchemaProperties(propSchema);
|
||||
}
|
||||
sanitized.properties = cleanProperties;
|
||||
}
|
||||
|
||||
if (sanitized.items) {
|
||||
sanitized.items = cleanJsonSchemaProperties(sanitized.items);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射结束原因
|
||||
* @param {string} reason - 结束原因
|
||||
* @param {string} sourceFormat - 源格式
|
||||
* @param {string} targetFormat - 目标格式
|
||||
* @returns {string} 映射后的结束原因
|
||||
*/
|
||||
export function mapFinishReason(reason, sourceFormat, targetFormat) {
|
||||
const reasonMappings = {
|
||||
openai: {
|
||||
anthropic: {
|
||||
stop: "end_turn",
|
||||
length: "max_tokens",
|
||||
content_filter: "stop_sequence",
|
||||
tool_calls: "tool_use"
|
||||
}
|
||||
},
|
||||
gemini: {
|
||||
anthropic: {
|
||||
STOP: "end_turn",
|
||||
MAX_TOKENS: "max_tokens",
|
||||
SAFETY: "stop_sequence",
|
||||
RECITATION: "stop_sequence",
|
||||
stop: "end_turn",
|
||||
length: "max_tokens",
|
||||
safety: "stop_sequence",
|
||||
recitation: "stop_sequence",
|
||||
other: "end_turn"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
return reasonMappings[sourceFormat][targetFormat][reason] || "end_turn";
|
||||
} catch (e) {
|
||||
return "end_turn";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据budget_tokens智能判断OpenAI reasoning_effort等级
|
||||
* @param {number|null} budgetTokens - Anthropic thinking的budget_tokens值
|
||||
* @returns {string} OpenAI reasoning_effort等级
|
||||
*/
|
||||
export function determineReasoningEffortFromBudget(budgetTokens) {
|
||||
if (budgetTokens === null || budgetTokens === undefined) {
|
||||
console.info("No budget_tokens provided, defaulting to reasoning_effort='high'");
|
||||
return "high";
|
||||
}
|
||||
|
||||
const LOW_THRESHOLD = 50;
|
||||
const HIGH_THRESHOLD = 200;
|
||||
|
||||
console.debug(`Threshold configuration: low <= ${LOW_THRESHOLD}, medium <= ${HIGH_THRESHOLD}, high > ${HIGH_THRESHOLD}`);
|
||||
|
||||
let effort;
|
||||
if (budgetTokens <= LOW_THRESHOLD) {
|
||||
effort = "low";
|
||||
} else if (budgetTokens <= HIGH_THRESHOLD) {
|
||||
effort = "medium";
|
||||
} else {
|
||||
effort = "high";
|
||||
}
|
||||
|
||||
console.info(`🎯 Budget tokens ${budgetTokens} -> reasoning_effort '${effort}' (thresholds: low<=${LOW_THRESHOLD}, high<=${HIGH_THRESHOLD})`);
|
||||
return effort;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从OpenAI文本中提取thinking内容
|
||||
* @param {string} text - 文本内容
|
||||
* @returns {string|Array} 提取后的内容
|
||||
*/
|
||||
export function extractThinkingFromOpenAIText(text) {
|
||||
const thinkingPattern = /<thinking>\s*(.*?)\s*<\/thinking>/gs;
|
||||
const matches = [...text.matchAll(thinkingPattern)];
|
||||
|
||||
const contentBlocks = [];
|
||||
let lastEnd = 0;
|
||||
|
||||
for (const match of matches) {
|
||||
const beforeText = text.substring(lastEnd, match.index).trim();
|
||||
if (beforeText) {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
text: beforeText
|
||||
});
|
||||
}
|
||||
|
||||
const thinkingText = match[1].trim();
|
||||
if (thinkingText) {
|
||||
contentBlocks.push({
|
||||
type: "thinking",
|
||||
thinking: thinkingText
|
||||
});
|
||||
}
|
||||
|
||||
lastEnd = match.index + match[0].length;
|
||||
}
|
||||
|
||||
const afterText = text.substring(lastEnd).trim();
|
||||
if (afterText) {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
text: afterText
|
||||
});
|
||||
}
|
||||
|
||||
if (contentBlocks.length === 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
if (contentBlocks.length === 1 && contentBlocks[0].type === "text") {
|
||||
return contentBlocks[0].text;
|
||||
}
|
||||
|
||||
return contentBlocks;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 工具状态管理器(单例模式)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* 全局工具状态管理器
|
||||
*/
|
||||
class ToolStateManager {
|
||||
constructor() {
|
||||
if (ToolStateManager.instance) {
|
||||
return ToolStateManager.instance;
|
||||
}
|
||||
ToolStateManager.instance = this;
|
||||
this._toolMappings = {};
|
||||
return this;
|
||||
}
|
||||
|
||||
storeToolMapping(funcName, toolId) {
|
||||
this._toolMappings[funcName] = toolId;
|
||||
}
|
||||
|
||||
getToolId(funcName) {
|
||||
return this._toolMappings[funcName] || null;
|
||||
}
|
||||
|
||||
clearMappings() {
|
||||
this._toolMappings = {};
|
||||
}
|
||||
}
|
||||
|
||||
export const toolStateManager = new ToolStateManager();
|
||||
1
src/example/claude/oldResponse1762501965597.json
Normal file
1
src/example/claude/oldResponse1762501965597.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"id":"msg_chatcmpl-690da54ca66df0e03c78831b","type":"message","role":"assistant","content":[{"type":"text","text":"I"}],"model":"claude-haiku-4-5-20251001","stop_reason":"max_tokens","stop_sequence":null,"usage":{"input_tokens":10855,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"prompt_tokens":10855,"cached_tokens":10855,"completion_tokens":1,"total_tokens":10856}}
|
||||
19
src/example/claude/oldResponseChunk1762501803864.json
Normal file
19
src/example/claude/oldResponseChunk1762501803864.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{"type":"message_start","message":{"id":"msg_chatcmpl-690da4a34b300f384a5563b3","type":"message","role":"assistant","content":[],"model":"claude-sonnet-4-5-20250929","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":22969,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"service_tier":"standard","prompt_tokens":22969,"cached_tokens":0}}}
|
||||
{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"TodoWrite_3","name":"TodoWrite","input":{}}}
|
||||
{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\""}}
|
||||
{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"t"}}
|
||||
{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"odos"}}
|
||||
{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"\":[{\""}}
|
||||
{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"content"}}
|
||||
{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"\":\""}}
|
||||
{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"读取"}}
|
||||
{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"当前的"}}
|
||||
{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"README"}}
|
||||
{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"文件"}}
|
||||
{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"\",\""}}
|
||||
{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"status"}}
|
||||
{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"\"}"}}
|
||||
{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"]}"}}
|
||||
{"type":"content_block_stop","index":0}
|
||||
{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":22969,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":80,"prompt_tokens":22969,"completion_tokens":80,"total_tokens":23049,"cached_tokens":15872}}
|
||||
{"type":"message_stop"}
|
||||
1
src/example/claude/originalRequestBody1762501795790.json
Normal file
1
src/example/claude/originalRequestBody1762501795790.json
Normal file
File diff suppressed because one or more lines are too long
1
src/example/claude/originalRequestBody1762501964743.json
Normal file
1
src/example/claude/originalRequestBody1762501964743.json
Normal file
File diff suppressed because one or more lines are too long
58
src/example/claude/responseChunk1762590717239.json
Normal file
58
src/example/claude/responseChunk1762590717239.json
Normal file
File diff suppressed because one or more lines are too long
1
src/example/gemini/oldResponse1762502386432.json
Normal file
1
src/example/gemini/oldResponse1762502386432.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"candidates":[{"content":{"role":"model","parts":[{"text":"好的,我为您整理并优化了一份可读性强的 Markdown 格式项目文档,文案风格力求专业、平易近人,并已将随机数纳入其中。\n\n---\n\n# 项目名称:[请在此处填写您的项目名称]\n\n## 概览\n\n本项目致力于提供一套[此处简要描述项目核心功能或解决的问题,例如:高效、稳定的数据处理方案 / 创新的前端组件库 / 自动化的部署工具]。我们注重性能、可靠性与用户体验,旨在为您简化工作流程,提升开发效率。\n\n## 核心特性\n\n* **简洁明了的架构**: 采用模块化设计,易于理解与维护。\n* **高效可靠的运算**: 核心算法经过优化,确保在多种场景下均能稳定快速响应。\n* **灵活的配置选项**: 提供丰富的配置接口,允许用户根据具体需求进行定制。\n* **[其他具体特性,例如:跨平台兼容性 / 友好的用户界面 / 完善的API文档]**\n\n## 快速上手\n\n### 1. 环境准备\n\n在开始使用本项目之前,请确保您的系统满足以下最低要求:\n\n* **操作系统**: Windows 10+, macOS 10.15+, Ubuntu 20.04+\n* **依赖软件**: [例如:Python 3.8+ / Node.js 14+ / Docker 20.10+]\n\n### 2. 安装指南\n\n请按照以下步骤安装本项目:\n\n1. **克隆仓库**:\n ```bash\n git clone https://github.com/your-username/your-project-name.git\n cd your-project-name\n ```\n2. **安装依赖**:\n ```bash\n # 例如,如果是Python项目\n pip install -r requirements.txt\n # 如果是Node.js项目\n npm install\n # 或者,如果需要构建\n npm run build\n ```\n\n### 3. 使用示例\n\n安装完成后,您可以尝试运行以下示例来验证项目功能:\n\n```bash\n# 例如,如果是命令行工具\npython main.py --help\n# 如果是启动服务\nnpm start\n# 或者调用SDK接口\n# import your_project\n# result = your_project.process_data(...)\n```\n[在此处添加更具体的代码示例或使用场景]\n\n## 贡献指南\n\n我们非常欢迎社区的贡献!如果您有兴趣改进本项目,请遵循以下步骤:\n\n1. **Fork 本仓库**: 将项目 Fork 到您的 GitHub 账户。\n2. **创建功能分支**: 从 `main` 分支拉出新分支,例如 `feature/add-new-function` 或 `bugfix/resolve-issue-123`。\n3. **提交代码**: 确保代码风格与项目保持一致,并编写清晰的提交信息。\n4. **提交 Pull Request**: 详细描述您所做的更改及原因。\n\n在提交 Pull Request 前,请务必运行测试以确保没有任何回归。\n\n## 许可证\n\n本项目采用 [请在此处填写许可证类型,例如:MIT License / Apache 2.0 License] 开放源代码许可。有关详细信息,请参阅项目根目录下的 `LICENSE` 文件。\n\n## 联系方式\n\n如果您在使用过程中遇到任何问题,欢迎通过以下方式与我们联系:\n\n* **GitHub Issues**: [https://github.com/your-username/your-project-name/issues](https://github.com/your-username/your-project-name/issues)\n* **电子邮件**: [your.email@example.com]\n\n## 附注随机标识\n\n本项目的特定生成标识为:`349187`\n\n---"}]},"finishReason":"STOP","avgLogprobs":-0.9484239548045448}],"usageMetadata":{"promptTokenCount":106,"candidatesTokenCount":821,"totalTokenCount":1674,"trafficType":"ON_DEMAND","promptTokensDetails":[{"modality":"TEXT","tokenCount":106}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":821}],"thoughtsTokenCount":747}}
|
||||
3
src/example/gemini/oldResponseChunk1762502078640.json
Normal file
3
src/example/gemini/oldResponseChunk1762502078640.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{"candidates":[{"content":{"role":"model","parts":[{"text":"<ask_followup_question>\n<question>请问有什么具体任务需要我协助完成吗?例如,您是需要我编写代码、修复错误、重构代码、回答问题,还是其他操作?</question>\n<"}]}}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"}}
|
||||
{"candidates":[{"content":{"role":"model","parts":[{"text":"follow_up>\n<suggest>我需要你编写一个新功能。</suggest>\n<suggest>我需要你帮助我修复一个bug。</suggest>\n<suggest>我有一个关于代码的问题想问你。</suggest>\n<suggest>我"}]}}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"}}
|
||||
{"candidates":[{"content":{"role":"model","parts":[{"text":"需要你检查一下我的代码。</suggest>\n</follow_up>\n</ask_followup_question>"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":19131,"candidatesTokenCount":127,"totalTokenCount":19351,"trafficType":"PROVISIONED_THROUGHPUT","promptTokensDetails":[{"modality":"TEXT","tokenCount":19131}],"candidatesTokensDetails":[{"modality":"TEXT","tokenCount":127}],"thoughtsTokenCount":93}}
|
||||
1
src/example/gemini/originalRequestBody1762502075001.json
Normal file
1
src/example/gemini/originalRequestBody1762502075001.json
Normal file
File diff suppressed because one or more lines are too long
1
src/example/gemini/originalRequestBody1762502375803.json
Normal file
1
src/example/gemini/originalRequestBody1762502375803.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"generationConfig":{"temperature":1,"topP":1,"thinkingConfig":{"includeThoughts":true}},"contents":[{"role":"user","parts":[{"text":"更新readme,写入一行随机数"}]}],"systemInstruction":{"parts":[{"text":"整理优化为可读性强的markdown 文档, 不需要按照时间顺序整理, 自行调整内容的段落位置, 可以读取图片内的文案,辅助理解文档,需要对文字进行润色,用简体中文输出。输出格式参考历史输出内容的格式,需要结合历史数据写新的文档,输出时不要遗漏消息内容。文案口吻不要夸张,有技术范且平易近人,符合istj人格"}]}}
|
||||
1
src/example/openai/oldResponse1762501667708.json
Normal file
1
src/example/openai/oldResponse1762501667708.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"id":"202511071547448d40fc1d67b440be","object":"","created":1762501667,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""},"finish_reason":"stop","message":{"role":"assistant","content":"\n现在我需要生成一个随机数并将其添加到 README.md 文件中。我将使用 JavaScript 的 Math.random() 函数生成一个随机数,并将其添加到文件的适当位置。\n\n<execute_command>\n<command>node -e \"console.log('随机数:', Math.floor(Math.random() * 1000000))\"</command>\n</execute_command>","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":40076,"completion_tokens":76,"total_tokens":40152}}
|
||||
71
src/example/openai/oldResponseChunk1762501528837.json
Normal file
71
src/example/openai/oldResponseChunk1762501528837.json
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"\n"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"用户"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"想要"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"更新"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"README"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"文件"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":","},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"添加"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"一行"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"随机"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"数"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"。"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"我"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"需要"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":":\n\n"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"1"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"."},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":" 先"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"查看"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"当前"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"目录"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"中的"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"文件"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"结构"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"\n"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"2"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"."},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":" "},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"找"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"到"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"README"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"文件"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"("},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"可能是"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"README"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":".md"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"、"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"README"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":".txt"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"等"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":")\n"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"3"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"."},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":" "},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"读取"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"现有"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"内容"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"\n"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"4"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"."},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":" "},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"添加"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"一行"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"随机"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"数"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"\n"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"5"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"."},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":" 更"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"新"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"文件"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"\n\n"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"让我"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"先"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"查看"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"文件"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"列表"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":"。"},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"\n","tool_calls":null,"function_calls":null,"reasoning_content":""},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":null,"content":"","tool_calls":[{"function":{"arguments":"{\"command\":dir}","name":"shell"},"id":"call_c1b2d1cfe12340ba85700f80","index":0,"type":"function"}],"function_calls":null,"reasoning_content":""},"finish_reason":null,"message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
|
||||
{"id":"2025110715445604284d0099e04d74","object":"","created":1762501496,"model":"ZhipuAI/GLM-4.6","system_fingerprint":"","choices":[{"index":0,"delta":{"role":"assistant","content":"","tool_calls":null,"function_calls":null,"reasoning_content":""},"finish_reason":"tool_calls","message":{"role":null,"content":"","tool_calls":null,"function_calls":null,"reasoning_content":""}}],"usage":{"prompt_tokens":6243,"completion_tokens":84,"total_tokens":6327}}
|
||||
1
src/example/openai/originalRequestBody1762501497434.json
Normal file
1
src/example/openai/originalRequestBody1762501497434.json
Normal file
File diff suppressed because one or more lines are too long
1
src/example/openai/originalRequestBody1762501665376.json
Normal file
1
src/example/openai/originalRequestBody1762501665376.json
Normal file
File diff suppressed because one or more lines are too long
56
src/example/openaiResponses/oldResponse1762502706486.json
Normal file
56
src/example/openaiResponses/oldResponse1762502706486.json
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"id": "resp_67ccd2bed1ec8190b14f964abc0542670bb6a6b452d3795b",
|
||||
"object": "response",
|
||||
"created_at": 1741476542,
|
||||
"status": "completed",
|
||||
"error": null,
|
||||
"incomplete_details": null,
|
||||
"instructions": null,
|
||||
"max_output_tokens": null,
|
||||
"model": "gpt-4.1",
|
||||
"output": [
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_67ccd2bf17f0819081ff3bb2cf6508e60bb6a6b452d3795b",
|
||||
"status": "completed",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": "在一个宁静的月夜下,一只名叫璐米娜的独角兽发现了一个倒映着星星的隐藏水池。当她将独角浸入水中时,水池开始闪烁,显现出通往一个有着无尽夜空的魔法世界的路径。充满好奇,璐米娜为所有做梦的人许下愿望,希望他们能找到自己的隐藏魔法,当她回头望去,她的蹄印像星尘一样闪烁。",
|
||||
"annotations": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"parallel_tool_calls": true,
|
||||
"previous_response_id": null,
|
||||
"reasoning": {
|
||||
"effort": null,
|
||||
"summary": null
|
||||
},
|
||||
"store": true,
|
||||
"temperature": 1.0,
|
||||
"text": {
|
||||
"format": {
|
||||
"type": "text"
|
||||
}
|
||||
},
|
||||
"tool_choice": "auto",
|
||||
"tools": [],
|
||||
"top_p": 1.0,
|
||||
"truncation": "disabled",
|
||||
"usage": {
|
||||
"input_tokens": 36,
|
||||
"input_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
},
|
||||
"output_tokens": 87,
|
||||
"output_tokens_details": {
|
||||
"reasoning_tokens": 0
|
||||
},
|
||||
"total_tokens": 123
|
||||
},
|
||||
"user": null,
|
||||
"metadata": {}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -4,7 +4,7 @@ import { promises as fs } from 'fs';
|
|||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as readline from 'readline';
|
||||
import { API_ACTIONS, ensureRolesInContents, formatExpiryTime } from '../common.js';
|
||||
import { API_ACTIONS, formatExpiryTime } from '../common.js';
|
||||
|
||||
// --- Constants ---
|
||||
const AUTH_REDIRECT_PORT = 8085;
|
||||
|
|
@ -18,7 +18,7 @@ const GEMINI_MODELS = ['gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5-
|
|||
const ANTI_TRUNCATION_MODELS = GEMINI_MODELS.map(model => `anti-${model}`);
|
||||
|
||||
function is_anti_truncation_model(model) {
|
||||
return ANTI_TRUNCATION_MODELS.some(antiModel => model.includes(antiModel) || antiModel.includes(model));
|
||||
return ANTI_TRUNCATION_MODELS.some(antiModel => model.includes(antiModel));
|
||||
}
|
||||
|
||||
// 从防截断模型名中提取实际模型名
|
||||
|
|
@ -41,6 +41,66 @@ function toGeminiApiResponse(codeAssistResponse) {
|
|||
return compliantResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that all content parts in a request body have a 'role' property.
|
||||
* If 'systemInstruction' is present and lacks a role, it defaults to 'user'.
|
||||
* If any 'contents' entry lacks a role, it defaults to 'user'.
|
||||
* @param {Object} requestBody - The request body object.
|
||||
* @returns {Object} The modified request body with roles ensured.
|
||||
*/
|
||||
function ensureRolesInContents(requestBody) {
|
||||
delete requestBody.model;
|
||||
// delete requestBody.system_instruction;
|
||||
// delete requestBody.systemInstruction;
|
||||
if (requestBody.system_instruction) {
|
||||
requestBody.systemInstruction = requestBody.system_instruction;
|
||||
delete requestBody.system_instruction;
|
||||
}
|
||||
|
||||
if (requestBody.systemInstruction && !requestBody.systemInstruction.role) {
|
||||
requestBody.systemInstruction.role = 'user';
|
||||
}
|
||||
|
||||
if (requestBody.contents && Array.isArray(requestBody.contents)) {
|
||||
requestBody.contents.forEach(content => {
|
||||
if (!content.role) {
|
||||
content.role = 'user';
|
||||
}
|
||||
});
|
||||
|
||||
// 如果存在 systemInstruction,将其放在 contents 索引 0 的位置
|
||||
// if (requestBody.systemInstruction) {
|
||||
// // 检查 contents[0] 是否与 systemInstruction 内容相同
|
||||
// const firstContent = requestBody.contents[0];
|
||||
// let isSame = false;
|
||||
|
||||
// if (firstContent && firstContent.parts && requestBody.systemInstruction.parts) {
|
||||
// // 比较 parts 数组的内容
|
||||
// const firstContentText = firstContent.parts
|
||||
// .filter(p => p?.text)
|
||||
// .map(p => p.text)
|
||||
// .join('\n');
|
||||
// const systemInstructionText = requestBody.systemInstruction.parts
|
||||
// .filter(p => p?.text)
|
||||
// .map(p => p.text)
|
||||
// .join('\n');
|
||||
|
||||
// isSame = firstContentText === systemInstructionText;
|
||||
// }
|
||||
|
||||
// // 如果内容不同,则将 systemInstruction 插入到索引 0 的位置
|
||||
// if (!isSame) {
|
||||
// requestBody.contents.unshift({
|
||||
// role: requestBody.systemInstruction.role || 'user',
|
||||
// parts: requestBody.systemInstruction.parts
|
||||
// });
|
||||
// }
|
||||
// delete requestBody.systemInstruction;
|
||||
// }
|
||||
}
|
||||
return requestBody;
|
||||
}
|
||||
|
||||
async function* apply_anti_truncation_to_stream(service, model, requestBody) {
|
||||
let currentRequest = { ...requestBody };
|
||||
let allGeneratedText = '';
|
||||
|
|
@ -166,12 +226,14 @@ export class GeminiApiService {
|
|||
const credentials = JSON.parse(data);
|
||||
this.authClient.setCredentials(credentials);
|
||||
console.log('[Gemini Auth] Authentication configured successfully from file.');
|
||||
|
||||
if (forceRefresh) {
|
||||
console.log('[Gemini Auth] Forcing token refresh...');
|
||||
const { credentials: newCredentials } = await this.authClient.refreshAccessToken();
|
||||
this.authClient.setCredentials(newCredentials);
|
||||
// Save refreshed credentials back to file
|
||||
await fs.writeFile(credPath, JSON.stringify(newCredentials, null, 2));
|
||||
console.log('[Gemini Auth] Token refresh response: ok');
|
||||
console.log('[Gemini Auth] Token refreshed and saved successfully.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Gemini Auth] Error initializing authentication:', error.code);
|
||||
|
|
@ -252,7 +314,7 @@ export class GeminiApiService {
|
|||
this.availableModels = GEMINI_MODELS;
|
||||
console.log(`[Gemini] Using fixed models: [${this.availableModels.join(', ')}]`);
|
||||
try {
|
||||
const initialProjectId = "default"
|
||||
const initialProjectId = ""
|
||||
// Prepare client metadata
|
||||
const clientMetadata = {
|
||||
ideType: "IDE_UNSPECIFIED",
|
||||
|
|
@ -261,18 +323,47 @@ export class GeminiApiService {
|
|||
duetProject: initialProjectId,
|
||||
}
|
||||
|
||||
const loadResponse = await this.callApi('loadCodeAssist', { metadata: clientMetadata });
|
||||
// Call loadCodeAssist to discover the actual project ID
|
||||
const loadRequest = {
|
||||
cloudaicompanionProject: initialProjectId,
|
||||
metadata: clientMetadata,
|
||||
}
|
||||
|
||||
const loadResponse = await this.callApi('loadCodeAssist', loadRequest);
|
||||
|
||||
// Check if we already have a project ID from the response
|
||||
if (loadResponse.cloudaicompanionProject) {
|
||||
return loadResponse.cloudaicompanionProject;
|
||||
}
|
||||
|
||||
// If no existing project, we need to onboard
|
||||
const defaultTier = loadResponse.allowedTiers?.find(tier => tier.isDefault);
|
||||
const onboardRequest = { tierId: defaultTier?.id || 'free-tier', metadata: clientMetadata , cloudaicompanionProject: initialProjectId,};
|
||||
let lro = await this.callApi('onboardUser', onboardRequest);
|
||||
while (!lro.done) {
|
||||
const tierId = defaultTier?.id || 'free-tier';
|
||||
|
||||
const onboardRequest = {
|
||||
tierId: tierId,
|
||||
cloudaicompanionProject: initialProjectId,
|
||||
metadata: clientMetadata,
|
||||
};
|
||||
|
||||
let lroResponse = await this.callApi('onboardUser', onboardRequest);
|
||||
|
||||
// Poll until operation is complete with timeout protection
|
||||
const MAX_RETRIES = 30; // Maximum number of retries (60 seconds total)
|
||||
let retryCount = 0;
|
||||
|
||||
while (!lroResponse.done && retryCount < MAX_RETRIES) {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
lro = await this.callApi('onboardUser', onboardRequest);
|
||||
lroResponse = await this.callApi('onboardUser', onboardRequest);
|
||||
retryCount++;
|
||||
}
|
||||
return lro.response?.cloudaicompanionProject?.id;
|
||||
|
||||
if (!lroResponse.done) {
|
||||
throw new Error('Onboarding timeout: Operation did not complete within expected time.');
|
||||
}
|
||||
|
||||
const discoveredProjectId = lroResponse.response?.cloudaicompanionProject?.id || initialProjectId;
|
||||
return discoveredProjectId;
|
||||
} catch (error) {
|
||||
console.error('[Gemini] Failed to discover Project ID:', error.response?.data || error.message);
|
||||
throw new Error('Could not discover a valid Google Cloud Project ID.');
|
||||
|
|
@ -294,8 +385,8 @@ export class GeminiApiService {
|
|||
}
|
||||
|
||||
async callApi(method, body, isRetry = false, retryCount = 0) {
|
||||
const maxRetries = this.config.REQUEST_MAX_RETRIES;
|
||||
const baseDelay = this.config.REQUEST_BASE_DELAY; // 1 second base delay
|
||||
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
|
||||
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay
|
||||
|
||||
try {
|
||||
const requestOptions = {
|
||||
|
|
@ -308,8 +399,11 @@ export class GeminiApiService {
|
|||
const res = await this.authClient.request(requestOptions);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
console.error(`[API] Error calling ${method}:`, error.response?.status, error.message);
|
||||
|
||||
// Handle 401 (Unauthorized) - refresh auth and retry once
|
||||
if ((error.response?.status === 400 || error.response?.status === 401) && !isRetry) {
|
||||
console.log('[API] Received 401. Refreshing auth and retrying...');
|
||||
console.log('[API] Received 401/400. Refreshing auth and retrying...');
|
||||
await this.initializeAuth(true);
|
||||
return this.callApi(method, body, true, retryCount);
|
||||
}
|
||||
|
|
@ -335,8 +429,8 @@ export class GeminiApiService {
|
|||
}
|
||||
|
||||
async * streamApi(method, body, isRetry = false, retryCount = 0) {
|
||||
const maxRetries = this.config.REQUEST_MAX_RETRIES;
|
||||
const baseDelay = this.config.REQUEST_BASE_DELAY; // 1 second base delay
|
||||
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
|
||||
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay
|
||||
|
||||
try {
|
||||
const requestOptions = {
|
||||
|
|
@ -355,8 +449,11 @@ export class GeminiApiService {
|
|||
}
|
||||
yield* this.parseSSEStream(res.data);
|
||||
} catch (error) {
|
||||
console.error(`[API] Error during stream ${method}:`, error.response?.status, error.message);
|
||||
|
||||
// Handle 401 (Unauthorized) - refresh auth and retry once
|
||||
if ((error.response?.status === 400 || error.response?.status === 401) && !isRetry) {
|
||||
console.log('[API] Received 401 during stream. Refreshing auth and retrying...');
|
||||
console.log('[API] Received 401/400 during stream. Refreshing auth and retrying...');
|
||||
await this.initializeAuth(true);
|
||||
yield* this.streamApi(method, body, true, retryCount);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export class OpenAIResponsesApiService {
|
|||
this.config = config;
|
||||
this.apiKey = config.OPENAI_API_KEY;
|
||||
this.baseUrl = config.OPENAI_BASE_URL || 'https://api.openai.com/v1';
|
||||
console.log(`[OpenAIResponsesApiService] Base URL: ${JSON.stringify(config)}`);
|
||||
// console.log(`[OpenAIResponsesApiService] Base URL: ${JSON.stringify(config)}`);
|
||||
this.axiosInstance = axios.create({
|
||||
baseURL: this.baseUrl,
|
||||
headers: {
|
||||
|
|
|
|||
|
|
@ -101,9 +101,9 @@ export class ProviderPoolManager {
|
|||
|
||||
if (provider.config.errorCount >= this.maxErrorCount) {
|
||||
provider.config.isHealthy = false;
|
||||
console.warn(`[ProviderPoolManager] Marked provider as unhealthy: ${JSON.stringify(providerConfig)} for type ${providerType}. Total errors: ${provider.config.errorCount}`);
|
||||
console.warn(`[ProviderPoolManager] Marked provider as unhealthy: ${providerConfig.uuid} for type ${providerType}. Total errors: ${provider.config.errorCount}`);
|
||||
} else {
|
||||
console.warn(`[ProviderPoolManager] Provider ${JSON.stringify(providerConfig)} for type ${providerType} error count: ${provider.config.errorCount}/${this.maxErrorCount}. Still healthy.`);
|
||||
console.warn(`[ProviderPoolManager] Provider ${providerConfig.uuid} for type ${providerType} error count: ${provider.config.errorCount}/${this.maxErrorCount}. Still healthy.`);
|
||||
}
|
||||
|
||||
// 优化1: 使用防抖保存
|
||||
|
|
@ -128,7 +128,7 @@ export class ProviderPoolManager {
|
|||
if (isInit) {
|
||||
provider.config.usageCount = 0; // Reset usage count on health recovery
|
||||
}
|
||||
console.log(`[ProviderPoolManager] Marked provider as healthy: ${JSON.stringify(providerConfig)} for type ${providerType}`);
|
||||
console.log(`[ProviderPoolManager] Marked provider as healthy: ${provider.config.uuid} for type ${providerType}`);
|
||||
|
||||
// 优化1: 使用防抖保存
|
||||
this._debouncedSave(providerType);
|
||||
|
|
@ -150,7 +150,7 @@ export class ProviderPoolManager {
|
|||
// Only attempt to health check unhealthy providers after a certain interval
|
||||
if (!providerStatus.config.isHealthy && providerStatus.config.lastErrorTime &&
|
||||
(now.getTime() - new Date(providerStatus.config.lastErrorTime).getTime() < this.healthCheckInterval)) {
|
||||
console.log(`[ProviderPoolManager] Skipping health check for ${JSON.stringify(providerConfig)} (${providerType}). Last error too recent.`);
|
||||
console.log(`[ProviderPoolManager] Skipping health check for ${providerConfig.uuid} (${providerType}). Last error too recent.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -162,20 +162,20 @@ export class ProviderPoolManager {
|
|||
if (!providerStatus.config.isHealthy) {
|
||||
// Provider was unhealthy but is now healthy
|
||||
this.markProviderHealthy(isInit, providerType, providerConfig);
|
||||
console.log(`[ProviderPoolManager] Health check for ${JSON.stringify(providerConfig)} (${providerType}): Marked Healthy (actual check)`);
|
||||
console.log(`[ProviderPoolManager] Health check for ${providerConfig.uuid} (${providerType}): Marked Healthy (actual check)`);
|
||||
} else {
|
||||
// Provider was already healthy and still is
|
||||
this.markProviderHealthy(isInit, providerType, providerConfig);
|
||||
console.log(`[ProviderPoolManager] Health check for ${JSON.stringify(providerConfig)} (${providerType}): Still Healthy`);
|
||||
console.log(`[ProviderPoolManager] Health check for ${providerConfig.uuid} (${providerType}): Still Healthy`);
|
||||
}
|
||||
} else {
|
||||
// Provider is not healthy
|
||||
console.warn(`[ProviderPoolManager] Health check for ${JSON.stringify(providerConfig)} (${providerType}) failed: Provider is not responding correctly.`);
|
||||
console.warn(`[ProviderPoolManager] Health check for ${providerConfig.uuid} (${providerType}) failed: Provider is not responding correctly.`);
|
||||
this.markProviderUnhealthy(providerType, providerConfig);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[ProviderPoolManager] Health check for ${JSON.stringify(providerConfig)} (${providerType}) failed: ${error.message}`);
|
||||
console.error(`[ProviderPoolManager] Health check for ${providerConfig.uuid} (${providerType}) failed: ${error.message}`);
|
||||
// If a health check fails, mark it unhealthy, which will update error count and lastErrorTime
|
||||
this.markProviderUnhealthy(providerType, providerConfig);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue