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