分享一个Deepseek格式转换的cf worker代码

让claude3.7调了一阵子。。最后勉强写了一个吧,至少能用。
就是把带think标签的挪到reasoning content这样可以省token,要不然很多时候一大半都是思考的token
这个流式可给他难坏了(可能是我问问题技艺不精)然后请求呢就是OpenAI格式的请求,URL变你的cf worker地址就可以,后面加/v1啥的,然后key我做了个调整key:https://endpoint.com/v1/chat/completionsendpoint一定要是完整的,实测可以从oneapi调用
欢迎各位佬可以提供更简单的实现

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  if (request.method !== 'POST') {
    return new Response('Method Not Allowed', { status: 405 });
  }

  try {
    // 解析请求体
    const requestBody = await request.json();
    
    // 从Authorization头中提取密钥和端点
    const authHeader = request.headers.get('Authorization') || '';
    const bearerMatch = authHeader.match(/^Bearer\s+(.+)$/i);
    const keyField = bearerMatch ? bearerMatch[1] : (requestBody.api_key || '');
    
    // 找到第一个冒号的位置,正确分割API密钥和端点URL
    const colonIndex = keyField.indexOf(':');
    if (colonIndex === -1) {
      return new Response(JSON.stringify({
        error: { message: 'Invalid API key format. Expected format: "key:https://endpoint.com"' }
      }), { status: 400, headers: { 'Content-Type': 'application/json' }});
    }
    
    const apiKey = keyField.substring(0, colonIndex);
    const endpoint = keyField.substring(colonIndex + 1);
    
    // 验证URL格式
    try {
      new URL(endpoint);
    } catch (e) {
      return new Response(JSON.stringify({
        error: { message: `Invalid endpoint URL: ${endpoint}` }
      }), { status: 400, headers: { 'Content-Type': 'application/json' }});
    }
    
    // 准备发送到DeepSeek的请求
    const isStreaming = requestBody.stream === true;
    const deepseekRequest = {
      model: requestBody.model || 'deepseek-chat',
      messages: requestBody.messages || [],
      temperature: requestBody.temperature,
      top_p: requestBody.top_p,
      max_tokens: requestBody.max_tokens,
      stream: isStreaming
    };
    
    // 发送请求到DeepSeek API
    const deepseekResponse = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiKey}`
      },
      body: JSON.stringify(deepseekRequest)
    });
    
    // 检查响应是否成功
    if (!deepseekResponse.ok) {
      const contentType = deepseekResponse.headers.get('Content-Type') || '';
      
      // 尝试读取错误信息
      if (contentType.includes('application/json')) {
        const errorData = await deepseekResponse.json();
        return new Response(JSON.stringify({
          error: { message: `DeepSeek API error: ${JSON.stringify(errorData)}` }
        }), { status: deepseekResponse.status, headers: { 'Content-Type': 'application/json' }});
      } else {
        // 非JSON错误响应
        const errorText = await deepseekResponse.text();
        return new Response(JSON.stringify({
          error: { message: `DeepSeek API returned non-JSON error: ${errorText.substring(0, 100)}...` }
        }), { status: 500, headers: { 'Content-Type': 'application/json' }});
      }
    }
    
    // 处理流式或非流式响应
    if (isStreaming) {
      return handleStreamingResponse(deepseekResponse);
    } else {
      return handleNonStreamingResponse(deepseekResponse);
    }
  } catch (error) {
    return new Response(JSON.stringify({
      error: { message: `Error: ${error.message}` }
    }), { status: 500, headers: { 'Content-Type': 'application/json' }});
  }
}

// 处理流式响应
function handleStreamingResponse(response) {
  const { readable, writable } = new TransformStream();
  
  processStreamResponse(response.body, writable);
  
  return new Response(readable, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive'
    }
  });
}

// 处理非流式响应
async function handleNonStreamingResponse(response) {
  try {
    const data = await response.json();
    const processedData = processNonStreamResponse(data);
    
    return new Response(JSON.stringify(processedData), {
      headers: { 'Content-Type': 'application/json' }
    });
  } catch (error) {
    return new Response(JSON.stringify({
      error: { message: `Error processing response: ${error.message}` }
    }), { status: 500, headers: { 'Content-Type': 'application/json' }});
  }
}

// 处理流式数据
async function processStreamResponse(responseBody, writable) {
  const writer = writable.getWriter();
  const reader = responseBody.getReader();
  const decoder = new TextDecoder();
  const encoder = new TextEncoder();
  
  try {
    // 对于流式处理,直接转发原始响应
    // 除非检测到<think>标签
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      
      // 转发原始数据
      await writer.write(value);
    }
  } catch (error) {
    console.error('Stream processing error:', error);
  } finally {
    await writer.close();
  }
}

// 对于请求内容的预处理,检查并提取<think>标签
async function processStreamResponse(responseBody, writable) {
  const writer = writable.getWriter();
  const reader = responseBody.getReader();
  const decoder = new TextDecoder();
  const encoder = new TextEncoder();
  
  let buffer = '';
  let insideThink = false;
  let thinkContent = '';
  let modelName = "deepseek-r1"; // Default, should be overridden with actual model from request
  let hasStartedRegularContent = false;
  
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      
      buffer += decoder.decode(value, { stream: true });
      
      // Process complete lines
      const lines = buffer.split('\n');
      buffer = lines.pop() || '';
      
      for (const line of lines) {
        if (line.trim() === '') continue;
        
        if (line === 'data: [DONE]') {
          await writer.write(encoder.encode('data: [DONE]\n\n'));
          continue;
        }
        
        if (line.startsWith('data: ')) {
          try {
            // Parse the JSON data
            const jsonData = JSON.parse(line.substring(6));
            
            // Update model name if available
            if (jsonData.model && !jsonData.model.includes("reasoning")) {
              modelName = jsonData.model;
            }
            
            // Check if we have content in the delta
            if (jsonData.choices && jsonData.choices[0] && jsonData.choices[0].delta) {
              const delta = jsonData.choices[0].delta;
              const content = delta.content || '';
              
              // Handle <think> tags and content
              if (content.includes('<think>') && !insideThink) {
                insideThink = true;
                
                // If <think> tag is not at the beginning, handle the text before it
                const parts = content.split('<think>');
                if (parts[0]) {
                  // Send any content before the <think> tag
                  const contentChunk = createContentChunk(parts[0], jsonData);
                  await writer.write(encoder.encode(`data: ${JSON.stringify(contentChunk)}\n\n`));
                }
                
                // Extract content from <think> tag if it's closed in the same chunk
                if (content.includes('</think>')) {
                  const thinkMatch = content.match(/<think>([\s\S]*?)<\/think>/);
                  if (thinkMatch && thinkMatch[1]) {
                    // Send thinking content
                    const chunks = thinkMatch[1].match(/.{1,5}|.+/g) || [];
                    for (const chunk of chunks) {
                      const reasoningChunk = createReasoningChunk(chunk, jsonData, modelName);
                      await writer.write(encoder.encode(`data: ${JSON.stringify(reasoningChunk)}\n\n`));
                    }
                    
                    // Handle content after </think>
                    const afterThink = content.split('</think>')[1] || '';
                    if (afterThink) {
                      hasStartedRegularContent = true;
                      const contentChunk = createContentChunk(afterThink, jsonData);
                      await writer.write(encoder.encode(`data: ${JSON.stringify(contentChunk)}\n\n`));
                    }
                    
                    insideThink = false;
                  }
                } else {
                  // Only the beginning of think tag, store what comes after <think>
                  thinkContent = parts[1] || '';
                }
              } 
              else if (content.includes('</think>') && insideThink) {
                // Handle end of thinking
                const parts = content.split('</think>');
                thinkContent += parts[0];
                
                // Send the complete thinking content
                if (thinkContent.trim()) {
                  const chunks = thinkContent.match(/.{1,5}|.+/g) || [];
                  for (const chunk of chunks) {
                    const reasoningChunk = createReasoningChunk(chunk, jsonData, modelName);
                    await writer.write(encoder.encode(`data: ${JSON.stringify(reasoningChunk)}\n\n`));
                  }
                }
                
                // Reset thinking state
                insideThink = false;
                thinkContent = '';
                
                // Handle content after </think>
                if (parts[1]) {
                  hasStartedRegularContent = true;
                  const contentChunk = createContentChunk(parts[1], jsonData);
                  await writer.write(encoder.encode(`data: ${JSON.stringify(contentChunk)}\n\n`));
                }
              }
              else if (insideThink) {
                // Inside thinking, accumulate content
                thinkContent += content;
                
                // Send thinking chunks for better streaming experience
                if (thinkContent.length > 10) {
                  const chunks = thinkContent.match(/.{1,5}|.+/g) || [];
                  for (const chunk of chunks) {
                    const reasoningChunk = createReasoningChunk(chunk, jsonData, modelName);
                    await writer.write(encoder.encode(`data: ${JSON.stringify(reasoningChunk)}\n\n`));
                  }
                  thinkContent = '';
                }
              }
              else {
                // Regular content outside of thinking
                hasStartedRegularContent = true;
                const contentChunk = createContentChunk(content, jsonData);
                await writer.write(encoder.encode(`data: ${JSON.stringify(contentChunk)}\n\n`));
              }
            } else {
              // No delta content or other message structure, pass through
              await writer.write(encoder.encode(`${line}\n\n`));
            }
          } catch (e) {
            // JSON parsing error, just forward the line
            console.error("Error parsing JSON:", e);
            await writer.write(encoder.encode(`${line}\n\n`));
          }
        } else {
          // Not a data line, forward as is
          await writer.write(encoder.encode(`${line}\n`));
        }
      }
    }
    
    // Process any remaining buffer
    if (buffer.trim()) {
      await writer.write(encoder.encode(`${buffer}\n\n`));
    }
    
    // If we have any remaining thinking content, send it
    if (insideThink && thinkContent.trim()) {
      const timestamp = Math.floor(Date.now() / 1000);
      const id = generateRandomId();
      const reasoningChunk = createReasoningChunk(thinkContent, { created: timestamp, id }, modelName);
      await writer.write(encoder.encode(`data: ${JSON.stringify(reasoningChunk)}\n\n`));
    }
  } catch (error) {
    console.error('Stream processing error:', error);
  } finally {
    await writer.close();
  }
}

// Helper function to create a reasoning chunk
function createReasoningChunk(content, originalData, modelName) {
  return {
    id: originalData.id || generateRandomId(),
    created: originalData.created || Math.floor(Date.now() / 1000),
    model: `${modelName}`,  // Use the original model name
    object: "chat.completion.chunk",
    choices: [{
      delta: {
        content: "",
        reasoning_content: content,
        role: "assistant"
      },
      index: 0
    }],
    usage: null
  };
}

// Helper function to create a content chunk
function createContentChunk(content, originalData) {
  return {
    id: originalData.id || generateRandomId(),
    created: originalData.created || Math.floor(Date.now() / 1000),
    model: originalData.model,
    object: "chat.completion.chunk",
    choices: [{
      delta: {
        content: content,
        role: "assistant"
      },
      index: 0
    }],
    usage: null
  };
}

// Generate a random ID for responses
function generateRandomId() {
  return Array.from({ length: 32 }, () => 
    Math.floor(Math.random() * 16).toString(16)
  ).join('');
}



// 将思考内容作为reasoning_content发送
async function sendThinkContentAsReasoning(content, writer, encoder) {
  // 将内容分成小块发送,模拟DeepSeek的流式行为
  const chunks = content.match(/.{1,5}|.+/g) || [];
  
  for (const chunk of chunks) {
    const data = {
      choices: [{
        delta: {
          content: "",
          reasoning_content: chunk,
          role: "assistant"
        },
        index: 0
      }],
      created: Math.floor(Date.now() / 1000),
      id: generateRandomId(),
      model: "deepseek-reasoning", // 使用请求中的模型名称更好
      object: "chat.completion.chunk",
      usage: null
    };
    
    await writer.write(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
  }
}

// 将内容作为普通内容发送
async function sendAsContent(content, writer, encoder) {
  const data = {
    choices: [{
      delta: {
        content: content,
        role: "assistant"
      },
      index: 0
    }],
    created: Math.floor(Date.now() / 1000),
    id: generateRandomId(),
    model: "deepseek-reasoning", // 使用请求中的模型名称更好
    object: "chat.completion.chunk",
    usage: null
  };
  
  await writer.write(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
}

// 生成随机ID
function generateRandomId() {
  return Array.from({length: 32}, () => 
    Math.floor(Math.random() * 16).toString(16)
  ).join('');
}

// 处理非流式响应中的<think>标签
function processNonStreamResponse(responseData) {
  if (responseData.choices && responseData.choices[0] && responseData.choices[0].message) {
    const content = responseData.choices[0].message.content || '';
    
    // 检查是否包含<think>标签
    const thinkMatch = content.match(/<think>([\s\S]*?)<\/think>/);
    
    if (thinkMatch) {
      // 提取思考内容
      const thinkContent = thinkMatch[1];
      
      // 更新消息内容,移除<think>标签
      responseData.choices[0].message.content = content.replace(/<think>[\s\S]*?<\/think>/, '').trim();
      
      // 添加reasoning_content字段
      responseData.choices[0].message.reasoning_content = thinkContent;
    }
  }
  
  return responseData;
}
9 个赞

感谢分享w

感谢分享了

deepseek 本身不就是兼容openai格式吗tieba_087

感谢大佬分享

感谢大佬!

此话题已在最后回复的 30 天后被自动关闭。不再允许新回复。