Created
February 19, 2025 13:05
-
-
Save BlueSkyXN/0f4a825b3694909fac3db58b648881cb to your computer and use it in GitHub Desktop.
cerebras-api.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const CEREBRAS_API_URL = 'https://gateway.ai.cloudflare.com/v1/xxx/cerebras/cerebras'; | |
| const MAX_RETRIES = 3; | |
| const RETRY_DELAY = 1000; | |
| // Debug 函数 | |
| async function debugFetch(url, options) { | |
| console.log('Request URL:', url); | |
| console.log('Request Headers:', JSON.stringify(options.headers)); | |
| try { | |
| const response = await fetch(url, options); | |
| console.log('Response Status:', response.status); | |
| const contentType = response.headers.get('content-type'); | |
| console.log('Response Content-Type:', contentType); | |
| const responseClone = response.clone(); | |
| try { | |
| const text = await responseClone.text(); | |
| console.log('Response Body:', text.substring(0, 200) + '...'); | |
| } catch (e) { | |
| console.log('Failed to read response body:', e); | |
| } | |
| return response; | |
| } catch (error) { | |
| console.log('Fetch Error:', error); | |
| throw error; | |
| } | |
| } | |
| // 重试逻辑 | |
| async function fetchWithRetry(url, options, retries = MAX_RETRIES) { | |
| try { | |
| const response = await debugFetch(url, options); | |
| if (!response.ok) { | |
| const contentType = response.headers.get('content-type'); | |
| let errorMessage; | |
| if (contentType && contentType.includes('application/json')) { | |
| const error = await response.json(); | |
| errorMessage = error.message || error.error || 'Unknown error'; | |
| } else { | |
| errorMessage = await response.text(); | |
| } | |
| throw new Error(`HTTP error! status: ${response.status}, message: ${errorMessage}`); | |
| } | |
| return response; | |
| } catch (error) { | |
| if (retries > 0) { | |
| console.log(`Retry attempt ${MAX_RETRIES - retries + 1}, Error: ${error.message}`); | |
| await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); | |
| return fetchWithRetry(url, options, retries - 1); | |
| } | |
| throw error; | |
| } | |
| } | |
| // 转换请求参数 | |
| function convertRequestParams(openaiParams) { | |
| const cereParams = { ...openaiParams }; | |
| if (cereParams.max_tokens) { | |
| cereParams.max_completion_tokens = cereParams.max_tokens; | |
| delete cereParams.max_tokens; | |
| } | |
| return cereParams; | |
| } | |
| // 转换单个数据块的响应 | |
| function convertChunkResponse(cereChunk) { | |
| if (cereChunk === '[DONE]') { | |
| return 'data: [DONE]\n\n'; | |
| } | |
| try { | |
| const chunk = JSON.parse(cereChunk); | |
| const {time_info, system_fingerprint, ...restChunk} = chunk; | |
| return `data: ${JSON.stringify(restChunk)}\n\n`; | |
| } catch (error) { | |
| console.error('Error parsing chunk:', error); | |
| return ''; | |
| } | |
| } | |
| // 转换完整响应 | |
| function convertFullResponse(cereResponse) { | |
| const {time_info, system_fingerprint, ...openaiResponse} = cereResponse; | |
| return openaiResponse; | |
| } | |
| // 处理流式响应 | |
| async function handleStreamResponse(response) { | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ''; | |
| return new ReadableStream({ | |
| async start(controller) { | |
| try { | |
| while (true) { | |
| const {done, value} = await reader.read(); | |
| if (done) { | |
| if (buffer) { | |
| const chunk = convertChunkResponse(buffer); | |
| if (chunk) { | |
| controller.enqueue(new TextEncoder().encode(chunk)); | |
| } | |
| } | |
| controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n')); | |
| break; | |
| } | |
| buffer += decoder.decode(value, {stream: true}); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop() || ''; | |
| for (const line of lines) { | |
| const trimmedLine = line.trim(); | |
| if (trimmedLine.startsWith('data: ')) { | |
| const chunk = convertChunkResponse(trimmedLine.slice(6)); | |
| if (chunk) { | |
| controller.enqueue(new TextEncoder().encode(chunk)); | |
| } | |
| } | |
| } | |
| } | |
| } catch (error) { | |
| controller.error(error); | |
| } finally { | |
| controller.close(); | |
| } | |
| }, | |
| }); | |
| } | |
| // 错误处理 | |
| function handleError(error) { | |
| console.error('Error details:', error); | |
| let status = 500; | |
| let message = error.message || 'Internal server error'; | |
| if (error.message.includes('Failed to fetch') || error.message.includes('ECONNREFUSED')) { | |
| status = 502; | |
| message = 'Failed to connect to Cerebras API: Connection refused'; | |
| } else if (error.message.includes('Missing API key')) { | |
| status = 401; | |
| } else if (error.message.includes('Missing required parameters')) { | |
| status = 400; | |
| } | |
| return new Response( | |
| JSON.stringify({ | |
| error: { | |
| message: message, | |
| type: 'adapter_error', | |
| code: status, | |
| details: error.message | |
| } | |
| }), | |
| { | |
| status: status, | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Access-Control-Allow-Origin': '*', | |
| 'Access-Control-Allow-Methods': 'POST, OPTIONS', | |
| 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Cerebras-Key' | |
| } | |
| } | |
| ); | |
| } | |
| // 主处理函数 | |
| async function handleRequest(request) { | |
| try { | |
| if (request.method === 'OPTIONS') { | |
| return new Response(null, { | |
| headers: { | |
| 'Access-Control-Allow-Origin': '*', | |
| 'Access-Control-Allow-Methods': 'POST, OPTIONS', | |
| 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Cerebras-Key', | |
| 'Access-Control-Max-Age': '86400' | |
| } | |
| }); | |
| } | |
| if (request.method !== 'POST') { | |
| throw new Error('Method not allowed'); | |
| } | |
| const requestBody = await request.json(); | |
| const isStream = requestBody.stream === true; | |
| if (!requestBody.model || !requestBody.messages) { | |
| throw new Error('Missing required parameters'); | |
| } | |
| const cereParams = convertRequestParams(requestBody); | |
| const cereKey = request.headers.get('X-Cerebras-Key') || | |
| request.headers.get('Authorization')?.replace('Bearer ', ''); | |
| if (!cereKey) { | |
| throw new Error('Missing API key'); | |
| } | |
| // 发送请求到 Cloudflare AI Gateway | |
| const cereResponse = await fetchWithRetry( | |
| `${CEREBRAS_API_URL}/chat/completions`, | |
| { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${cereKey}`, | |
| 'Accept': isStream ? 'text/event-stream' : 'application/json' | |
| }, | |
| body: JSON.stringify(cereParams) | |
| } | |
| ); | |
| if (isStream) { | |
| const stream = await handleStreamResponse(cereResponse); | |
| return new Response(stream, { | |
| headers: { | |
| 'Content-Type': 'text/event-stream', | |
| 'Cache-Control': 'no-cache', | |
| 'Connection': 'keep-alive', | |
| 'Access-Control-Allow-Origin': '*' | |
| } | |
| }); | |
| } | |
| const cereData = await cereResponse.json(); | |
| const openaiData = convertFullResponse(cereData); | |
| return new Response(JSON.stringify(openaiData), { | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Access-Control-Allow-Origin': '*' | |
| } | |
| }); | |
| } catch (error) { | |
| return handleError(error); | |
| } | |
| } | |
| // Worker 入口点 | |
| addEventListener('fetch', event => { | |
| const url = new URL(event.request.url); | |
| if (url.pathname.startsWith('/v1/chat/completions')) { | |
| event.respondWith(handleRequest(event.request)); | |
| } else { | |
| event.respondWith(new Response( | |
| JSON.stringify({ | |
| error: { | |
| message: 'Not found', | |
| type: 'adapter_error', | |
| code: 404 | |
| } | |
| }), | |
| { | |
| status: 404, | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Access-Control-Allow-Origin': '*' | |
| } | |
| } | |
| )); | |
| } | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment