Cloudflare搭建OpenAI转发脚本


这是一个将兼容OpenAI API的会话请求转发到Github Copilot Chat API的Cloudflare Worker脚本,模型参数仅支持gpt-4gpt-3.5-turbo,实测使用其他模型均会以默认的3.5处理(对比OpenAI API的返回结果,猜测应该是最早的版本gpt-4-0314gpt-3.5-turbo-0301

使用方法

1. 创建一个KV容器

  • 创建命名空间,名字自己起

2. 创建一个Cloudflare Worker


  • 填写自己的worker名称,先创建并部署,一会修改代码

3. 将KV容器绑定到Worker中(可以在Settings -> Variables下找到)

绑定好再做下一步,左值key为变量名称自己起,右值选择kv容器名称

4. 将如下内容粘贴到Worker编辑器页面中

const GithubCopilotChat = GITHUB_COPILOT_CHAT; // 此处替换你绑定KV namespace的名称

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

async function handleRequest(request) {
  const corsHeaders = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
    'Access-Control-Allow-Headers': '*',
  }

  if (request.method === 'OPTIONS') {
    return new Response(null, {
      status: 200,
      headers: corsHeaders,
    })
  }

  if (request.method === 'GET') {
    let data = {
      object: "list",
      data: [
        { "id": "gpt-4", "object": "model", "created": 1687882411, "owned_by": "openai" },
        { "id": "gpt-3.5-turbo", "object": "model", "created": 1677610602, "owned_by": "openai" },
      ],
    }
    return new Response(JSON.stringify(data), {
      status: 200,
      headers: corsHeaders,
    })
  }

  if (request.method !== 'POST') {
    return new Response('Method Not Allowed', {
      status: 405,
      headers: corsHeaders,
    })
  }

  try {
    const authorizationHeader = request.headers.get('Authorization') || ''
    const match = authorizationHeader.match(/^Bearer\s+(.*)$/)
    if (!match) {
      throw new Error('Missing or malformed Authorization header')
    }
    const githubToken = match[1]

    const copilotToken = await getCopilotToken(githubToken)

    const headers = await createHeaders(copilotToken);

    const requestData = await request.json()

    const openAIResponse = await fetch('https://api.githubcopilot.com/chat/completions', {
      method: 'POST',
      headers: {
        ...headers,
      },
      body: typeof requestData === 'object' ? JSON.stringify(requestData) : '{}',
    })

    const { readable, writable } = new TransformStream();
    streamResponse(openAIResponse, writable, requestData);
    return new Response(readable, {
      headers: {
        ...corsHeaders,
        'Content-Type': 'text/event-stream; charset=utf-8',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
      }
    });
  } catch (error) {
    return new Response(error.message, {
      status: 500,
      headers: corsHeaders,
    });
  }
}

async function streamResponse(openAIResponse, writable, requestData) {
  const reader = openAIResponse.body.getReader();
  const writer = writable.getWriter();
  const encoder = new TextEncoder();
  const decoder = new TextDecoder("utf-8");
  let buffer = "";

  function push() {
    reader.read().then(({ done, value }) => {
      if (done) {
        writer.close();
        return;
      }
      const chunk = decoder.decode(value, { stream: true });
      let to_send = "";
      (buffer + chunk).split("data: ").forEach((raw) => {
        if (raw === "")
          return;
        else if (!raw.endsWith("\n\n"))
          buffer = raw;
        else if (raw.startsWith("[DONE]"))
          to_send += "data: [DONE]\n\n";
        else {
          let data = JSON.parse(raw);
          if (data.choices[0].delta?.content === null)
            data.choices[0].delta.content = "";
          if (data.choices[0].finish_reason === undefined)
            data.choices[0].finish_reason = null;
          if (data.model === undefined && requestData.model !== undefined)
            data.model = requestData.model;
          if (data.object === undefined)
            data.object = "chat.completion.chunk";
          to_send += `data: ${JSON.stringify(data)}\n\n`;
        }
      });
      writer.write(encoder.encode(to_send));
      push();
    }).catch(error => {
      console.error(error);
      writer.close();
    });
  }

  push();
}

async function getCopilotToken(githubToken) {
  let tokenData = await GithubCopilotChat.get(githubToken, "json");
  
  if (tokenData && tokenData.expires_at * 1000 > Date.now()) {
    return tokenData.token;
  }

  const getTokenUrl = 'https://api.github.com/copilot_internal/v2/token';
  const response = await fetch(getTokenUrl, {
    headers: {
      'Authorization': `token ${githubToken}`, 
      'User-Agent': 'GitHubCopilotChat/0.11.1',
    }
  });

  if (!response.ok) {
    const errorResponse = await response.text();
    console.error('Failed to get Copilot token from GitHub:', errorResponse);
    throw new Error('Failed to get Copilot token from GitHub:');
  }

  const data = await response.json();

  await GithubCopilotChat.put(githubToken, JSON.stringify({ token: data.token, expires_at: data.expires_at }), {
    expirationTtl: data.expires_at
  });

  return data.token;
}

async function createHeaders(copilotToken) {
  function genHexStr(length) {
    const arr = new Uint8Array(length / 2);
    crypto.getRandomValues(arr);
    return Array.from(arr, (byte) => byte.toString(16).padStart(2, '0')).join('');
  }

  return {
    'Authorization': `Bearer ${copilotToken}`,
    'X-Request-Id': `${genHexStr(8)}-${genHexStr(4)}-${genHexStr(4)}-${genHexStr(4)}-${genHexStr(12)}`,
    'X-Github-Api-Version': "2023-07-07",
    'Vscode-Sessionid': `${genHexStr(8)}-${genHexStr(4)}-${genHexStr(4)}-${genHexStr(4)}-${genHexStr(25)}`,
    'Vscode-Machineid': genHexStr(64),
    'Editor-Version': 'vscode/1.85.1',
    'Editor-Plugin-Version': 'copilot-chat/0.11.1',
    'Openai-Organization': 'github-copilot',
    'Openai-Intent': 'conversation-panel',
    'Content-Type': 'text/event-stream; charset=utf-8',
    'User-Agent': 'GitHubCopilotChat/0.11.1',
    'Accept': '*/*',
    'Accept-Encoding': 'gzip,deflate,br',
    'Connection': 'close'
  };
}

5. 修改代码第一行的GITHUB_COPILOT_CHAT为你绑定KV namespace时使用的变量名称

6. 保存并部署Worker

7. 可添加自己的域名解析


文章作者: 阿坤
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 阿坤 !
  目录