@lobehub/chat 1.29.6 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.env.example +5 -0
  2. package/CHANGELOG.md +50 -0
  3. package/Dockerfile +2 -0
  4. package/Dockerfile.database +2 -0
  5. package/docs/usage/features/database.zh-CN.mdx +3 -3
  6. package/locales/ar/modelProvider.json +12 -0
  7. package/locales/bg-BG/modelProvider.json +12 -0
  8. package/locales/de-DE/modelProvider.json +12 -0
  9. package/locales/en-US/modelProvider.json +12 -0
  10. package/locales/es-ES/modelProvider.json +12 -0
  11. package/locales/fr-FR/modelProvider.json +12 -0
  12. package/locales/it-IT/modelProvider.json +12 -0
  13. package/locales/ja-JP/modelProvider.json +12 -0
  14. package/locales/ko-KR/modelProvider.json +12 -0
  15. package/locales/nl-NL/modelProvider.json +12 -0
  16. package/locales/pl-PL/modelProvider.json +12 -0
  17. package/locales/pt-BR/modelProvider.json +12 -0
  18. package/locales/ru-RU/modelProvider.json +12 -0
  19. package/locales/tr-TR/modelProvider.json +12 -0
  20. package/locales/vi-VN/modelProvider.json +12 -0
  21. package/locales/zh-CN/modelProvider.json +12 -0
  22. package/locales/zh-TW/modelProvider.json +12 -0
  23. package/package.json +2 -2
  24. package/src/app/(main)/settings/llm/ProviderList/Cloudflare/index.tsx +43 -0
  25. package/src/app/(main)/settings/llm/ProviderList/providers.tsx +6 -0
  26. package/src/config/llm.ts +17 -0
  27. package/src/config/modelProviders/cloudflare.ts +89 -0
  28. package/src/config/modelProviders/index.ts +8 -0
  29. package/src/config/modelProviders/xai.ts +29 -0
  30. package/src/const/auth.ts +2 -0
  31. package/src/const/settings/llm.ts +10 -0
  32. package/src/libs/agent-runtime/AgentRuntime.ts +14 -1
  33. package/src/libs/agent-runtime/cloudflare/index.test.ts +648 -0
  34. package/src/libs/agent-runtime/cloudflare/index.ts +123 -0
  35. package/src/libs/agent-runtime/types/type.ts +2 -0
  36. package/src/libs/agent-runtime/utils/cloudflareHelpers.test.ts +339 -0
  37. package/src/libs/agent-runtime/utils/cloudflareHelpers.ts +134 -0
  38. package/src/libs/agent-runtime/xai/index.test.ts +255 -0
  39. package/src/libs/agent-runtime/xai/index.ts +10 -0
  40. package/src/locales/default/modelProvider.ts +13 -1
  41. package/src/server/globalConfig/index.ts +16 -0
  42. package/src/server/modules/AgentRuntime/index.ts +18 -0
  43. package/src/services/_auth.ts +9 -0
  44. package/src/services/chat.ts +7 -0
  45. package/src/store/user/slices/modelList/selectors/keyVaults.ts +2 -0
  46. package/src/store/user/slices/modelList/selectors/modelConfig.ts +2 -0
  47. package/src/types/user/settings/keyVaults.ts +7 -0
