Spaces:
Runtime error
Runtime error
File size: 4,327 Bytes
077865a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | import { BaseProvider, providerHttpError } from './base.js';
import { contentToString } from '../lib/content.js';
/**
* Cloudflare Workers AI provider.
* API key format expected: "account_id:api_token"
* The account_id is extracted from the key to build the URL.
*/
export class CloudflareProvider extends BaseProvider {
platform = 'cloudflare';
name = 'Cloudflare Workers AI';
parseKey(apiKey) {
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) };
}
// Cloudflare's OpenAI-compat endpoint:
// - rejects `content: null` on assistant messages that carry tool_calls,
// even though the OpenAI spec allows it (collapse to '');
// - doesn't accept the array content envelope, so flatten to string.
normalizeMessages(messages) {
return messages.map(m => ({ ...m, content: contentToString(m.content) }));
}
async chatCompletion(apiKey, messages, modelId, options) {
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 providerHttpError(res, `Cloudflare API error ${res.status}: ${err.error?.message ?? err.errors?.[0]?.message ?? res.statusText}`);
}
const data = await res.json();
data._routed_via = { platform: 'cloudflare', model: modelId };
return data;
}
async *streamChatCompletion(apiKey, messages, modelId, options) {
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 providerHttpError(res, `Cloudflare API error ${res.status}: ${err.error?.message ?? err.errors?.[0]?.message ?? res.statusText}`);
}
yield* this.readSseStream(res);
}
async validateKey(apiKey) {
// Transport errors propagate — health.ts marks status='error' without
// counting toward auto-disable. Only confirmed bad/inactive tokens disable.
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; // unexpected non-2xx that isn't auth — don't disable
const data = await res.json();
return data.success === true && data.result?.status === 'active';
}
}
//# sourceMappingURL=cloudflare.js.map |