| import type { |
| ChatMessage, |
| ChatCompletionResponse, |
| ChatCompletionChunk, |
| } from '@freellmapi/shared/types.js'; |
| import { BaseProvider, type CompletionOptions } from './base.js'; |
| import { contentToString } from '../lib/content.js'; |
|
|
| |
| |
| |
| |
| |
| export class CloudflareProvider extends BaseProvider { |
| readonly platform = 'cloudflare' as const; |
| readonly name = 'Cloudflare Workers AI'; |
|
|
| private parseKey(apiKey: string): { accountId: string; token: string } { |
| const sep = apiKey.indexOf(':'); |
| if (sep === -1) throw new Error('Cloudflare key must be in format "account_id:api_token"'); |
| return { accountId: apiKey.slice(0, sep), token: apiKey.slice(sep + 1) }; |
| } |
|
|
| |
| |
| |
| |
| private normalizeMessages(messages: ChatMessage[]): ChatMessage[] { |
| return messages.map(m => ({ ...m, content: contentToString(m.content) })); |
| } |
|
|
| async chatCompletion( |
| apiKey: string, |
| messages: ChatMessage[], |
| modelId: string, |
| options?: CompletionOptions, |
| ): Promise<ChatCompletionResponse> { |
| const { accountId, token } = this.parseKey(apiKey); |
| const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`; |
|
|
| const res = await this.fetchWithTimeout(url, { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${token}`, |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ |
| model: modelId, |
| messages: this.normalizeMessages(messages), |
| temperature: options?.temperature, |
| max_tokens: options?.max_tokens, |
| top_p: options?.top_p, |
| tools: options?.tools, |
| tool_choice: options?.tool_choice, |
| parallel_tool_calls: options?.parallel_tool_calls, |
| }), |
| }); |
|
|
| if (!res.ok) { |
| const err = await res.json().catch(() => ({})); |
| throw new Error(`Cloudflare API error ${res.status}: ${(err as any).error?.message ?? (err as any).errors?.[0]?.message ?? res.statusText}`); |
| } |
|
|
| const data = await res.json() as ChatCompletionResponse; |
| data._routed_via = { platform: 'cloudflare', model: modelId }; |
| return data; |
| } |
|
|
| async *streamChatCompletion( |
| apiKey: string, |
| messages: ChatMessage[], |
| modelId: string, |
| options?: CompletionOptions, |
| ): AsyncGenerator<ChatCompletionChunk> { |
| const { accountId, token } = this.parseKey(apiKey); |
| const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`; |
|
|
| const res = await this.fetchWithTimeout(url, { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${token}`, |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ |
| model: modelId, |
| messages: this.normalizeMessages(messages), |
| temperature: options?.temperature, |
| max_tokens: options?.max_tokens, |
| top_p: options?.top_p, |
| tools: options?.tools, |
| tool_choice: options?.tool_choice, |
| parallel_tool_calls: options?.parallel_tool_calls, |
| stream: true, |
| }), |
| }); |
|
|
| if (!res.ok) { |
| const err = await res.json().catch(() => ({})); |
| throw new Error(`Cloudflare API error ${res.status}: ${(err as any).error?.message ?? (err as any).errors?.[0]?.message ?? res.statusText}`); |
| } |
|
|
| const reader = res.body?.getReader(); |
| if (!reader) throw new Error('No response body'); |
|
|
| const decoder = new TextDecoder(); |
| let buffer = ''; |
|
|
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) break; |
|
|
| buffer += decoder.decode(value, { stream: true }); |
| const lines = buffer.split('\n'); |
| buffer = lines.pop() ?? ''; |
|
|
| for (const line of lines) { |
| const trimmed = line.trim(); |
| if (!trimmed || !trimmed.startsWith('data: ')) continue; |
| const data = trimmed.slice(6); |
| if (data === '[DONE]') return; |
| try { |
| yield JSON.parse(data) as ChatCompletionChunk; |
| } catch { |
| |
| } |
| } |
| } |
| } |
|
|
| async validateKey(apiKey: string): Promise<boolean> { |
| |
| |
| const { token } = this.parseKey(apiKey); |
| const res = await this.fetchWithTimeout( |
| 'https://api.cloudflare.com/client/v4/user/tokens/verify', |
| { method: 'GET', headers: { 'Authorization': `Bearer ${token}` } }, |
| 10000, |
| ); |
| if (res.status === 401 || res.status === 403) return false; |
| if (!res.ok) return true; |
| const data = await res.json() as any; |
| return data.success === true && data.result?.status === 'active'; |
| } |
| } |
|
|