diff --git a/.env.example b/.env.example
index 9fd3f7bfa1dc..40d6433822ef 100644
--- a/.env.example
+++ b/.env.example
@@ -112,6 +112,11 @@ OPENAI_API_KEY=sk-xxxxxxxxx
# QWEN_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+### Cloudflare Workers AI ####
+
+# CLOUDFLARE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+# CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
### SiliconCloud AI ####
# SILICONCLOUD_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
diff --git a/locales/ar/modelProvider.json b/locales/ar/modelProvider.json
index c642d740fde6..4016b0f13c2e 100644
--- a/locales/ar/modelProvider.json
+++ b/locales/ar/modelProvider.json
@@ -51,6 +51,18 @@
"title": "استخدام معلومات المصادقة الخاصة بـ Bedrock المخصصة"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "يرجى ملء Cloudflare API Key",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "أدخل رقم حساب Cloudflare أو عنوان URL API المخصص",
+ "placeholder": "رقم حساب Cloudflare / عنوان URL API المخصص",
+ "title": "رقم حساب Cloudflare / عنوان URL API"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "أدخل رمز الوصول الشخصي الخاص بك على Github، انقر [هنا](https://github.com/settings/tokens) لإنشاء واحد",
diff --git a/locales/bg-BG/modelProvider.json b/locales/bg-BG/modelProvider.json
index 39dfe28d9e4b..3d8b512b2aa3 100644
--- a/locales/bg-BG/modelProvider.json
+++ b/locales/bg-BG/modelProvider.json
@@ -51,6 +51,18 @@
"title": "Използване на персонализирана информация за удостоверяване на Bedrock"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "Моля, въведете Cloudflare API Key",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "Въведете ID на Cloudflare или личен API адрес",
+ "placeholder": "ID на Cloudflare / личен API адрес",
+ "title": "ID на Cloudflare / API адрес"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "Въведете вашия GitHub PAT, кликнете [тук](https://github.com/settings/tokens), за да създадете",
diff --git a/locales/de-DE/modelProvider.json b/locales/de-DE/modelProvider.json
index 77b0aaf02bf4..b7fb7eea4f73 100644
--- a/locales/de-DE/modelProvider.json
+++ b/locales/de-DE/modelProvider.json
@@ -51,6 +51,18 @@
"title": "Verwenden Sie benutzerdefinierte Bedrock-Authentifizierungsinformationen"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "Bitte füllen Sie die Cloudflare API Key",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "Eingeben Sie die Cloudflare-Kundenkennung oder die benutzerdefinierte API-Adresse",
+ "placeholder": "Cloudflare-Kundenkennung / benutzerdefinierte API-Adresse",
+ "title": "Cloudflare-Kundenkennung / API-Adresse"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "Geben Sie Ihr GitHub-PAT ein und klicken Sie [hier](https://github.com/settings/tokens), um eines zu erstellen.",
diff --git a/locales/en-US/modelProvider.json b/locales/en-US/modelProvider.json
index 642f34f9f050..d8d12c21a277 100644
--- a/locales/en-US/modelProvider.json
+++ b/locales/en-US/modelProvider.json
@@ -51,6 +51,18 @@
"title": "Use Custom Bedrock Authentication Information"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "Please enter Cloudflare API Key",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "Enter your Cloudflare account ID or custom API address",
+ "placeholder": "Cloudflare Account ID / custom API URL",
+ "title": "Cloudflare Account ID / API Address"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "Enter your GitHub PAT. Click [here](https://github.com/settings/tokens) to create one.",
diff --git a/locales/es-ES/modelProvider.json b/locales/es-ES/modelProvider.json
index 163598936e25..6eb711b5cbc9 100644
--- a/locales/es-ES/modelProvider.json
+++ b/locales/es-ES/modelProvider.json
@@ -51,6 +51,18 @@
"title": "Usar información de autenticación de Bedrock personalizada"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "Por favor complete la Cloudflare API Key",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "Ingrese el ID de cuenta de Cloudflare o la dirección URL personalizada de API",
+ "placeholder": "ID de cuenta de Cloudflare / URL de API personalizada",
+ "title": "ID de cuenta de Cloudflare / dirección URL de API"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "Introduce tu PAT de Github, haz clic [aquí](https://github.com/settings/tokens) para crear uno",
diff --git a/locales/fr-FR/modelProvider.json b/locales/fr-FR/modelProvider.json
index 11b2e0d0182e..b52562eefbc9 100644
--- a/locales/fr-FR/modelProvider.json
+++ b/locales/fr-FR/modelProvider.json
@@ -51,6 +51,18 @@
"title": "Utiliser des informations d'authentification Bedrock personnalisées"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "Veuillez remplir l'Cloudflare API Key",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "Saisir l'ID de compte Cloudflare ou l'adresse API personnalisée",
+ "placeholder": "ID de compte Cloudflare / URL API personnalisée",
+ "title": "ID de compte Cloudflare / adresse API"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "Entrez votre PAT GitHub, cliquez [ici](https://github.com/settings/tokens) pour en créer un.",
diff --git a/locales/it-IT/modelProvider.json b/locales/it-IT/modelProvider.json
index 097531c5f245..0cf9b5bd45e1 100644
--- a/locales/it-IT/modelProvider.json
+++ b/locales/it-IT/modelProvider.json
@@ -51,6 +51,18 @@
"title": "Usa le informazioni di autenticazione Bedrock personalizzate"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "Compila l'Cloudflare API Key",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "Inserisci l'ID dell'account Cloudflare o l'indirizzo API personalizzato",
+ "placeholder": "ID account Cloudflare / URL API personalizzato",
+ "title": "ID account Cloudflare / indirizzo API"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "Inserisci il tuo PAT di Github, clicca [qui](https://github.com/settings/tokens) per crearne uno",
diff --git a/locales/ja-JP/modelProvider.json b/locales/ja-JP/modelProvider.json
index 7d61b7e570a9..c2ee771f0b1f 100644
--- a/locales/ja-JP/modelProvider.json
+++ b/locales/ja-JP/modelProvider.json
@@ -51,6 +51,18 @@
"title": "使用カスタム Bedrock 認証情報"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "Cloudflare API Key を入力してください",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "Cloudflare アカウント ID またはカスタム API アドレスを入力してください。",
+ "placeholder": "Cloudflare アカウント ID / カスタム API URL",
+ "title": "Cloudflare アカウント ID / API アドレス"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "あなたのGithub PATを入力してください。[こちら](https://github.com/settings/tokens)をクリックして作成します",
diff --git a/locales/ko-KR/modelProvider.json b/locales/ko-KR/modelProvider.json
index b1f15789e388..d5bf8b55e6dc 100644
--- a/locales/ko-KR/modelProvider.json
+++ b/locales/ko-KR/modelProvider.json
@@ -51,6 +51,18 @@
"title": "사용자 정의 Bedrock 인증 정보 사용"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "Cloudflare API Key 를 작성해 주세요.",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "클라우드 플레어 계정 ID 또는 사용자 지정 API 주소 입력",
+ "placeholder": "클라우드 플레어 계정 ID / 사용자 지정 API 주소",
+ "title": "클라우드 플레어 계정 ID / API 주소"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "당신의 Github PAT를 입력하세요. [여기](https://github.com/settings/tokens)를 클릭하여 생성하세요.",
diff --git a/locales/nl-NL/modelProvider.json b/locales/nl-NL/modelProvider.json
index 52fa264e7588..29e565d5273d 100644
--- a/locales/nl-NL/modelProvider.json
+++ b/locales/nl-NL/modelProvider.json
@@ -51,6 +51,18 @@
"title": "Gebruik aangepaste Bedrock-verificatiegegevens"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "Voer Cloudflare API Key in",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "Voer uw Cloudflare-account ID of een custom API-URL in",
+ "placeholder": "Cloudflare-account ID / custom API-URL",
+ "title": "Cloudflare-account ID / API-URL"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "Vul je Github PAT in, klik [hier](https://github.com/settings/tokens) om er een te maken",
diff --git a/locales/pl-PL/modelProvider.json b/locales/pl-PL/modelProvider.json
index 706293b052fd..487ffa73921c 100644
--- a/locales/pl-PL/modelProvider.json
+++ b/locales/pl-PL/modelProvider.json
@@ -51,6 +51,18 @@
"title": "Użyj niestandardowych informacji uwierzytelniających Bedrock"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "Wprowadź klucz Cloudflare API Key",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "Wprowadź ID konta Cloudflare lub adres API niestandardowy",
+ "placeholder": "ID konta Cloudflare / adres API niestandardowy",
+ "title": "ID konta Cloudflare / adres API"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "Wprowadź swój osobisty token dostępu GitHub (PAT), kliknij [tutaj](https://github.com/settings/tokens), aby go utworzyć",
diff --git a/locales/pt-BR/modelProvider.json b/locales/pt-BR/modelProvider.json
index 117dd10b6859..d8489e7d5cc2 100644
--- a/locales/pt-BR/modelProvider.json
+++ b/locales/pt-BR/modelProvider.json
@@ -51,6 +51,18 @@
"title": "Usar informações de autenticação Bedrock personalizadas"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "Insira o Cloudflare API Key",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "Insira o ID da conta do Cloudflare ou o endereço da API personalizado",
+ "placeholder": "ID da conta do Cloudflare / URL da API personalizada",
+ "title": "ID da conta do Cloudflare / Endereço da API"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "Insira seu PAT do Github, clique [aqui](https://github.com/settings/tokens) para criar",
diff --git a/locales/ru-RU/modelProvider.json b/locales/ru-RU/modelProvider.json
index 47c763ad262b..0671ad80f324 100644
--- a/locales/ru-RU/modelProvider.json
+++ b/locales/ru-RU/modelProvider.json
@@ -51,6 +51,18 @@
"title": "Использовать пользовательскую информацию аутентификации Bedrock"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "Пожалуйста, заполните Cloudflare API Key",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "Введите ID аккаунта Cloudflare или адрес API по умолчанию",
+ "placeholder": "ID аккаунта Cloudflare / адрес API по умолчанию",
+ "title": "ID аккаунта Cloudflare / адрес API"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "Введите ваш персональный токен доступа GitHub (PAT), нажмите [здесь](https://github.com/settings/tokens), чтобы создать его",
diff --git a/locales/tr-TR/modelProvider.json b/locales/tr-TR/modelProvider.json
index 586005014022..2efb5b416f91 100644
--- a/locales/tr-TR/modelProvider.json
+++ b/locales/tr-TR/modelProvider.json
@@ -51,6 +51,18 @@
"title": "Özel Bedrock Kimlik Bilgilerini Kullan"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "Lütfen doldurun Cloudflare API Key",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "Cloudflare hesabınızın ID'sini veya özel API adresinizi girin",
+ "placeholder": "Cloudflare Hesap ID / Özel API Adresi",
+ "title": "Cloudflare Hesap ID / API Adresi"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "Github PAT'nizi girin, [buraya](https://github.com/settings/tokens) tıklayarak oluşturun",
diff --git a/locales/vi-VN/modelProvider.json b/locales/vi-VN/modelProvider.json
index 55cdbe74caa1..f899b3461539 100644
--- a/locales/vi-VN/modelProvider.json
+++ b/locales/vi-VN/modelProvider.json
@@ -51,6 +51,18 @@
"title": "Sử dụng Thông tin Xác thực Bedrock tùy chỉnh"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "Vui lòng nhập Cloudflare API Key",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "Nhập ID tài khoản Cloudflare hoặc địa chỉ API tùy chỉnh",
+ "placeholder": "ID tài khoản Cloudflare / địa chỉ API tùy chỉnh",
+ "title": "ID tài khoản Cloudflare / địa chỉ API"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "Nhập mã truy cập cá nhân Github của bạn, nhấp vào [đây](https://github.com/settings/tokens) để tạo",
diff --git a/locales/zh-CN/modelProvider.json b/locales/zh-CN/modelProvider.json
index 2474379c7c90..834ed382ed9b 100644
--- a/locales/zh-CN/modelProvider.json
+++ b/locales/zh-CN/modelProvider.json
@@ -51,6 +51,18 @@
"title": "使用自定义 Bedrock 鉴权信息"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "请填写 Cloudflare API Key",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "填入 Cloudflare 账户 ID 或 自定义 API 地址",
+ "placeholder": "Cloudflare Account ID / custom API URL",
+ "title": "Cloudflare 账户 ID / API 地址"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "填入你的 Github PAT,点击 [这里](https://github.com/settings/tokens) 创建",
diff --git a/locales/zh-TW/modelProvider.json b/locales/zh-TW/modelProvider.json
index 1e464750d9d6..6afea77a200e 100644
--- a/locales/zh-TW/modelProvider.json
+++ b/locales/zh-TW/modelProvider.json
@@ -51,6 +51,18 @@
"title": "使用自定義 Bedrock 驗證資訊"
}
},
+ "cloudflare": {
+ "apiKey": {
+ "desc": "請填入 Cloudflare API Key",
+ "placeholder": "Cloudflare API Key",
+ "title": "Cloudflare API Key"
+ },
+ "baseURLOrAccountID": {
+ "desc": "填入 Cloudflare 帳戶 ID 或 自定義 API 位址",
+ "placeholder": "Cloudflare 帳戶 ID / 自定義 API 位址",
+ "title": "Cloudflare 帳戶 ID / API 位址"
+ }
+ },
"github": {
"personalAccessToken": {
"desc": "填入你的 Github 個人存取權杖,點擊[這裡](https://github.com/settings/tokens) 創建",
diff --git a/package.json b/package.json
index c2e392a18bfd..cf6a03724538 100644
--- a/package.json
+++ b/package.json
@@ -123,7 +123,7 @@
"@langchain/community": "^0.3.0",
"@lobehub/chat-plugin-sdk": "^1.32.4",
"@lobehub/chat-plugins-gateway": "^1.9.0",
- "@lobehub/icons": "^1.37.0",
+ "@lobehub/icons": "^1.38.1",
"@lobehub/tts": "^1.25.1",
"@lobehub/ui": "^1.152.0",
"@neondatabase/serverless": "^0.10.1",
diff --git a/src/app/(main)/settings/llm/ProviderList/Cloudflare/index.tsx b/src/app/(main)/settings/llm/ProviderList/Cloudflare/index.tsx
new file mode 100644
index 000000000000..f4cf3e72774d
--- /dev/null
+++ b/src/app/(main)/settings/llm/ProviderList/Cloudflare/index.tsx
@@ -0,0 +1,43 @@
+'use client';
+
+import { Input } from 'antd';
+import { useTranslation } from 'react-i18next';
+
+import { CloudflareProviderCard } from '@/config/modelProviders';
+import { GlobalLLMProviderKey } from '@/types/user/settings';
+
+import { KeyVaultsConfigKey } from '../../const';
+import { ProviderItem } from '../../type';
+
+const providerKey: GlobalLLMProviderKey = 'cloudflare';
+
+export const useCloudflareProvider = (): ProviderItem => {
+ const { t } = useTranslation('modelProvider');
+
+ return {
+ ...CloudflareProviderCard,
+ apiKeyItems: [
+ {
+ children: (
+
+ ),
+ desc: t(`${providerKey}.apiKey.desc`),
+ label: t(`${providerKey}.apiKey.title`),
+ name: [KeyVaultsConfigKey, providerKey, 'apiKey'],
+ },
+ {
+ children: (
+
+ ),
+ desc: t(`${providerKey}.baseURLOrAccountID.desc`),
+ label: t(`${providerKey}.baseURLOrAccountID.title`),
+ name: [KeyVaultsConfigKey, providerKey, 'baseURLOrAccountID'],
+ },
+ ],
+ };
+};
diff --git a/src/app/(main)/settings/llm/ProviderList/providers.tsx b/src/app/(main)/settings/llm/ProviderList/providers.tsx
index a2e24524f98e..314abb774f4b 100644
--- a/src/app/(main)/settings/llm/ProviderList/providers.tsx
+++ b/src/app/(main)/settings/llm/ProviderList/providers.tsx
@@ -30,6 +30,7 @@ import {
import { ProviderItem } from '../type';
import { useAzureProvider } from './Azure';
import { useBedrockProvider } from './Bedrock';
+import { useCloudflareProvider } from './Cloudflare';
import { useGithubProvider } from './Github';
import { useHuggingFaceProvider } from './HuggingFace';
import { useOllamaProvider } from './Ollama';
@@ -42,6 +43,7 @@ export const useProviderList = (): ProviderItem[] => {
const OllamaProvider = useOllamaProvider();
const OpenAIProvider = useOpenAIProvider();
const BedrockProvider = useBedrockProvider();
+ const CloudflareProvider = useCloudflareProvider();
const GithubProvider = useGithubProvider();
const HuggingFaceProvider = useHuggingFaceProvider();
const WenxinProvider = useWenxinProvider();
@@ -58,6 +60,7 @@ export const useProviderList = (): ProviderItem[] => {
DeepSeekProviderCard,
HuggingFaceProvider,
OpenRouterProviderCard,
+ CloudflareProvider,
GithubProvider,
NovitaProviderCard,
TogetherAIProviderCard,
@@ -87,6 +90,7 @@ export const useProviderList = (): ProviderItem[] => {
OllamaProvider,
OpenAIProvider,
BedrockProvider,
+ CloudflareProvider,
GithubProvider,
WenxinProvider,
HuggingFaceProvider,
diff --git a/src/config/llm.ts b/src/config/llm.ts
index 8060a708198f..4a8f64ab4cfd 100644
--- a/src/config/llm.ts
+++ b/src/config/llm.ts
@@ -115,6 +115,10 @@ export const getLLMConfig = () => {
TAICHU_API_KEY: z.string().optional(),
TAICHU_MODEL_LIST: z.string().optional(),
+ ENABLED_CLOUDFLARE: z.boolean(),
+ CLOUDFLARE_API_KEY: z.string().optional(),
+ CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID: z.string().optional(),
+
ENABLED_AI360: z.boolean(),
AI360_API_KEY: z.string().optional(),
AI360_MODEL_LIST: z.string().optional(),
@@ -261,6 +265,11 @@ export const getLLMConfig = () => {
TAICHU_API_KEY: process.env.TAICHU_API_KEY,
TAICHU_MODEL_LIST: process.env.TAICHU_MODEL_LIST,
+ ENABLED_CLOUDFLARE:
+ !!process.env.CLOUDFLARE_API_KEY && !!process.env.CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID,
+ CLOUDFLARE_API_KEY: process.env.CLOUDFLARE_API_KEY,
+ CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID: process.env.CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID,
+
ENABLED_AI360: !!process.env.AI360_API_KEY,
AI360_API_KEY: process.env.AI360_API_KEY,
AI360_MODEL_LIST: process.env.AI360_MODEL_LIST,
diff --git a/src/config/modelProviders/cloudflare.ts b/src/config/modelProviders/cloudflare.ts
new file mode 100644
index 000000000000..e1a5f55b91a2
--- /dev/null
+++ b/src/config/modelProviders/cloudflare.ts
@@ -0,0 +1,89 @@
+import { ModelProviderCard } from '@/types/llm';
+
+// ref https://developers.cloudflare.com/workers-ai/models/#text-generation
+// api https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility
+const Cloudflare: ModelProviderCard = {
+ chatModels: [
+ {
+ displayName: 'deepseek-coder-6.7b-instruct-awq',
+ enabled: true,
+ id: '@hf/thebloke/deepseek-coder-6.7b-instruct-awq',
+ tokens: 16_384,
+ },
+ {
+ displayName: 'gemma-7b-it',
+ enabled: true,
+ id: '@hf/google/gemma-7b-it',
+ tokens: 2048,
+ },
+ {
+ displayName: 'hermes-2-pro-mistral-7b',
+ enabled: true,
+ // functionCall: true,
+ id: '@hf/nousresearch/hermes-2-pro-mistral-7b',
+ tokens: 4096,
+ },
+ {
+ displayName: 'llama-3-8b-instruct-awq',
+ id: '@cf/meta/llama-3-8b-instruct-awq',
+ tokens: 8192,
+ },
+ {
+ displayName: 'mistral-7b-instruct-v0.2',
+ id: '@hf/mistral/mistral-7b-instruct-v0.2',
+ tokens: 4096,
+ },
+ {
+ displayName: 'neural-chat-7b-v3-1-awq',
+ enabled: true,
+ id: '@hf/thebloke/neural-chat-7b-v3-1-awq',
+ tokens: 32_768,
+ },
+ {
+ displayName: 'openchat-3.5-0106',
+ id: '@cf/openchat/openchat-3.5-0106',
+ tokens: 8192,
+ },
+ {
+ displayName: 'openhermes-2.5-mistral-7b-awq',
+ enabled: true,
+ id: '@hf/thebloke/openhermes-2.5-mistral-7b-awq',
+ tokens: 32_768,
+ },
+ {
+ displayName: 'qwen1.5-14b-chat-awq',
+ enabled: true,
+ id: '@cf/qwen/qwen1.5-14b-chat-awq',
+ tokens: 32_768,
+ },
+ {
+ displayName: 'starling-lm-7b-beta',
+ enabled: true,
+ id: '@hf/nexusflow/starling-lm-7b-beta',
+ tokens: 4096,
+ },
+ {
+ displayName: 'zephyr-7b-beta-awq',
+ enabled: true,
+ id: '@hf/thebloke/zephyr-7b-beta-awq',
+ tokens: 32_768,
+ },
+ {
+ description:
+ 'Generation over generation, Meta Llama 3 demonstrates state-of-the-art performance on a wide range of industry benchmarks and offers new capabilities, including improved reasoning.\t',
+ displayName: 'meta-llama-3-8b-instruct',
+ enabled: true,
+ functionCall: false,
+ id: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ },
+ ],
+ checkModel: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ id: 'cloudflare',
+ modelList: {
+ showModelFetcher: true,
+ },
+ name: 'Cloudflare Workers AI',
+ url: 'https://developers.cloudflare.com/workers-ai/models',
+};
+
+export default Cloudflare;
diff --git a/src/config/modelProviders/index.ts b/src/config/modelProviders/index.ts
index 2237ef877b7c..3b0c8eb1e589 100644
--- a/src/config/modelProviders/index.ts
+++ b/src/config/modelProviders/index.ts
@@ -6,6 +6,7 @@ import AnthropicProvider from './anthropic';
import AzureProvider from './azure';
import BaichuanProvider from './baichuan';
import BedrockProvider from './bedrock';
+import CloudflareProvider from './cloudflare';
import DeepSeekProvider from './deepseek';
import FireworksAIProvider from './fireworksai';
import GithubProvider from './github';
@@ -57,6 +58,7 @@ export const LOBE_DEFAULT_MODEL_LIST: ChatModelCard[] = [
NovitaProvider.chatModels,
BaichuanProvider.chatModels,
TaichuProvider.chatModels,
+ CloudflareProvider.chatModels,
Ai360Provider.chatModels,
SiliconCloudProvider.chatModels,
UpstageProvider.chatModels,
@@ -99,6 +101,7 @@ export const DEFAULT_MODEL_PROVIDER_LIST = [
MinimaxProvider,
Ai360Provider,
TaichuProvider,
+ CloudflareProvider,
SiliconCloudProvider,
];
@@ -117,6 +120,7 @@ export { default as AnthropicProviderCard } from './anthropic';
export { default as AzureProviderCard } from './azure';
export { default as BaichuanProviderCard } from './baichuan';
export { default as BedrockProviderCard } from './bedrock';
+export { default as CloudflareProviderCard } from './cloudflare';
export { default as DeepSeekProviderCard } from './deepseek';
export { default as FireworksAIProviderCard } from './fireworksai';
export { default as GithubProviderCard } from './github';
diff --git a/src/const/auth.ts b/src/const/auth.ts
index 33c0180fc766..fe3626aef288 100644
--- a/src/const/auth.ts
+++ b/src/const/auth.ts
@@ -37,6 +37,8 @@ export interface JWTPayload {
awsSecretAccessKey?: string;
awsSessionToken?: string;
+ cloudflareBaseURLOrAccountID?: string;
+
wenxinAccessKey?: string;
wenxinSecretKey?: string;
diff --git a/src/const/settings/llm.ts b/src/const/settings/llm.ts
index 9c478db2e95b..e69e99d79c36 100644
--- a/src/const/settings/llm.ts
+++ b/src/const/settings/llm.ts
@@ -4,6 +4,7 @@ import {
AnthropicProviderCard,
BaichuanProviderCard,
BedrockProviderCard,
+ CloudflareProviderCard,
DeepSeekProviderCard,
FireworksAIProviderCard,
GithubProviderCard,
@@ -59,6 +60,10 @@ export const DEFAULT_LLM_CONFIG: UserModelProviderConfig = {
enabled: false,
enabledModels: filterEnabledModels(BedrockProviderCard),
},
+ cloudflare: {
+ enabled: false,
+ enabledModels: filterEnabledModels(CloudflareProviderCard),
+ },
deepseek: {
enabled: false,
enabledModels: filterEnabledModels(DeepSeekProviderCard),
diff --git a/src/libs/agent-runtime/AgentRuntime.ts b/src/libs/agent-runtime/AgentRuntime.ts
index ed6bc9ae16d3..5930c5f8dc92 100644
--- a/src/libs/agent-runtime/AgentRuntime.ts
+++ b/src/libs/agent-runtime/AgentRuntime.ts
@@ -9,6 +9,7 @@ import { LobeAnthropicAI } from './anthropic';
import { LobeAzureOpenAI } from './azureOpenai';
import { LobeBaichuanAI } from './baichuan';
import { LobeBedrockAI, LobeBedrockAIParams } from './bedrock';
+import { LobeCloudflareAI, LobeCloudflareParams } from './cloudflare';
import { LobeDeepSeekAI } from './deepseek';
import { LobeFireworksAI } from './fireworksai';
import { LobeGithubAI } from './github';
@@ -131,6 +132,7 @@ class AgentRuntime {
azure: { apiVersion?: string; apikey?: string; endpoint?: string };
baichuan: Partial;
bedrock: Partial;
+ cloudflare: Partial;
deepseek: Partial;
fireworksai: Partial;
github: Partial;
@@ -321,8 +323,12 @@ class AgentRuntime {
runtimeModel = await LobeSenseNovaAI.fromAPIKey(params.sensenova);
break;
}
- }
+ case ModelProvider.Cloudflare: {
+ runtimeModel = new LobeCloudflareAI(params.cloudflare ?? {});
+ break;
+ }
+ }
return new AgentRuntime(runtimeModel);
}
}
diff --git a/src/libs/agent-runtime/cloudflare/index.test.ts b/src/libs/agent-runtime/cloudflare/index.test.ts
new file mode 100644
index 000000000000..1e8535f07e19
--- /dev/null
+++ b/src/libs/agent-runtime/cloudflare/index.test.ts
@@ -0,0 +1,648 @@
+// @vitest-environment node
+import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { ChatCompletionTool } from '@/libs/agent-runtime';
+
+import * as debugStreamModule from '../utils/debugStream';
+import { LobeCloudflareAI } from './index';
+
+const provider = 'cloudflare';
+
+const bizErrorType = 'ProviderBizError';
+const invalidErrorType = 'InvalidProviderAPIKey';
+
+// Mock the console.error to avoid polluting test output
+vi.spyOn(console, 'error').mockImplementation(() => {});
+
+let instance: LobeCloudflareAI;
+const textEncoder = new TextEncoder();
+
+afterEach(() => {
+ vi.restoreAllMocks();
+});
+
+describe('LobeCloudflareAI', () => {
+ const accountID = '80009000a000b000c000d000e000f000';
+ describe('init', () => {
+ it('should correctly initialize with API key and Account ID', async () => {
+ const instance = new LobeCloudflareAI({
+ apiKey: 'test_api_key',
+ baseURLOrAccountID: accountID,
+ });
+ expect(instance).toBeInstanceOf(LobeCloudflareAI);
+ expect(instance.baseURL).toBe(
+ `https://api.cloudflare.com/client/v4/accounts/${accountID}/ai/run/`,
+ );
+ expect(instance.accountID).toBe(accountID);
+ });
+
+ it('should correctly initialize with API key and Gateway URL', async () => {
+ const baseURL = `https://gateway.ai.cloudflare.com/v1/${accountID}/test-gateway/workers-ai`;
+ const instance = new LobeCloudflareAI({
+ apiKey: 'test_api_key',
+ baseURLOrAccountID: baseURL,
+ });
+ expect(instance).toBeInstanceOf(LobeCloudflareAI);
+ expect(instance.baseURL).toBe(baseURL + '/'); // baseURL MUST end with '/'.
+ expect(instance.accountID).toBe(accountID);
+ });
+ });
+
+ describe('chat', () => {
+ beforeEach(() => {
+ instance = new LobeCloudflareAI({
+ apiKey: 'test_api_key',
+ baseURLOrAccountID: accountID,
+ });
+
+ // Mock fetch
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(
+ new Response(
+ new ReadableStream({
+ start(controller) {
+ controller.enqueue(textEncoder.encode('data: {"response": "Hello, world!"}\n\n'));
+ controller.close();
+ },
+ }),
+ ),
+ );
+ });
+
+ it('should return a Response on successful API call', async () => {
+ const result = await instance.chat({
+ messages: [{ content: 'Hello', role: 'user' }],
+ model: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ temperature: 0,
+ });
+
+ // Assert
+ expect(result).toBeInstanceOf(Response);
+ });
+
+ it('should handle text messages correctly', async () => {
+ // Arrange
+ const textEncoder = new TextEncoder();
+ const mockResponse = new Response(
+ new ReadableStream({
+ start(controller) {
+ controller.enqueue(textEncoder.encode('data: {"response": "Hello, world!"}\n\n'));
+ controller.close();
+ },
+ }),
+ );
+ (globalThis.fetch as Mock).mockResolvedValue(mockResponse);
+
+ // Act
+ const result = await instance.chat({
+ messages: [{ content: 'Hello', role: 'user' }],
+ model: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ temperature: 0,
+ top_p: 1,
+ });
+
+ // Assert
+ expect(globalThis.fetch).toHaveBeenCalledWith(
+ // url
+ expect.objectContaining({
+ pathname: `/client/v4/accounts/${accountID}/ai/run/@hf/meta-llama/meta-llama-3-8b-instruct`,
+ }),
+ // body
+ expect.objectContaining({
+ body: expect.any(String),
+ method: 'POST',
+ }),
+ );
+
+ const fetchCallArgs = (globalThis.fetch as Mock).mock.calls[0];
+ const body = JSON.parse(fetchCallArgs[1].body);
+ expect(body).toEqual(
+ expect.objectContaining({
+ //max_tokens: 4096,
+ messages: [{ content: 'Hello', role: 'user' }],
+ //stream: true,
+ temperature: 0,
+ top_p: 1,
+ }),
+ );
+
+ expect(result).toBeInstanceOf(Response);
+ });
+
+ it('should handle system prompt correctly', async () => {
+ // Arrange
+ const textEncoder = new TextEncoder();
+ const mockResponse = new Response(
+ new ReadableStream({
+ start(controller) {
+ controller.enqueue(textEncoder.encode('data: {"response": "Hello, world!"}\n\n'));
+ controller.close();
+ },
+ }),
+ );
+ (globalThis.fetch as Mock).mockResolvedValue(mockResponse);
+
+ // Act
+ const result = await instance.chat({
+ messages: [
+ { content: 'You are an awesome greeter', role: 'system' },
+ { content: 'Hello', role: 'user' },
+ ],
+ model: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ temperature: 0,
+ });
+
+ // Assert
+ expect(globalThis.fetch).toHaveBeenCalledWith(
+ // url
+ expect.objectContaining({
+ pathname: `/client/v4/accounts/${accountID}/ai/run/@hf/meta-llama/meta-llama-3-8b-instruct`,
+ }),
+ // body
+ expect.objectContaining({
+ body: expect.any(String),
+ method: 'POST',
+ }),
+ );
+
+ const fetchCallArgs = (globalThis.fetch as Mock).mock.calls[0];
+ const body = JSON.parse(fetchCallArgs[1].body);
+ expect(body).toEqual(
+ expect.objectContaining({
+ //max_tokens: 4096,
+ messages: [
+ { content: 'You are an awesome greeter', role: 'system' },
+ { content: 'Hello', role: 'user' },
+ ],
+ //stream: true,
+ temperature: 0,
+ }),
+ );
+
+ expect(result).toBeInstanceOf(Response);
+ });
+
+ it('should call Cloudflare API with supported opions', async () => {
+ // Arrange
+ const mockResponse = new Response(
+ new ReadableStream({
+ start(controller) {
+ controller.enqueue(textEncoder.encode('data: {"response": "Hello, world!"}\n\n'));
+ controller.close();
+ },
+ }),
+ );
+ (globalThis.fetch as Mock).mockResolvedValue(mockResponse);
+
+ // Act
+ const result = await instance.chat({
+ max_tokens: 2048,
+ messages: [{ content: 'Hello', role: 'user' }],
+ model: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ temperature: 0.5,
+ top_p: 1,
+ });
+
+ // Assert
+ expect(globalThis.fetch).toHaveBeenCalledWith(
+ // url
+ expect.objectContaining({
+ pathname: `/client/v4/accounts/${accountID}/ai/run/@hf/meta-llama/meta-llama-3-8b-instruct`,
+ }),
+ // body
+ expect.objectContaining({
+ body: expect.any(String),
+ method: 'POST',
+ }),
+ );
+
+ const fetchCallArgs = (globalThis.fetch as Mock).mock.calls[0];
+ const body = JSON.parse(fetchCallArgs[1].body);
+ expect(body).toEqual(
+ expect.objectContaining({
+ max_tokens: 2048,
+ messages: [{ content: 'Hello', role: 'user' }],
+ //stream: true,
+ temperature: 0.5,
+ top_p: 1,
+ }),
+ );
+
+ expect(result).toBeInstanceOf(Response);
+ });
+
+ it('should call debugStream in DEBUG mode', async () => {
+ // Arrange
+ const mockProdStream = new ReadableStream({
+ start(controller) {
+ controller.enqueue('Hello, world!');
+ controller.close();
+ },
+ }) as any;
+ const mockDebugStream = new ReadableStream({
+ start(controller) {
+ controller.enqueue('Debug stream content');
+ controller.close();
+ },
+ }) as any;
+ mockDebugStream.toReadableStream = () => mockDebugStream;
+
+ (globalThis.fetch as Mock).mockResolvedValue({
+ body: {
+ tee: () => [mockProdStream, { toReadableStream: () => mockDebugStream }],
+ },
+ });
+
+ const originalDebugValue = process.env.DEBUG_CLOUDFLARE_CHAT_COMPLETION;
+
+ process.env.DEBUG_CLOUDFLARE_CHAT_COMPLETION = '1';
+ vi.spyOn(debugStreamModule, 'debugStream').mockImplementation(() => Promise.resolve());
+
+ // Act
+ await instance.chat({
+ messages: [{ content: 'Hello', role: 'user' }],
+ model: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ temperature: 0,
+ });
+
+ // Assert
+ expect(debugStreamModule.debugStream).toHaveBeenCalled();
+
+ // Cleanup
+ process.env.DEBUG_CLOUDFLARE_CHAT_COMPLETION = originalDebugValue;
+ });
+
+ describe('chat with tools', () => {
+ it('should call client.beta.tools.messages.create when tools are provided', async () => {
+ // Arrange
+ const tools: ChatCompletionTool[] = [
+ { function: { name: 'tool1', description: 'desc1' }, type: 'function' },
+ ];
+
+ // Act
+ await instance.chat({
+ messages: [{ content: 'Hello', role: 'user' }],
+ model: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ temperature: 1,
+ tools,
+ });
+
+ // Assert
+ expect(globalThis.fetch).toHaveBeenCalled();
+
+ const fetchCallArgs = (globalThis.fetch as Mock).mock.calls[0];
+ const body = JSON.parse(fetchCallArgs[1].body);
+ expect(body).toEqual(
+ expect.objectContaining({
+ tools: tools.map((t) => t.function),
+ }),
+ );
+ });
+ });
+
+ describe('Error', () => {
+ it('should throw ProviderBizError error on 400 error', async () => {
+ // Arrange
+ const apiError = {
+ status: 400,
+ error: {
+ type: 'error',
+ error: {
+ type: 'authentication_error',
+ message: 'invalid x-api-key',
+ },
+ },
+ };
+ (globalThis.fetch as Mock).mockRejectedValue(apiError);
+
+ try {
+ // Act
+ await instance.chat({
+ messages: [{ content: 'Hello', role: 'user' }],
+ model: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ temperature: 0,
+ });
+ } catch (e) {
+ // Assert
+ expect(e).toEqual({
+ endpoint: expect.stringMatching(/https:\/\/.+/),
+ error: apiError,
+ errorType: bizErrorType,
+ provider,
+ });
+ }
+ });
+
+ it('should throw InvalidProviderAPIKey if no accountID is provided', async () => {
+ try {
+ new LobeCloudflareAI({
+ apiKey: 'test',
+ });
+ } catch (e) {
+ expect(e).toEqual({ errorType: invalidErrorType });
+ }
+ });
+
+ it('should throw InvalidProviderAPIKey if no apiKey is provided', async () => {
+ try {
+ new LobeCloudflareAI({
+ baseURLOrAccountID: accountID,
+ });
+ } catch (e) {
+ expect(e).toEqual({ errorType: invalidErrorType });
+ }
+ });
+
+ it('should not throw Error when apiKey is not provided but baseURL is provided', async () => {
+ const customInstance = new LobeCloudflareAI({
+ baseURLOrAccountID: 'https://custom.cloudflare.url/',
+ });
+ expect(customInstance).toBeInstanceOf(LobeCloudflareAI);
+ expect(customInstance.apiKey).toBeUndefined();
+ expect(customInstance.baseURL).toBe('https://custom.cloudflare.url/');
+ });
+ });
+
+ describe('Error handling', () => {
+ it('should throw ProviderBizError on other error status codes', async () => {
+ // Arrange
+ const apiError = { status: 400 };
+ (globalThis.fetch as Mock).mockRejectedValue(apiError);
+
+ // Act & Assert
+ await expect(
+ instance.chat({
+ messages: [{ content: 'Hello', role: 'user' }],
+ model: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ temperature: 1,
+ }),
+ ).rejects.toEqual({
+ endpoint: expect.stringMatching(/https:\/\/.+/),
+ error: apiError,
+ errorType: bizErrorType,
+ provider,
+ });
+ });
+
+ it('should desensitize accountID in error message', async () => {
+ // Arrange
+ const apiError = { status: 400 };
+ const customInstance = new LobeCloudflareAI({
+ apiKey: 'test',
+ baseURLOrAccountID: accountID,
+ });
+ (globalThis.fetch as Mock).mockRejectedValue(apiError);
+
+ // Act & Assert
+ await expect(
+ customInstance.chat({
+ messages: [{ content: 'Hello', role: 'user' }],
+ model: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ temperature: 0,
+ }),
+ ).rejects.toEqual({
+ endpoint: expect.not.stringContaining(accountID),
+ error: apiError,
+ errorType: bizErrorType,
+ provider,
+ });
+ });
+ });
+
+ describe('Options', () => {
+ it('should pass signal to API call', async () => {
+ // Arrange
+ const controller = new AbortController();
+
+ // Act
+ await instance.chat(
+ {
+ messages: [{ content: 'Hello', role: 'user' }],
+ model: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ temperature: 1,
+ },
+ { signal: controller.signal },
+ );
+
+ // Assert
+ expect(globalThis.fetch).toHaveBeenCalledWith(
+ expect.any(URL),
+ expect.objectContaining({ signal: controller.signal }),
+ );
+ });
+
+ it('should apply callback to the returned stream', async () => {
+ // Arrange
+ const callback = vi.fn();
+
+ // Act
+ await instance.chat(
+ {
+ messages: [{ content: 'Hello', role: 'user' }],
+ model: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ temperature: 0,
+ },
+ {
+ callback: { onStart: callback },
+ },
+ );
+
+ // Assert
+ expect(callback).toHaveBeenCalled();
+ });
+
+ it('should set headers on the response', async () => {
+ // Arrange
+ const headers = { 'X-Test-Header': 'test' };
+
+ // Act
+ const result = await instance.chat(
+ {
+ messages: [{ content: 'Hello', role: 'user' }],
+ model: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ temperature: 1,
+ },
+ { headers },
+ );
+
+ // Assert
+ expect(result.headers.get('X-Test-Header')).toBe('test');
+ });
+ });
+
+ describe('Edge cases', () => {
+ it('should handle empty messages array', async () => {
+ // Act & Assert
+ await expect(
+ instance.chat({
+ messages: [],
+ model: '@hf/meta-llama/meta-llama-3-8b-instruct',
+ temperature: 1,
+ }),
+ ).resolves.toBeInstanceOf(Response);
+ });
+ });
+ });
+
+ describe('models', () => {
+ it('should send request', async () => {
+ // Arrange
+ const apiKey = 'test_api_key';
+ const instance = new LobeCloudflareAI({ apiKey, baseURLOrAccountID: accountID });
+
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(
+ new Response(
+ JSON.stringify({
+ result: [
+ {
+ description: 'Model 1',
+ name: 'model1',
+ task: { name: 'Text Generation' },
+ properties: [{ property_id: 'beta', value: 'false' }],
+ },
+ {
+ description: 'Model 2',
+ name: 'model2',
+ task: { name: 'Text Generation' },
+ properties: [{ property_id: 'beta', value: 'true' }],
+ },
+ ],
+ }),
+ ),
+ );
+
+ // Act
+ const result = await instance.models();
+
+ // Assert
+ expect(globalThis.fetch).toHaveBeenCalledWith(
+ `https://api.cloudflare.com/client/v4/accounts/${accountID}/ai/models/search`,
+ {
+ headers: {
+ 'Authorization': `Bearer ${apiKey}`,
+ 'Content-Type': 'application/json',
+ },
+ method: 'GET',
+ },
+ );
+
+ expect(result).toHaveLength(2);
+ });
+
+ it('should set id to name', async () => {
+ // Arrange
+ const instance = new LobeCloudflareAI({
+ apiKey: 'test_api_key',
+ baseURLOrAccountID: accountID,
+ });
+
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(
+ new Response(
+ JSON.stringify({
+ result: [
+ {
+ id: 'id1',
+ name: 'name1',
+ task: { name: 'Text Generation' },
+ },
+ ],
+ }),
+ ),
+ );
+
+ // Act
+ const result = await instance.models();
+
+ // Assert
+ expect(result).toEqual([
+ expect.objectContaining({
+ displayName: 'name1',
+ id: 'name1',
+ }),
+ ]);
+ });
+
+ it('should filter text generation models', async () => {
+ // Arrange
+ const instance = new LobeCloudflareAI({
+ apiKey: 'test_api_key',
+ baseURLOrAccountID: accountID,
+ });
+
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(
+ new Response(
+ JSON.stringify({
+ result: [
+ {
+ id: '1',
+ name: 'model1',
+ task: { name: 'Text Generation' },
+ },
+ {
+ id: '2',
+ name: 'model2',
+ task: { name: 'Text Classification' },
+ },
+ ],
+ }),
+ ),
+ );
+
+ // Act
+ const result = await instance.models();
+
+ // Assert
+ expect(result).toEqual([
+ expect.objectContaining({
+ displayName: 'model1',
+ id: 'model1',
+ }),
+ ]);
+ });
+
+ it('should enable non-beta models and mark beta models', async () => {
+ // Arrange
+ const instance = new LobeCloudflareAI({
+ apiKey: 'test_api_key',
+ baseURLOrAccountID: accountID,
+ });
+
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(
+ new Response(
+ JSON.stringify({
+ result: [
+ {
+ id: '1',
+ name: 'model1',
+ task: { name: 'Text Generation' },
+ properties: [{ property_id: 'beta', value: 'false' }],
+ },
+ {
+ id: '2',
+ name: 'model2',
+ task: { name: 'Text Generation' },
+ properties: [{ property_id: 'beta', value: 'true' }],
+ },
+ ],
+ }),
+ ),
+ );
+
+ // Act
+ const result = await instance.models();
+
+ // Assert
+ expect(result).toEqual([
+ expect.objectContaining({
+ displayName: 'model1',
+ enabled: true,
+ id: 'model1',
+ }),
+ expect.objectContaining({
+ displayName: 'model2 (Beta)',
+ enabled: false,
+ id: 'model2',
+ }),
+ ]);
+ });
+ });
+});
diff --git a/src/libs/agent-runtime/cloudflare/index.ts b/src/libs/agent-runtime/cloudflare/index.ts
new file mode 100644
index 000000000000..885e3fd7543b
--- /dev/null
+++ b/src/libs/agent-runtime/cloudflare/index.ts
@@ -0,0 +1,123 @@
+import { ChatModelCard } from '@/types/llm';
+
+import { LobeRuntimeAI } from '../BaseAI';
+import { AgentRuntimeErrorType } from '../error';
+import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from '../types';
+import {
+ CloudflareStreamTransformer,
+ DEFAULT_BASE_URL_PREFIX,
+ convertModelManifest,
+ desensitizeCloudflareUrl,
+ fillUrl,
+} from '../utils/cloudflareHelpers';
+import { AgentRuntimeError } from '../utils/createError';
+import { debugStream } from '../utils/debugStream';
+import { StreamingResponse } from '../utils/response';
+import { createCallbacksTransformer } from '../utils/streams';
+
+export interface LobeCloudflareParams {
+ apiKey?: string;
+ baseURLOrAccountID?: string;
+}
+
+export class LobeCloudflareAI implements LobeRuntimeAI {
+ baseURL: string;
+ accountID: string;
+ apiKey?: string;
+
+ constructor({ apiKey, baseURLOrAccountID }: LobeCloudflareParams) {
+ if (!baseURLOrAccountID) {
+ throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey);
+ }
+ if (baseURLOrAccountID.startsWith('http')) {
+ this.baseURL = baseURLOrAccountID.endsWith('/')
+ ? baseURLOrAccountID
+ : baseURLOrAccountID + '/';
+ // Try get accountID from baseURL
+ this.accountID = baseURLOrAccountID.replaceAll(/^.*\/([\dA-Fa-f]{32})\/.*$/g, '$1');
+ } else {
+ if (!apiKey) {
+ throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey);
+ }
+ this.accountID = baseURLOrAccountID;
+ this.baseURL = fillUrl(baseURLOrAccountID);
+ }
+ this.apiKey = apiKey;
+ }
+
+ async chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions): Promise {
+ try {
+ const { model, tools, ...restPayload } = payload;
+ const functions = tools?.map((tool) => tool.function);
+ const headers = options?.headers || {};
+ if (this.apiKey) {
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
+ }
+ const url = new URL(model, this.baseURL);
+ const response = await fetch(url, {
+ body: JSON.stringify({ tools: functions, ...restPayload }),
+ headers: { 'Content-Type': 'application/json', ...headers },
+ method: 'POST',
+ signal: options?.signal,
+ });
+
+ const desensitizedEndpoint = desensitizeCloudflareUrl(url.toString());
+
+ switch (response.status) {
+ case 400: {
+ throw AgentRuntimeError.chat({
+ endpoint: desensitizedEndpoint,
+ error: response,
+ errorType: AgentRuntimeErrorType.ProviderBizError,
+ provider: ModelProvider.Cloudflare,
+ });
+ }
+ }
+
+ // Only tee when debugging
+ let responseBody: ReadableStream;
+ if (process.env.DEBUG_CLOUDFLARE_CHAT_COMPLETION === '1') {
+ const [prod, useForDebug] = response.body!.tee();
+ debugStream(useForDebug).catch();
+ responseBody = prod;
+ } else {
+ responseBody = response.body!;
+ }
+
+ return StreamingResponse(
+ responseBody
+ .pipeThrough(new TransformStream(new CloudflareStreamTransformer()))
+ .pipeThrough(createCallbacksTransformer(options?.callback)),
+ { headers: options?.headers },
+ );
+ } catch (error) {
+ const desensitizedEndpoint = desensitizeCloudflareUrl(this.baseURL);
+
+ throw AgentRuntimeError.chat({
+ endpoint: desensitizedEndpoint,
+ error: error as any,
+ errorType: AgentRuntimeErrorType.ProviderBizError,
+ provider: ModelProvider.Cloudflare,
+ });
+ }
+ }
+
+ async models(): Promise {
+ const url = `${DEFAULT_BASE_URL_PREFIX}/client/v4/accounts/${this.accountID}/ai/models/search`;
+ const response = await fetch(url, {
+ headers: {
+ 'Authorization': `Bearer ${this.apiKey}`,
+ 'Content-Type': 'application/json',
+ },
+ method: 'GET',
+ });
+ const j = await response.json();
+ const models: any[] = j['result'].filter(
+ (model: any) => model['task']['name'] === 'Text Generation',
+ );
+ const chatModels: ChatModelCard[] = models
+ .map((model) => convertModelManifest(model))
+ .sort((a, b) => a.displayName.localeCompare(b.displayName));
+ return chatModels;
+ }
+}
diff --git a/src/libs/agent-runtime/types/type.ts b/src/libs/agent-runtime/types/type.ts
index db64c94f23fb..35e55189b141 100644
--- a/src/libs/agent-runtime/types/type.ts
+++ b/src/libs/agent-runtime/types/type.ts
@@ -28,6 +28,7 @@ export enum ModelProvider {
Azure = 'azure',
Baichuan = 'baichuan',
Bedrock = 'bedrock',
+ Cloudflare = 'cloudflare',
DeepSeek = 'deepseek',
FireworksAI = 'fireworksai',
Github = 'github',
diff --git a/src/libs/agent-runtime/utils/cloudflareHelpers.test.ts b/src/libs/agent-runtime/utils/cloudflareHelpers.test.ts
new file mode 100644
index 000000000000..3a69bd7c6994
--- /dev/null
+++ b/src/libs/agent-runtime/utils/cloudflareHelpers.test.ts
@@ -0,0 +1,339 @@
+// @vitest-environment node
+import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import * as desensitizeTool from '../utils/desensitizeUrl';
+import {
+ CloudflareStreamTransformer,
+ desensitizeCloudflareUrl,
+ fillUrl,
+ getModelBeta,
+ getModelDisplayName,
+ getModelFunctionCalling,
+ getModelTokens,
+} from './cloudflareHelpers';
+
+//const {
+// getModelBeta,
+// getModelDisplayName,
+// getModelFunctionCalling,
+// getModelTokens,
+//} = require('./cloudflareHelpers');
+
+//const cloudflareHelpers = require('./cloudflareHelpers');
+//const getModelBeta = cloudflareHelpers.__get__('getModelBeta');
+//const getModelDisplayName = cloudflareHelpers.__get__('getModelDisplayName');
+//const getModelFunctionCalling = cloudflareHelpers.__get__('getModelFunctionCalling');
+//const getModelTokens = cloudflareHelpers.__get__('getModelTokens');
+
+afterEach(() => {
+ vi.restoreAllMocks();
+});
+
+describe('cloudflareHelpers', () => {
+ describe('CloudflareStreamTransformer', () => {
+ let transformer: CloudflareStreamTransformer;
+ beforeEach(() => {
+ transformer = new CloudflareStreamTransformer();
+ });
+
+ describe('parseChunk', () => {
+ let chunks: string[];
+ let controller: TransformStreamDefaultController;
+
+ beforeEach(() => {
+ chunks = [];
+ controller = Object.create(TransformStreamDefaultController.prototype);
+ vi.spyOn(controller, 'enqueue').mockImplementation((chunk) => {
+ chunks.push(chunk);
+ });
+ });
+
+ it('should parse chunk', () => {
+ // Arrange
+ const chunk = 'data: {"key": "value", "response": "response1"}';
+ const textDecoder = new TextDecoder();
+
+ // Act
+ transformer['parseChunk'](chunk, controller);
+
+ // Assert
+ expect(chunks.length).toBe(2);
+ expect(chunks[0]).toBe('event: text\n');
+ expect(chunks[1]).toBe('data: "response1"\n\n');
+ });
+
+ it('should not replace `data` in text', () => {
+ // Arrange
+ const chunk = 'data: {"key": "value", "response": "data: a"}';
+ const textDecoder = new TextDecoder();
+
+ // Act
+ transformer['parseChunk'](chunk, controller);
+
+ // Assert
+ expect(chunks.length).toBe(2);
+ expect(chunks[0]).toBe('event: text\n');
+ expect(chunks[1]).toBe('data: "data: a"\n\n');
+ });
+ });
+
+ describe('transform', () => {
+ const textDecoder = new TextDecoder();
+ const textEncoder = new TextEncoder();
+ let chunks: string[];
+
+ beforeEach(() => {
+ chunks = [];
+ vi.spyOn(
+ transformer as any as {
+ parseChunk: (chunk: string, controller: TransformStreamDefaultController) => void;
+ },
+ 'parseChunk',
+ ).mockImplementation((chunk: string, _) => {
+ chunks.push(chunk);
+ });
+ });
+
+ it('should split single chunk', async () => {
+ // Arrange
+ const chunk = textEncoder.encode('data: {"key": "value", "response": "response1"}\n\n');
+
+ // Act
+ await transformer.transform(chunk, undefined!);
+
+ // Assert
+ expect(chunks.length).toBe(1);
+ expect(chunks[0]).toBe('data: {"key": "value", "response": "response1"}');
+ });
+
+ it('should split multiple chunks', async () => {
+ // Arrange
+ const chunk = textEncoder.encode(
+ 'data: {"key": "value", "response": "response1"}\n\n' +
+ 'data: {"key": "value", "response": "response2"}\n\n',
+ );
+
+ // Act
+ await transformer.transform(chunk, undefined!);
+
+ // Assert
+ expect(chunks.length).toBe(2);
+ expect(chunks[0]).toBe('data: {"key": "value", "response": "response1"}');
+ expect(chunks[1]).toBe('data: {"key": "value", "response": "response2"}');
+ });
+
+ it('should ignore empty chunk', async () => {
+ // Arrange
+ const chunk = textEncoder.encode('\n\n');
+
+ // Act
+ await transformer.transform(chunk, undefined!);
+
+ // Assert
+ expect(chunks.join()).toBe('');
+ });
+
+ it('should split and concat delayed chunks', async () => {
+ // Arrange
+ const chunk1 = textEncoder.encode('data: {"key": "value", "respo');
+ const chunk2 = textEncoder.encode('nse": "response1"}\n\ndata: {"key": "val');
+ const chunk3 = textEncoder.encode('ue", "response": "response2"}\n\n');
+
+ // Act & Assert
+ await transformer.transform(chunk1, undefined!);
+ expect(transformer['parseChunk']).not.toHaveBeenCalled();
+ expect(chunks.length).toBe(0);
+ expect(transformer['buffer']).toBe('data: {"key": "value", "respo');
+
+ await transformer.transform(chunk2, undefined!);
+ expect(chunks.length).toBe(1);
+ expect(chunks[0]).toBe('data: {"key": "value", "response": "response1"}');
+ expect(transformer['buffer']).toBe('data: {"key": "val');
+
+ await transformer.transform(chunk3, undefined!);
+ expect(chunks.length).toBe(2);
+ expect(chunks[1]).toBe('data: {"key": "value", "response": "response2"}');
+ expect(transformer['buffer']).toBe('');
+ });
+
+ it('should ignore standalone [DONE]', async () => {
+ // Arrange
+ const chunk = textEncoder.encode('data: [DONE]\n\n');
+
+ // Act
+ await transformer.transform(chunk, undefined!);
+
+ // Assert
+ expect(transformer['parseChunk']).not.toHaveBeenCalled();
+ expect(chunks.length).toBe(0);
+ expect(transformer['buffer']).toBe('');
+ });
+
+ it('should ignore [DONE] in chunk', async () => {
+ // Arrange
+ const chunk = textEncoder.encode(
+ 'data: {"key": "value", "response": "response1"}\n\ndata: [DONE]\n\n',
+ );
+
+ // Act
+ await transformer.transform(chunk, undefined!);
+
+ // Assert
+ expect(chunks.length).toBe(1);
+ expect(chunks[0]).toBe('data: {"key": "value", "response": "response1"}');
+ expect(transformer['buffer']).toBe('');
+ });
+ });
+ });
+
+ describe('fillUrl', () => {
+ it('should return URL with account id', () => {
+ const url = fillUrl('80009000a000b000c000d000e000f000');
+ expect(url).toBe(
+ 'https://api.cloudflare.com/client/v4/accounts/80009000a000b000c000d000e000f000/ai/run/',
+ );
+ });
+ });
+
+ describe('maskAccountId', () => {
+ describe('desensitizeAccountId', () => {
+ it('should replace account id with **** in official API endpoint', () => {
+ const url =
+ 'https://api.cloudflare.com/client/v4/accounts/80009000a000b000c000d000e000f000/ai/run/';
+ const maskedUrl = desensitizeCloudflareUrl(url);
+ expect(maskedUrl).toBe('https://api.cloudflare.com/client/v4/accounts/****/ai/run/');
+ });
+
+ it('should replace account id with **** in custom API endpoint', () => {
+ const url =
+ 'https://api.cloudflare.com/custom/prefix/80009000a000b000c000d000e000f000/custom/suffix/';
+ const maskedUrl = desensitizeCloudflareUrl(url);
+ expect(maskedUrl).toBe('https://api.cloudflare.com/custom/prefix/****/custom/suffix/');
+ });
+ });
+
+ describe('desensitizeCloudflareUrl', () => {
+ it('should mask account id in official API endpoint', () => {
+ const url =
+ 'https://api.cloudflare.com/client/v4/accounts/80009000a000b000c000d000e000f000/ai/run/';
+ const maskedUrl = desensitizeCloudflareUrl(url);
+ expect(maskedUrl).toBe('https://api.cloudflare.com/client/v4/accounts/****/ai/run/');
+ });
+
+ it('should call desensitizeUrl for custom API endpoint', () => {
+ const url = 'https://custom.url/path';
+ vi.spyOn(desensitizeTool, 'desensitizeUrl').mockImplementation(
+ (_) => 'https://custom.mocked.url',
+ );
+ const maskedUrl = desensitizeCloudflareUrl(url);
+ expect(desensitizeTool.desensitizeUrl).toHaveBeenCalledWith('https://custom.url');
+ expect(maskedUrl).toBe('https://custom.mocked.url/path');
+ });
+
+ it('should mask account id in custom API endpoint', () => {
+ const url =
+ 'https://custom.url/custom/prefix/80009000a000b000c000d000e000f000/custom/suffix/';
+ const maskedUrl = desensitizeCloudflareUrl(url);
+ expect(maskedUrl).toBe('https://cu****om.url/custom/prefix/****/custom/suffix/');
+ });
+
+ it('should mask account id in custom API endpoint with query params', () => {
+ const url =
+ 'https://custom.url/custom/prefix/80009000a000b000c000d000e000f000/custom/suffix/?query=param';
+ const maskedUrl = desensitizeCloudflareUrl(url);
+ expect(maskedUrl).toBe(
+ 'https://cu****om.url/custom/prefix/****/custom/suffix/?query=param',
+ );
+ });
+
+ it('should mask account id in custom API endpoint with port', () => {
+ const url =
+ 'https://custom.url:8080/custom/prefix/80009000a000b000c000d000e000f000/custom/suffix/';
+ const maskedUrl = desensitizeCloudflareUrl(url);
+ expect(maskedUrl).toBe('https://cu****om.url:****/custom/prefix/****/custom/suffix/');
+ });
+ });
+ });
+
+ describe('modelManifest', () => {
+ describe('getModelBeta', () => {
+ it('should get beta property', () => {
+ const model = { properties: [{ property_id: 'beta', value: 'true' }] };
+ const beta = getModelBeta(model);
+ expect(beta).toBe(true);
+ });
+
+ it('should return false if beta property is false', () => {
+ const model = { properties: [{ property_id: 'beta', value: 'false' }] };
+ const beta = getModelBeta(model);
+ expect(beta).toBe(false);
+ });
+
+ it('should return false if beta property is not present', () => {
+ const model = { properties: [] };
+ const beta = getModelBeta(model);
+ expect(beta).toBe(false);
+ });
+ });
+
+ describe('getModelDisplayName', () => {
+ it('should return display name with beta suffix', () => {
+ const model = { name: 'model', properties: [{ property_id: 'beta', value: 'true' }] };
+ const name = getModelDisplayName(model, true);
+ expect(name).toBe('model (Beta)');
+ });
+
+ it('should return display name without beta suffix', () => {
+ const model = { name: 'model', properties: [] };
+ const name = getModelDisplayName(model, false);
+ expect(name).toBe('model');
+ });
+
+ it('should return model["name"]', () => {
+ const model = { id: 'modelID', name: 'modelName' };
+ const name = getModelDisplayName(model, false);
+ expect(name).toBe('modelName');
+ });
+
+ it('should return last part of model["name"]', () => {
+ const model = { name: '@provider/modelFamily/modelName' };
+ const name = getModelDisplayName(model, false);
+ expect(name).toBe('modelName');
+ });
+ });
+
+ describe('getModelFunctionCalling', () => {
+ it('should return true if function_calling property is true', () => {
+ const model = { properties: [{ property_id: 'function_calling', value: 'true' }] };
+ const functionCalling = getModelFunctionCalling(model);
+ expect(functionCalling).toBe(true);
+ });
+
+ it('should return false if function_calling property is false', () => {
+ const model = { properties: [{ property_id: 'function_calling', value: 'false' }] };
+ const functionCalling = getModelFunctionCalling(model);
+ expect(functionCalling).toBe(false);
+ });
+
+ it('should return false if function_calling property is not set', () => {
+ const model = { properties: [] };
+ const functionCalling = getModelFunctionCalling(model);
+ expect(functionCalling).toBe(false);
+ });
+ });
+
+ describe('getModelTokens', () => {
+ it('should return tokens property value', () => {
+ const model = { properties: [{ property_id: 'max_total_tokens', value: '100' }] };
+ const tokens = getModelTokens(model);
+ expect(tokens).toBe(100);
+ });
+
+ it('should return undefined if tokens property is not present', () => {
+ const model = { properties: [] };
+ const tokens = getModelTokens(model);
+ expect(tokens).toBeUndefined();
+ });
+ });
+ });
+});
diff --git a/src/libs/agent-runtime/utils/cloudflareHelpers.ts b/src/libs/agent-runtime/utils/cloudflareHelpers.ts
new file mode 100644
index 000000000000..5fd596f4c7c5
--- /dev/null
+++ b/src/libs/agent-runtime/utils/cloudflareHelpers.ts
@@ -0,0 +1,134 @@
+import { desensitizeUrl } from '../utils/desensitizeUrl';
+
+class CloudflareStreamTransformer {
+ private textDecoder = new TextDecoder();
+ private buffer: string = '';
+
+ private parseChunk(chunk: string, controller: TransformStreamDefaultController) {
+ const dataPrefix = /^data: /;
+ const json = chunk.replace(dataPrefix, '');
+ const parsedChunk = JSON.parse(json);
+ controller.enqueue(`event: text\n`);
+ controller.enqueue(`data: ${JSON.stringify(parsedChunk.response)}\n\n`);
+ }
+
+ public async transform(chunk: Uint8Array, controller: TransformStreamDefaultController) {
+ let textChunk = this.textDecoder.decode(chunk);
+ if (this.buffer.trim() !== '') {
+ textChunk = this.buffer + textChunk;
+ this.buffer = '';
+ }
+ const splits = textChunk.split('\n\n');
+ for (let i = 0; i < splits.length - 1; i++) {
+ if (/\[DONE]/.test(splits[i].trim())) {
+ return;
+ }
+ this.parseChunk(splits[i], controller);
+ }
+ const lastChunk = splits.at(-1)!;
+ if (lastChunk.trim() !== '') {
+ this.buffer += lastChunk; // does not need to be trimmed.
+ } // else drop.
+ }
+}
+
+const CF_PROPERTY_NAME = 'property_id';
+const DEFAULT_BASE_URL_PREFIX = 'https://api.cloudflare.com';
+
+function fillUrl(accountID: string): string {
+ return `${DEFAULT_BASE_URL_PREFIX}/client/v4/accounts/${accountID}/ai/run/`;
+}
+
+function desensitizeAccountId(path: string): string {
+ return path.replace(/\/[\dA-Fa-f]{32}\//, '/****/');
+}
+
+function desensitizeCloudflareUrl(url: string): string {
+ const urlObj = new URL(url);
+ let { protocol, hostname, port, pathname, search } = urlObj;
+ if (url.startsWith(DEFAULT_BASE_URL_PREFIX)) {
+ return `${protocol}//${hostname}${port ? `:${port}` : ''}${desensitizeAccountId(pathname)}${search}`;
+ } else {
+ const desensitizedUrl = desensitizeUrl(`${protocol}//${hostname}${port ? `:${port}` : ''}`);
+ if (desensitizedUrl.endsWith('/') && pathname.startsWith('/')) {
+ pathname = pathname.slice(1);
+ }
+ return `${desensitizedUrl}${desensitizeAccountId(pathname)}${search}`;
+ }
+}
+
+function getModelBeta(model: any): boolean {
+ try {
+ const betaProperty = model['properties'].filter(
+ (property: any) => property[CF_PROPERTY_NAME] === 'beta',
+ );
+ if (betaProperty.length === 1) {
+ return betaProperty[0]['value'] === 'true'; // This is a string now.
+ }
+ return false;
+ } catch {
+ return false;
+ }
+}
+
+function getModelDisplayName(model: any, beta: boolean): string {
+ const modelId = model['name'];
+ let name = modelId.split('/').at(-1)!;
+ if (beta) {
+ name += ' (Beta)';
+ }
+ return name;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
+function getModelFunctionCalling(model: any): boolean {
+ try {
+ const fcProperty = model['properties'].filter(
+ (property: any) => property[CF_PROPERTY_NAME] === 'function_calling',
+ );
+ if (fcProperty.length === 1) {
+ return fcProperty[0]['value'] === 'true';
+ }
+ return false;
+ } catch {
+ return false;
+ }
+}
+
+function getModelTokens(model: any): number | undefined {
+ try {
+ const tokensProperty = model['properties'].filter(
+ (property: any) => property[CF_PROPERTY_NAME] === 'max_total_tokens',
+ );
+ if (tokensProperty.length === 1) {
+ return parseInt(tokensProperty[0]['value']);
+ }
+ return undefined;
+ } catch {
+ return undefined;
+ }
+}
+
+function convertModelManifest(model: any) {
+ const modelBeta = getModelBeta(model);
+ return {
+ description: model['description'],
+ displayName: getModelDisplayName(model, modelBeta),
+ enabled: !modelBeta,
+ functionCall: false, //getModelFunctionCalling(model),
+ id: model['name'],
+ tokens: getModelTokens(model),
+ };
+}
+
+export {
+ CloudflareStreamTransformer,
+ convertModelManifest,
+ DEFAULT_BASE_URL_PREFIX,
+ desensitizeCloudflareUrl,
+ fillUrl,
+ getModelBeta,
+ getModelDisplayName,
+ getModelFunctionCalling,
+ getModelTokens,
+};
diff --git a/src/locales/default/modelProvider.ts b/src/locales/default/modelProvider.ts
index f3ba99530b4a..73d4eb2359ad 100644
--- a/src/locales/default/modelProvider.ts
+++ b/src/locales/default/modelProvider.ts
@@ -21,7 +21,7 @@ export default {
},
bedrock: {
accessKeyId: {
- desc: '填入AWS Access Key Id',
+ desc: '填入 AWS Access Key Id',
placeholder: 'AWS Access Key Id',
title: 'AWS Access Key Id',
},
@@ -52,6 +52,18 @@ export default {
title: '使用自定义 Bedrock 鉴权信息',
},
},
+ cloudflare: {
+ apiKey: {
+ desc: '请填写 Cloudflare API Key',
+ placeholder: 'Cloudflare API Key',
+ title: 'Cloudflare API Key',
+ },
+ baseURLOrAccountID: {
+ desc: '填入 Cloudflare 账户 ID 或 自定义 API 地址',
+ placeholder: 'Cloudflare Account ID / custom API URL',
+ title: 'Cloudflare 账户 ID / API 地址',
+ }
+ },
github: {
personalAccessToken: {
desc: '填入你的 Github PAT,点击 [这里](https://github.com/settings/tokens) 创建',
diff --git a/src/server/globalConfig/index.ts b/src/server/globalConfig/index.ts
index 7c4704cda103..437486ce0a92 100644
--- a/src/server/globalConfig/index.ts
+++ b/src/server/globalConfig/index.ts
@@ -99,6 +99,9 @@ export const getServerGlobalConfig = () => {
BAICHUAN_MODEL_LIST,
ENABLED_TAICHU,
+
+ ENABLED_CLOUDFLARE,
+
TAICHU_MODEL_LIST,
ENABLED_AI21,
@@ -202,6 +205,7 @@ export const getServerGlobalConfig = () => {
modelString: AWS_BEDROCK_MODEL_LIST,
}),
},
+ cloudflare: { enabled: ENABLED_CLOUDFLARE },
deepseek: {
enabled: ENABLED_DEEPSEEK,
enabledModels: extractEnabledModels(DEEPSEEK_MODEL_LIST),
diff --git a/src/server/modules/AgentRuntime/index.ts b/src/server/modules/AgentRuntime/index.ts
index 0fc0cfe557e1..f440a1058851 100644
--- a/src/server/modules/AgentRuntime/index.ts
+++ b/src/server/modules/AgentRuntime/index.ts
@@ -210,6 +210,17 @@ const getLlmOptionsFromPayload = (provider: string, payload: JWTPayload) => {
return { apiKey };
}
+ case ModelProvider.Cloudflare: {
+ const { CLOUDFLARE_API_KEY, CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID } = getLLMConfig();
+
+ const apiKey = apiKeyManager.pick(payload?.apiKey || CLOUDFLARE_API_KEY);
+ const baseURLOrAccountID =
+ payload.apiKey && payload.cloudflareBaseURLOrAccountID
+ ? payload.cloudflareBaseURLOrAccountID
+ : CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID;
+
+ return { apiKey, baseURLOrAccountID };
+ }
case ModelProvider.Ai360: {
const { AI360_API_KEY } = getLLMConfig();
diff --git a/src/services/_auth.ts b/src/services/_auth.ts
index 7795290e7791..8b73330300d6 100644
--- a/src/services/_auth.ts
+++ b/src/services/_auth.ts
@@ -69,6 +69,15 @@ export const getProviderAuthPayload = (provider: string) => {
return { endpoint: config?.baseURL };
}
+ case ModelProvider.Cloudflare: {
+ const config = keyVaultsConfigSelectors.cloudflareConfig(useUserStore.getState());
+
+ return {
+ apiKey: config?.apiKey,
+ cloudflareBaseURLOrAccountID: config?.baseURLOrAccountID,
+ };
+ }
+
default: {
const config = keyVaultsConfigSelectors.getVaultByProvider(provider as GlobalLLMProviderKey)(
useUserStore.getState(),
diff --git a/src/services/chat.ts b/src/services/chat.ts
index 0c9b17f9307a..f57f05db2466 100644
--- a/src/services/chat.ts
+++ b/src/services/chat.ts
@@ -175,6 +175,13 @@ export function initializeWithClientStore(provider: string, payload: any) {
case ModelProvider.ZeroOne: {
break;
}
+ case ModelProvider.Cloudflare: {
+ providerOptions = {
+ apikey: providerAuthPayload?.apiKey,
+ baseURLOrAccountID: providerAuthPayload?.cloudflareBaseURLOrAccountID,
+ };
+ break;
+ }
}
/**
diff --git a/src/store/user/slices/modelList/selectors/keyVaults.ts b/src/store/user/slices/modelList/selectors/keyVaults.ts
index 6f5047d55481..0ec5188461df 100644
--- a/src/store/user/slices/modelList/selectors/keyVaults.ts
+++ b/src/store/user/slices/modelList/selectors/keyVaults.ts
@@ -18,6 +18,7 @@ const wenxinConfig = (s: UserStore) => keyVaultsSettings(s).wenxin || {};
const ollamaConfig = (s: UserStore) => keyVaultsSettings(s).ollama || {};
const sensenovaConfig = (s: UserStore) => keyVaultsSettings(s).sensenova || {};
const azureConfig = (s: UserStore) => keyVaultsSettings(s).azure || {};
+const cloudflareConfig = (s: UserStore) => keyVaultsSettings(s).cloudflare || {};
const getVaultByProvider = (provider: GlobalLLMProviderKey) => (s: UserStore) =>
(keyVaultsSettings(s)[provider] || {}) as OpenAICompatibleKeyVault &
AzureOpenAIKeyVault &
@@ -38,6 +39,7 @@ const password = (s: UserStore) => keyVaultsSettings(s).password || '';
export const keyVaultsConfigSelectors = {
azureConfig,
bedrockConfig,
+ cloudflareConfig,
getVaultByProvider,
isProviderApiKeyNotEmpty,
isProviderEndpointNotEmpty,
diff --git a/src/store/user/slices/modelList/selectors/modelConfig.ts b/src/store/user/slices/modelList/selectors/modelConfig.ts
index a41e14a9ab8c..7f68dcacacb0 100644
--- a/src/store/user/slices/modelList/selectors/modelConfig.ts
+++ b/src/store/user/slices/modelList/selectors/modelConfig.ts
@@ -69,6 +69,7 @@ const openAIConfig = (s: UserStore) => currentLLMSettings(s).openai;
const bedrockConfig = (s: UserStore) => currentLLMSettings(s).bedrock;
const ollamaConfig = (s: UserStore) => currentLLMSettings(s).ollama;
const azureConfig = (s: UserStore) => currentLLMSettings(s).azure;
+const cloudflareConfig = (s: UserStore) => currentLLMSettings(s).cloudflare;
const sensenovaConfig = (s: UserStore) => currentLLMSettings(s).sensenova;
const isAzureEnabled = (s: UserStore) => currentLLMSettings(s).azure.enabled;
@@ -76,6 +77,7 @@ const isAzureEnabled = (s: UserStore) => currentLLMSettings(s).azure.enabled;
export const modelConfigSelectors = {
azureConfig,
bedrockConfig,
+ cloudflareConfig,
currentEditingCustomModelCard,
getCustomModelCard,
diff --git a/src/types/user/settings/keyVaults.ts b/src/types/user/settings/keyVaults.ts
index 8ff980fa055f..ad0e5221d586 100644
--- a/src/types/user/settings/keyVaults.ts
+++ b/src/types/user/settings/keyVaults.ts
@@ -16,6 +16,11 @@ export interface AWSBedrockKeyVault {
sessionToken?: string;
}
+export interface CloudflareKeyVault {
+ apiKey?: string;
+ baseURLOrAccountID?: string;
+}
+
export interface SenseNovaKeyVault {
sensenovaAccessKeyID?: string;
sensenovaAccessKeySecret?: string;
@@ -33,6 +38,7 @@ export interface UserKeyVaults {
azure?: AzureOpenAIKeyVault;
baichuan?: OpenAICompatibleKeyVault;
bedrock?: AWSBedrockKeyVault;
+ cloudflare?: CloudflareKeyVault;
deepseek?: OpenAICompatibleKeyVault;
fireworksai?: OpenAICompatibleKeyVault;
github?: OpenAICompatibleKeyVault;