@@ -0,0 +1,123 @@
1
+ import { ChatModelCard } from '@/types/llm';
2
+
3
+ import { LobeRuntimeAI } from '../BaseAI';
4
+ import { AgentRuntimeErrorType } from '../error';
5
+ import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from '../types';
6
+ import {
7
+ CloudflareStreamTransformer,
8
+ DEFAULT_BASE_URL_PREFIX,
9
+ convertModelManifest,
10
+ desensitizeCloudflareUrl,
11
+ fillUrl,
12
+ } from '../utils/cloudflareHelpers';
13
+ import { AgentRuntimeError } from '../utils/createError';
14
+ import { debugStream } from '../utils/debugStream';
15
+ import { StreamingResponse } from '../utils/response';
16
+ import { createCallbacksTransformer } from '../utils/streams';
17
+
18
+ export interface LobeCloudflareParams {
19
+ apiKey?: string;
20
+ baseURLOrAccountID?: string;
21
+ }
22
+
23
+ export class LobeCloudflareAI implements LobeRuntimeAI {
24
+ baseURL: string;
25
+ accountID: string;
26
+ apiKey?: string;
27
+
28
+ constructor({ apiKey, baseURLOrAccountID }: LobeCloudflareParams) {
29
+ if (!baseURLOrAccountID) {
30
+ throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey);
31
+ }
32
+ if (baseURLOrAccountID.startsWith('http')) {
33
+ this.baseURL = baseURLOrAccountID.endsWith('/')
34
+ ? baseURLOrAccountID
35
+ : baseURLOrAccountID + '/';
36
+ // Try get accountID from baseURL
37
+ this.accountID = baseURLOrAccountID.replaceAll(/^.*\/([\dA-Fa-f]{32})\/.*$/g, '$1');
38
+ } else {
39
+ if (!apiKey) {
40
+ throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey);
41
+ }
42
+ this.accountID = baseURLOrAccountID;
43
+ this.baseURL = fillUrl(baseURLOrAccountID);
44
+ }
45
+ this.apiKey = apiKey;
46
+ }
47
+
48
+ async chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions): Promise<Response> {
49
+ try {
50
+ const { model, tools, ...restPayload } = payload;
51
+ const functions = tools?.map((tool) => tool.function);
52
+ const headers = options?.headers || {};
53
+ if (this.apiKey) {
54
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
55
+ }
56
+ const url = new URL(model, this.baseURL);
57
+ const response = await fetch(url, {
58
+ body: JSON.stringify({ tools: functions, ...restPayload }),
59
+ headers: { 'Content-Type': 'application/json', ...headers },
60
+ method: 'POST',
61
+ signal: options?.signal,
62
+ });
63
+
64
+ const desensitizedEndpoint = desensitizeCloudflareUrl(url.toString());
65
+
66
+ switch (response.status) {
67
+ case 400: {
68
+ throw AgentRuntimeError.chat({
69
+ endpoint: desensitizedEndpoint,
70
+ error: response,
71
+ errorType: AgentRuntimeErrorType.ProviderBizError,
72
+ provider: ModelProvider.Cloudflare,
73
+ });
74
+ }
75
+ }
76
+
77
+ // Only tee when debugging
78
+ let responseBody: ReadableStream;
79
+ if (process.env.DEBUG_CLOUDFLARE_CHAT_COMPLETION === '1') {
80
+ const [prod, useForDebug] = response.body!.tee();
81
+ debugStream(useForDebug).catch();
82
+ responseBody = prod;
83
+ } else {
84
+ responseBody = response.body!;
85
+ }
86
+
87
+ return StreamingResponse(
88
+ responseBody
89
+ .pipeThrough(new TransformStream(new CloudflareStreamTransformer()))
90
+ .pipeThrough(createCallbacksTransformer(options?.callback)),
91
+ { headers: options?.headers },
92
+ );
93
+ } catch (error) {
94
+ const desensitizedEndpoint = desensitizeCloudflareUrl(this.baseURL);
95
+
96
+ throw AgentRuntimeError.chat({
97
+ endpoint: desensitizedEndpoint,
98
+ error: error as any,
99
+ errorType: AgentRuntimeErrorType.ProviderBizError,
100
+ provider: ModelProvider.Cloudflare,
101
+ });
102
+ }
103
+ }
104
+
105
+ async models(): Promise<ChatModelCard[]> {
106
+ const url = `${DEFAULT_BASE_URL_PREFIX}/client/v4/accounts/${this.accountID}/ai/models/search`;
107
+ const response = await fetch(url, {
108
+ headers: {
109
+ 'Authorization': `Bearer ${this.apiKey}`,
110
+ 'Content-Type': 'application/json',
111
+ },
112
+ method: 'GET',
113
+ });
114
+ const j = await response.json();
115
+ const models: any[] = j['result'].filter(
116
+ (model: any) => model['task']['name'] === 'Text Generation',
117
+ );
118
+ const chatModels: ChatModelCard[] = models
119
+ .map((model) => convertModelManifest(model))
120
+ .sort((a, b) => a.displayName.localeCompare(b.displayName));
121
+ return chatModels;
122
+ }
123
+ }
@@ -28,6 +28,7 @@ export enum ModelProvider {
28
28
  Azure = 'azure',
29
29
  Baichuan = 'baichuan',
30
30
  Bedrock = 'bedrock',
31
+ Cloudflare = 'cloudflare',
31
32
  DeepSeek = 'deepseek',
32
33
  FireworksAI = 'fireworksai',
33
34
  Github = 'github',
@@ -52,6 +53,7 @@ export enum ModelProvider {
52
53
  TogetherAI = 'togetherai',
53
54
  Upstage = 'upstage',
54
55
  Wenxin = 'wenxin',
56
+ XAI = 'xai',
55
57
  ZeroOne = 'zeroone',
56
58
  ZhiPu = 'zhipu',
57
59
  }
@@ -0,0 +1,339 @@
1
+ // @vitest-environment node
2
+ import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import * as desensitizeTool from '../utils/desensitizeUrl';
5
+ import {
6
+ CloudflareStreamTransformer,
7
+ desensitizeCloudflareUrl,
8
+ fillUrl,
9
+ getModelBeta,
10
+ getModelDisplayName,
11
+ getModelFunctionCalling,
12
+ getModelTokens,
13
+ } from './cloudflareHelpers';
14
+
15
+ //const {
16
+ // getModelBeta,
17
+ // getModelDisplayName,
18
+ // getModelFunctionCalling,
19
+ // getModelTokens,
20
+ //} = require('./cloudflareHelpers');
21
+
22
+ //const cloudflareHelpers = require('./cloudflareHelpers');
23
+ //const getModelBeta = cloudflareHelpers.__get__('getModelBeta');
24
+ //const getModelDisplayName = cloudflareHelpers.__get__('getModelDisplayName');
25
+ //const getModelFunctionCalling = cloudflareHelpers.__get__('getModelFunctionCalling');
26
+ //const getModelTokens = cloudflareHelpers.__get__('getModelTokens');
27
+
28
+ afterEach(() => {
29
+ vi.restoreAllMocks();
30
+ });
31
+
32
+ describe('cloudflareHelpers', () => {
33
+ describe('CloudflareStreamTransformer', () => {
34
+ let transformer: CloudflareStreamTransformer;
35
+ beforeEach(() => {
36
+ transformer = new CloudflareStreamTransformer();
37
+ });
38
+
39
+ describe('parseChunk', () => {
40
+ let chunks: string[];
41
+ let controller: TransformStreamDefaultController;
42
+
43
+ beforeEach(() => {
44
+ chunks = [];
45
+ controller = Object.create(TransformStreamDefaultController.prototype);
46
+ vi.spyOn(controller, 'enqueue').mockImplementation((chunk) => {
47
+ chunks.push(chunk);
48
+ });
49
+ });
50
+
51
+ it('should parse chunk', () => {
52
+ // Arrange
53
+ const chunk = 'data: {"key": "value", "response": "response1"}';
54
+ const textDecoder = new TextDecoder();
55
+
56
+ // Act
57
+ transformer['parseChunk'](chunk, controller);
58
+
59
+ // Assert
60
+ expect(chunks.length).toBe(2);
61
+ expect(chunks[0]).toBe('event: text\n');
62
+ expect(chunks[1]).toBe('data: "response1"\n\n');
63
+ });
64
+
65
+ it('should not replace `data` in text', () => {
66
+ // Arrange
67
+ const chunk = 'data: {"key": "value", "response": "data: a"}';
68
+ const textDecoder = new TextDecoder();
69
+
70
+ // Act
71
+ transformer['parseChunk'](chunk, controller);
72
+
73
+ // Assert
74
+ expect(chunks.length).toBe(2);
75
+ expect(chunks[0]).toBe('event: text\n');
76
+ expect(chunks[1]).toBe('data: "data: a"\n\n');
77
+ });
78
+ });
79
+
80
+ describe('transform', () => {
81
+ const textDecoder = new TextDecoder();
82
+ const textEncoder = new TextEncoder();
83
+ let chunks: string[];
84
+
85
+ beforeEach(() => {
86
+ chunks = [];
87
+ vi.spyOn(
88
+ transformer as any as {
89
+ parseChunk: (chunk: string, controller: TransformStreamDefaultController) => void;
90
+ },
91
+ 'parseChunk',
92
+ ).mockImplementation((chunk: string, _) => {
93
+ chunks.push(chunk);
94
+ });
95
+ });
96
+
97
+ it('should split single chunk', async () => {
98
+ // Arrange
99
+ const chunk = textEncoder.encode('data: {"key": "value", "response": "response1"}\n\n');
100
+
101
+ // Act
102
+ await transformer.transform(chunk, undefined!);
103
+
104
+ // Assert
105
+ expect(chunks.length).toBe(1);
106
+ expect(chunks[0]).toBe('data: {"key": "value", "response": "response1"}');
107
+ });
108
+
109
+ it('should split multiple chunks', async () => {
110
+ // Arrange
111
+ const chunk = textEncoder.encode(
112
+ 'data: {"key": "value", "response": "response1"}\n\n' +
113
+ 'data: {"key": "value", "response": "response2"}\n\n',
114
+ );
115
+
116
+ // Act
117
+ await transformer.transform(chunk, undefined!);
118
+
119
+ // Assert
120
+ expect(chunks.length).toBe(2);
121
+ expect(chunks[0]).toBe('data: {"key": "value", "response": "response1"}');
122
+ expect(chunks[1]).toBe('data: {"key": "value", "response": "response2"}');
123
+ });
124
+
125
+ it('should ignore empty chunk', async () => {
126
+ // Arrange
127
+ const chunk = textEncoder.encode('\n\n');
128
+
129
+ // Act
130
+ await transformer.transform(chunk, undefined!);
131
+
132
+ // Assert
133
+ expect(chunks.join()).toBe('');
134
+ });
135
+
136
+ it('should split and concat delayed chunks', async () => {
137
+ // Arrange
138
+ const chunk1 = textEncoder.encode('data: {"key": "value", "respo');
139
+ const chunk2 = textEncoder.encode('nse": "response1"}\n\ndata: {"key": "val');
140
+ const chunk3 = textEncoder.encode('ue", "response": "response2"}\n\n');
141
+
142
+ // Act & Assert
143
+ await transformer.transform(chunk1, undefined!);
144
+ expect(transformer['parseChunk']).not.toHaveBeenCalled();
145
+ expect(chunks.length).toBe(0);
146
+ expect(transformer['buffer']).toBe('data: {"key": "value", "respo');
147
+
148
+ await transformer.transform(chunk2, undefined!);
149
+ expect(chunks.length).toBe(1);
150
+ expect(chunks[0]).toBe('data: {"key": "value", "response": "response1"}');
151
+ expect(transformer['buffer']).toBe('data: {"key": "val');
152
+
153
+ await transformer.transform(chunk3, undefined!);
154
+ expect(chunks.length).toBe(2);
155
+ expect(chunks[1]).toBe('data: {"key": "value", "response": "response2"}');
156
+ expect(transformer['buffer']).toBe('');
157
+ });
158
+
159
+ it('should ignore standalone [DONE]', async () => {
160
+ // Arrange
161
+ const chunk = textEncoder.encode('data: [DONE]\n\n');
162
+
163
+ // Act
164
+ await transformer.transform(chunk, undefined!);
165
+
166
+ // Assert
167
+ expect(transformer['parseChunk']).not.toHaveBeenCalled();
168
+ expect(chunks.length).toBe(0);
169
+ expect(transformer['buffer']).toBe('');
170
+ });
171
+
172
+ it('should ignore [DONE] in chunk', async () => {
173
+ // Arrange
174
+ const chunk = textEncoder.encode(
175
+ 'data: {"key": "value", "response": "response1"}\n\ndata: [DONE]\n\n',
176
+ );
177
+
178
+ // Act
179
+ await transformer.transform(chunk, undefined!);
180
+
181
+ // Assert
182
+ expect(chunks.length).toBe(1);
183
+ expect(chunks[0]).toBe('data: {"key": "value", "response": "response1"}');
184
+ expect(transformer['buffer']).toBe('');
185
+ });
186
+ });
187
+ });
188
+
189
+ describe('fillUrl', () => {
190
+ it('should return URL with account id', () => {
191
+ const url = fillUrl('80009000a000b000c000d000e000f000');
192
+ expect(url).toBe(
193
+ 'https://api.cloudflare.com/client/v4/accounts/80009000a000b000c000d000e000f000/ai/run/',
194
+ );
195
+ });
196
+ });
197
+
198
+ describe('maskAccountId', () => {
199
+ describe('desensitizeAccountId', () => {
200
+ it('should replace account id with **** in official API endpoint', () => {
201
+ const url =
202
+ 'https://api.cloudflare.com/client/v4/accounts/80009000a000b000c000d000e000f000/ai/run/';
203
+ const maskedUrl = desensitizeCloudflareUrl(url);
204
+ expect(maskedUrl).toBe('https://api.cloudflare.com/client/v4/accounts/****/ai/run/');
205
+ });
206
+
207
+ it('should replace account id with **** in custom API endpoint', () => {
208
+ const url =
209
+ 'https://api.cloudflare.com/custom/prefix/80009000a000b000c000d000e000f000/custom/suffix/';
210
+ const maskedUrl = desensitizeCloudflareUrl(url);
211
+ expect(maskedUrl).toBe('https://api.cloudflare.com/custom/prefix/****/custom/suffix/');
212
+ });
213
+ });
214
+
215
+ describe('desensitizeCloudflareUrl', () => {
216
+ it('should mask account id in official API endpoint', () => {
217
+ const url =
218
+ 'https://api.cloudflare.com/client/v4/accounts/80009000a000b000c000d000e000f000/ai/run/';
219
+ const maskedUrl = desensitizeCloudflareUrl(url);
220
+ expect(maskedUrl).toBe('https://api.cloudflare.com/client/v4/accounts/****/ai/run/');
221
+ });
222
+
223
+ it('should call desensitizeUrl for custom API endpoint', () => {
224
+ const url = 'https://custom.url/path';
225
+ vi.spyOn(desensitizeTool, 'desensitizeUrl').mockImplementation(
226
+ (_) => 'https://custom.mocked.url',
227
+ );
228
+ const maskedUrl = desensitizeCloudflareUrl(url);
229
+ expect(desensitizeTool.desensitizeUrl).toHaveBeenCalledWith('https://custom.url');
230
+ expect(maskedUrl).toBe('https://custom.mocked.url/path');
231
+ });
232
+
233
+ it('should mask account id in custom API endpoint', () => {
234
+ const url =
235
+ 'https://custom.url/custom/prefix/80009000a000b000c000d000e000f000/custom/suffix/';
236
+ const maskedUrl = desensitizeCloudflareUrl(url);
237
+ expect(maskedUrl).toBe('https://cu****om.url/custom/prefix/****/custom/suffix/');
238
+ });
239
+
240
+ it('should mask account id in custom API endpoint with query params', () => {
241
+ const url =
242
+ 'https://custom.url/custom/prefix/80009000a000b000c000d000e000f000/custom/suffix/?query=param';
243
+ const maskedUrl = desensitizeCloudflareUrl(url);
244
+ expect(maskedUrl).toBe(
245
+ 'https://cu****om.url/custom/prefix/****/custom/suffix/?query=param',
246
+ );
247
+ });
248
+
249
+ it('should mask account id in custom API endpoint with port', () => {
250
+ const url =
251
+ 'https://custom.url:8080/custom/prefix/80009000a000b000c000d000e000f000/custom/suffix/';
252
+ const maskedUrl = desensitizeCloudflareUrl(url);
253
+ expect(maskedUrl).toBe('https://cu****om.url:****/custom/prefix/****/custom/suffix/');
254
+ });
255
+ });
256
+ });
257
+
258
+ describe('modelManifest', () => {
259
+ describe('getModelBeta', () => {
260
+ it('should get beta property', () => {
261
+ const model = { properties: [{ property_id: 'beta', value: 'true' }] };
262
+ const beta = getModelBeta(model);
263
+ expect(beta).toBe(true);
264
+ });
265
+
266
+ it('should return false if beta property is false', () => {
267
+ const model = { properties: [{ property_id: 'beta', value: 'false' }] };
268
+ const beta = getModelBeta(model);
269
+ expect(beta).toBe(false);
270
+ });
271
+
272
+ it('should return false if beta property is not present', () => {
273
+ const model = { properties: [] };
274
+ const beta = getModelBeta(model);
275
+ expect(beta).toBe(false);
276
+ });
277
+ });
278
+
279
+ describe('getModelDisplayName', () => {
280
+ it('should return display name with beta suffix', () => {
281
+ const model = { name: 'model', properties: [{ property_id: 'beta', value: 'true' }] };
282
+ const name = getModelDisplayName(model, true);
283
+ expect(name).toBe('model (Beta)');
284
+ });
285
+
286
+ it('should return display name without beta suffix', () => {
287
+ const model = { name: 'model', properties: [] };
288
+ const name = getModelDisplayName(model, false);
289
+ expect(name).toBe('model');
290
+ });
291
+
292
+ it('should return model["name"]', () => {
293
+ const model = { id: 'modelID', name: 'modelName' };
294
+ const name = getModelDisplayName(model, false);
295
+ expect(name).toBe('modelName');
296
+ });
297
+
298
+ it('should return last part of model["name"]', () => {
299
+ const model = { name: '@provider/modelFamily/modelName' };
300
+ const name = getModelDisplayName(model, false);
301
+ expect(name).toBe('modelName');
302
+ });
303
+ });
304
+
305
+ describe('getModelFunctionCalling', () => {
306
+ it('should return true if function_calling property is true', () => {
307
+ const model = { properties: [{ property_id: 'function_calling', value: 'true' }] };
308
+ const functionCalling = getModelFunctionCalling(model);
309
+ expect(functionCalling).toBe(true);
310
+ });
311
+
312
+ it('should return false if function_calling property is false', () => {
313
+ const model = { properties: [{ property_id: 'function_calling', value: 'false' }] };
314
+ const functionCalling = getModelFunctionCalling(model);
315
+ expect(functionCalling).toBe(false);
316
+ });
317
+
318
+ it('should return false if function_calling property is not set', () => {
319
+ const model = { properties: [] };
320
+ const functionCalling = getModelFunctionCalling(model);
321
+ expect(functionCalling).toBe(false);
322
+ });
323
+ });
324
+
325
+ describe('getModelTokens', () => {
326
+ it('should return tokens property value', () => {
327
+ const model = { properties: [{ property_id: 'max_total_tokens', value: '100' }] };
328
+ const tokens = getModelTokens(model);
329
+ expect(tokens).toBe(100);
330
+ });
331
+
332
+ it('should return undefined if tokens property is not present', () => {
333
+ const model = { properties: [] };
334
+ const tokens = getModelTokens(model);
335
+ expect(tokens).toBeUndefined();
336
+ });
337
+ });
338
+ });
339
+ });
@@ -0,0 +1,134 @@
1
+ import { desensitizeUrl } from '../utils/desensitizeUrl';
2
+
3
+ class CloudflareStreamTransformer {
4
+ private textDecoder = new TextDecoder();
5
+ private buffer: string = '';
6
+
7
+ private parseChunk(chunk: string, controller: TransformStreamDefaultController) {
8
+ const dataPrefix = /^data: /;
9
+ const json = chunk.replace(dataPrefix, '');
10
+ const parsedChunk = JSON.parse(json);
11
+ controller.enqueue(`event: text\n`);
12
+ controller.enqueue(`data: ${JSON.stringify(parsedChunk.response)}\n\n`);
13
+ }
14
+
15
+ public async transform(chunk: Uint8Array, controller: TransformStreamDefaultController) {
16
+ let textChunk = this.textDecoder.decode(chunk);
17
+ if (this.buffer.trim() !== '') {
18
+ textChunk = this.buffer + textChunk;
19
+ this.buffer = '';
20
+ }
21
+ const splits = textChunk.split('\n\n');
22
+ for (let i = 0; i < splits.length - 1; i++) {
23
+ if (/\[DONE]/.test(splits[i].trim())) {
24
+ return;
25
+ }
26
+ this.parseChunk(splits[i], controller);
27
+ }
28
+ const lastChunk = splits.at(-1)!;
29
+ if (lastChunk.trim() !== '') {
30
+ this.buffer += lastChunk; // does not need to be trimmed.
31
+ } // else drop.
32
+ }
33
+ }
34
+
35
+ const CF_PROPERTY_NAME = 'property_id';
36
+ const DEFAULT_BASE_URL_PREFIX = 'https://api.cloudflare.com';
37
+
38
+ function fillUrl(accountID: string): string {
39
+ return `${DEFAULT_BASE_URL_PREFIX}/client/v4/accounts/${accountID}/ai/run/`;
40
+ }
41
+
42
+ function desensitizeAccountId(path: string): string {
43
+ return path.replace(/\/[\dA-Fa-f]{32}\//, '/****/');
44
+ }
45
+
46
+ function desensitizeCloudflareUrl(url: string): string {
47
+ const urlObj = new URL(url);
48
+ let { protocol, hostname, port, pathname, search } = urlObj;
49
+ if (url.startsWith(DEFAULT_BASE_URL_PREFIX)) {
50
+ return `${protocol}//${hostname}${port ? `:${port}` : ''}${desensitizeAccountId(pathname)}${search}`;
51
+ } else {
52
+ const desensitizedUrl = desensitizeUrl(`${protocol}//${hostname}${port ? `:${port}` : ''}`);
53
+ if (desensitizedUrl.endsWith('/') && pathname.startsWith('/')) {
54
+ pathname = pathname.slice(1);
55
+ }
56
+ return `${desensitizedUrl}${desensitizeAccountId(pathname)}${search}`;
57
+ }
58
+ }
59
+
60
+ function getModelBeta(model: any): boolean {
61
+ try {
62
+ const betaProperty = model['properties'].filter(
63
+ (property: any) => property[CF_PROPERTY_NAME] === 'beta',
64
+ );
65
+ if (betaProperty.length === 1) {
66
+ return betaProperty[0]['value'] === 'true'; // This is a string now.
67
+ }
68
+ return false;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ function getModelDisplayName(model: any, beta: boolean): string {
75
+ const modelId = model['name'];
76
+ let name = modelId.split('/').at(-1)!;
77
+ if (beta) {
78
+ name += ' (Beta)';
79
+ }
80
+ return name;
81
+ }
82
+
83
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
84
+ function getModelFunctionCalling(model: any): boolean {
85
+ try {
86
+ const fcProperty = model['properties'].filter(
87
+ (property: any) => property[CF_PROPERTY_NAME] === 'function_calling',
88
+ );
89
+ if (fcProperty.length === 1) {
90
+ return fcProperty[0]['value'] === 'true';
91
+ }
92
+ return false;
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ function getModelTokens(model: any): number | undefined {
99
+ try {
100
+ const tokensProperty = model['properties'].filter(
101
+ (property: any) => property[CF_PROPERTY_NAME] === 'max_total_tokens',
102
+ );
103
+ if (tokensProperty.length === 1) {
104
+ return parseInt(tokensProperty[0]['value']);
105
+ }
106
+ return undefined;
107
+ } catch {
108
+ return undefined;
109
+ }
110
+ }
111
+
112
+ function convertModelManifest(model: any) {
113
+ const modelBeta = getModelBeta(model);
114
+ return {
115
+ description: model['description'],
116
+ displayName: getModelDisplayName(model, modelBeta),
117
+ enabled: !modelBeta,
118
+ functionCall: false, //getModelFunctionCalling(model),
119
+ id: model['name'],
120
+ tokens: getModelTokens(model),
121
+ };
122
+ }
123
+
124
+ export {
125
+ CloudflareStreamTransformer,
126
+ convertModelManifest,
127
+ DEFAULT_BASE_URL_PREFIX,
128
+ desensitizeCloudflareUrl,
129
+ fillUrl,
130
+ getModelBeta,
131
+ getModelDisplayName,
132
+ getModelFunctionCalling,
133
+ getModelTokens,
134
+ };