From 6a7ea4ae951bbffab7f7f36dfd065b03390c8ece Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 1 Sep 2026 17:38:18 +0800 Subject: [PATCH] feat(ai): add Jalapeno Cloud partner preset --- .../public/icons/ai/jalapeno-cloud.png | Bin 0 -> 10542 bytes .../src/components/editor/AiAssistant.vue | 28 ++++- .../editor/EditorSettingsDialog.vue | 105 ++++++++++++------ .../src/components/icons/AiProviderLogo.vue | 15 +-- apps/desktop/src/i18n/locales/en.ts | 6 + apps/desktop/src/i18n/locales/es.ts | 6 + apps/desktop/src/i18n/locales/it.ts | 6 + apps/desktop/src/i18n/locales/ja.ts | 6 + apps/desktop/src/i18n/locales/ko.ts | 6 + apps/desktop/src/i18n/locales/pt-BR.ts | 6 + apps/desktop/src/i18n/locales/zh-CN.ts | 6 + apps/desktop/src/i18n/locales/zh-TW.ts | 6 + .../__tests__/settingsPageNavigation.spec.ts | 1 + apps/desktop/src/stores/settingsStore.ts | 56 +++++++++- crates/dbx-core/src/ai.rs | 29 +++++ packages/app-tests/settingsStore.test.ts | 18 ++- 16 files changed, 253 insertions(+), 47 deletions(-) create mode 100644 apps/desktop/public/icons/ai/jalapeno-cloud.png diff --git a/apps/desktop/public/icons/ai/jalapeno-cloud.png b/apps/desktop/public/icons/ai/jalapeno-cloud.png new file mode 100644 index 0000000000000000000000000000000000000000..59e8801d0d61d5ecd8cf1104e39aca14ca1e0c60 GIT binary patch literal 10542 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

e^}PYPs0QAPM+?_C z|Es(4y~eQu>A$ENlS~TJ^b9Qt+{s6&tR|;~vI?#ndNL zM;kX5EBN?i)6<>0sR7-6`DB~;{0mScex5*?d{1FKV((q9o5Jx%Lx|`Q>$3b*_XK;d zg(hGLv}o^N{n$2j?6dR>!|PYq{Nk3ZCh#<^)j`(YL{Y0tr`K4?@sv7u{(E-4w^sLu zteWN(cGqRR|0_^40@fj@_;~|Ii=Uz5ixOjdQ6A^|7q+?P+`G zP@})p4He8BpdO^X)`|P+NtBxs2ZB2~&1TKxuF(9ma#FBF;Vi}2(84fT8N<1%3y#sQ zpZ!{wS9;l0id?0sh%4XN`1`30Eq3YABh`P!T-Aw11>1nv%4uZ5?8`GS^=3XaBk4YE z=C8ux+d5DQ!534H716!;$wUwR3Q%bPOBwWFAi+%=!m4nH_l5Tz&rF{-aB-DQVXDx9 z-_QVf=;fJ_jz3j7EG6CBQWClB+;~7jbhuaM%ZAndTSO?Vg$)6=j4qx-Gj2gILwRa~ zR78JQ%dlP)7~KuSq3Ev`8`TXwY$|ENr8HIn4f3ZcMm)5zB6eR*k)8}N&i<2-y1t;b zsnZ7=g4ZLJ`dXK6{YGrTB=s|$Bvrf9`UFi-Zr^!!$nV_XKlN|JUppb)vl@D0tH^iDU zmKyGi4Fs>do;_~bPK(hmh~RCzv+yV2$?xPKpm+V(LY!lAgS>NMyWhVs#J$@N7pl?R z|0V0zj@qVD$*}iw)-F0tP*;%k?~%!na4BubR={i38Hm|Q6A(0>Au08h%6$7uBsD+3 zS*ji&!(;hQWkNG-fRFWrR$TQEAU9Z*<#QZ4akT`zI@=r%-5PNC`SYc$k8ymNBf3Ml zKXGZFBAzJhAG96mAUe(Vn9Yq^YMXpN^e7tY^kf5t`ts`1jSs*2=Tcf7nh)osVJQ8ej8kIGg8JOovoOgDK0YLn@ zh~9;ef+cye3sMm?=nv+Mcp(xZ`VB{qAa zSauF(Q!>I)W;_`^;*H{uXVS(NQ@o-?BAoQ^!OwE=eWXGEUOAIthECj!m@mg}h;xG& zizVypVJI0t4aJN95-(A$adyH#EY{Y6E&DMU?R(lI!O+vR#As0KF<-~lTJDwa9my-?7veEW3v0n z0&0o48p_>$hyA=f^x$T2BadDo0)R)9T_Jb;(=*a-9%c9m(X8{J5WQn?ir%oZTR}Oc0f)?Q#UG-8Fh6R74;pE zXwYPxXq{}rr}DD?RKT?(vjeLL0V+qD2*viBe&#R7JIQ{7Ix@YPIUF&2s8f@)O|V36 z1M$4fO=y35p)-6&3sr=>QVfKL93NvauzG*Ec-g>XPi0^7DJ|nH2NSC*rN+a>q;Qc% z7l$E@9P2WpFRVZ(&2OUo$EPkJx5$nrcuyZTp<>T{eYCjG*n`_QU*Jrx#2hiGD3~}#UzMHVZ4VigCfl@)NP4Hx z!qxehu{I0VfKgVj_OJprX1?jsLxJM&n=uC3&IVonYt!^#jHWXKN^BwW9f_&wZ_dIY z01gD@$)ziR^&<@lCf6>2Fxfr|2T#jlAQR0 z?=>fLsQ=-D+g~R~0%z6b`==3yw|fDSWzcSP*c%trAO5J&QooNXdOI;W_IiaFe3$JD z8vV;=zNL0968Y_Cl7p~;!D^D+dTS={4(^~CAT;t^Mjy=u0cBa{;KWN^+j8K6Y%) zj-6&SIScRBek5H8ukt}n#zl|U z33@ISUjs4;@W0iIjbvpl zf(im!%s$2se-*3;)!$Ji_zY7})b-VW(w=sUUH58OSZ2PJwrNy^3D1;3v3+NQ{VzX)0g)<$CoD1X zV2a53mAGgn>_3$7W>kW?re&Cc+rI0bj**1zXXSxroqVt1{YzTXb(WzjxaaK&+kWSx zz;YZpX+^a4%5tCrdgiO|-k<(DV{#yeucEN#R~zpV9*i} { }); function isModelCandidate(config: AiConfigItem): boolean { - return isAiConfigModelCandidate(config, AI_PROVIDER_PRESETS[config.provider].requiresApiKey, supportsCliProviders); + return isAiConfigModelCandidate(config, getAiProviderPreset(config.provider, config.endpoint).requiresApiKey, supportsCliProviders); } function getModelsForConfig(configId: string) { @@ -724,8 +724,14 @@ function getModelsForConfig(configId: string) { return aiModelOptions(config, modelCatalogs.get(configId)?.models ?? []); } +function aiConfigProviderLabel(config: { provider: AiConfigItem["provider"]; endpoint: string } | null | undefined): string { + if (!config) return aiProviderLabel("claude", t); + const preset = getAiProviderPreset(config.provider, config.endpoint); + return preset.provider === "custom" ? aiProviderLabel(config.provider, t) : preset.label; +} + function configMatchesModelQuery(config: AiConfigItem, query: string): boolean { - return config.name.toLowerCase().includes(query) || config.provider.toLowerCase().includes(query) || aiProviderLabel(config.provider, t).toLowerCase().includes(query) || AI_PROVIDER_PRESETS[config.provider].label.toLowerCase().includes(query); + return config.name.toLowerCase().includes(query) || config.provider.toLowerCase().includes(query) || aiConfigProviderLabel(config).toLowerCase().includes(query); } function getConfigModelOptions(config: AiConfigItem) { @@ -5001,7 +5007,13 @@ async function openExternalUrl(url: string) { @@ -5015,7 +5027,13 @@ async function openExternalUrl(url: string) {

+
+ @@ -7388,7 +7423,13 @@ onUnmounted(() => {
- +
+ + +
diff --git a/apps/desktop/src/components/icons/AiProviderLogo.vue b/apps/desktop/src/components/icons/AiProviderLogo.vue index 5e602bff04..475540be45 100644 --- a/apps/desktop/src/components/icons/AiProviderLogo.vue +++ b/apps/desktop/src/components/icons/AiProviderLogo.vue @@ -9,22 +9,23 @@ const props = defineProps<{ provider: AiProvider; label: string; iconSlug?: string; + iconPath?: string; }>(); const failed = ref(false); const { isDark } = useTheme(); -watch( - () => props.iconSlug, - () => { - failed.value = false; - }, -); +watch([() => props.iconSlug, () => props.iconPath], () => { + failed.value = false; +}); const usesWhiteDarkIcon = computed( - () => props.provider === "claude" || props.provider === "anthropic-compatible" || props.provider === "ollama" || props.provider === "openai" || props.provider === "openai-compatible" || props.provider === "opencode-cli" || props.provider === "cursor-cli" || props.provider === "grok-cli", + () => + !props.iconPath && + (props.provider === "claude" || props.provider === "anthropic-compatible" || props.provider === "ollama" || props.provider === "openai" || props.provider === "openai-compatible" || props.provider === "opencode-cli" || props.provider === "cursor-cli" || props.provider === "grok-cli"), ); const localIconUrl = computed(() => { + if (props.iconPath) return webPath(props.iconPath); if (props.provider === "openai-compatible") return webPath("/icons/ai/openai.svg"); return props.iconSlug ? webPath(`/icons/ai/${props.iconSlug}.svg`) : ""; }); diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index dd89247310..3107a4588b 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -2494,6 +2494,12 @@ export default { noConfig: "AI is not configured. Please set up your AI provider in Settings > AI.", provider: "Provider", providerCustom: "Custom", + builtinProviders: "Built-in support", + partnerProviders: "Premium sponsors", + jalapenoSponsored: "$1 free credit", + jalapenoDescription: "🎉 Enterprise-grade AI trusted by leading teams is now available to you! Try cutting-edge models at 50% off and get more bonus credit when you top up more!", + visitPartner: "Visit partner website", + getApiKey: "Get API key", providerStatusActive: "Active", providerStatusConfigured: "Configured", switchProvider: "Switch Provider", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 16b0f58b09..7feb5620d3 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -2430,6 +2430,12 @@ export default withEnglishFallback({ noConfig: "La IA no está configurada. Configura tu proveedor de IA en Ajustes > IA.", provider: "Proveedor", providerCustom: "Personalizado", + builtinProviders: "Compatibilidad integrada", + partnerProviders: "Patrocinadores destacados", + jalapenoSponsored: "1 USD de crédito gratis", + jalapenoDescription: "🎉 ¡La IA de nivel empresarial que utilizan los equipos líderes ya está disponible para ti! ¡Prueba modelos de vanguardia con un 50 % de descuento y recibe más bonificaciones al recargar!", + visitPartner: "Visitar el sitio web del socio", + getApiKey: "Obtener clave API", providerStatusActive: "Activo", providerStatusConfigured: "Configurado", switchProvider: "Cambiar proveedor", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 80d371f979..667980fade 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -2428,6 +2428,12 @@ export default withEnglishFallback({ noConfig: "L'AI non è configurata. Configura il tuo provider AI in Impostazioni > AI.", provider: "Provider", providerCustom: "Personalizzato", + builtinProviders: "Supporto integrato", + partnerProviders: "Sponsor selezionati", + jalapenoSponsored: "$1 di credito gratuito", + jalapenoDescription: "🎉 L'AI di livello enterprise usata dai team migliori è ora disponibile per te! Prova i modelli all'avanguardia con il 50% di sconto e ottieni più bonus ricaricando di più!", + visitPartner: "Visita il sito del partner", + getApiKey: "Ottieni la chiave API", providerStatusActive: "Attivo", providerStatusConfigured: "Configurato", switchProvider: "Cambia provider", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 85ce619382..00516a36e3 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -2454,6 +2454,12 @@ export default withEnglishFallback({ noConfig: "AIが設定されていません。設定 > AIでAIプロバイダーを設定してください。", provider: "プロバイダー", providerCustom: "カスタム", + builtinProviders: "組み込みサポート", + partnerProviders: "優良スポンサー", + jalapenoSponsored: "$1分の無料クレジット", + jalapenoDescription: "🎉トップチームも利用するエンタープライズ向けAIが、あなたのもとへ!最先端モデルを50%オフで今すぐ体験。さらにチャージするほどボーナスも増量!", + visitPartner: "パートナーサイトを開く", + getApiKey: "APIキーを取得", providerStatusActive: "使用中", providerStatusConfigured: "設定済み", switchProvider: "プロバイダーを切替", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 853b1d96db..d439fc6e39 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -2326,6 +2326,12 @@ export default withEnglishFallback({ noConfig: "AI가 구성되지 않았습니다. 설정 > AI에서 AI 공급자를 설정하세요.", provider: "공급자", providerCustom: "사용자 지정", + builtinProviders: "내장 지원", + partnerProviders: "우수 스폰서", + jalapenoSponsored: "$1 무료 크레딧", + jalapenoDescription: "🎉 최고의 팀들이 사용하는 엔터프라이즈급 AI를 이제 만나보세요! 최첨단 모델을 50% 할인으로 바로 체험하고, 충전할수록 더 많은 보너스를 받으세요!", + visitPartner: "파트너 웹사이트 방문", + getApiKey: "API 키 받기", providerStatusActive: "활성", providerStatusConfigured: "구성됨", switchProvider: "공급자 전환", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 4b726238a2..453b5a31ee 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -2430,6 +2430,12 @@ export default withEnglishFallback({ noConfig: "A AI não está configurada. Configure seu provedor de AI em Configurações > AI.", provider: "Provedor", providerCustom: "Personalizado", + builtinProviders: "Suporte integrado", + partnerProviders: "Patrocinadores premium", + jalapenoSponsored: "$1 de crédito grátis", + jalapenoDescription: "🎉 A IA de nível empresarial usada pelas principais equipes agora está disponível para você! Experimente modelos de ponta com 50% de desconto e ganhe mais bônus ao recarregar mais!", + visitPartner: "Visitar site do parceiro", + getApiKey: "Obter chave de API", providerStatusActive: "Ativo", providerStatusConfigured: "Configurado", switchProvider: "Trocar provedor", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 43138519cc..5a4cda7da2 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -2417,6 +2417,12 @@ export default withEnglishFallback({ noConfig: "AI 尚未配置。请在 设置 > AI 中配置你的 AI 服务。", provider: "提供商", providerCustom: "自定义", + builtinProviders: "内置支持", + partnerProviders: "优质赞助商", + jalapenoSponsored: "$1 免费额度", + jalapenoDescription: "🎉顶尖团队都在用的企业级 AI,现在为你而来!【5折前沿模型】立刻体验!更有【多充多送】羊毛狠狠薅!", + visitPartner: "访问合作伙伴网站", + getApiKey: "获取 API Key", providerStatusActive: "当前", providerStatusConfigured: "已配置", switchProvider: "切换服务商", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 6d8136c21e..0bab8cc23a 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -2427,6 +2427,12 @@ export default withEnglishFallback({ noConfig: "AI 尚未設定。請在 設定 > AI 中設定你的 AI 服務。", provider: "供應商", providerCustom: "自訂", + builtinProviders: "內建支援", + partnerProviders: "優質贊助商", + jalapenoSponsored: "$1 免費額度", + jalapenoDescription: "🎉頂尖團隊都在使用的企業級 AI,現在也為你而來!【5 折前沿模型】立即體驗!還有【儲值越多送越多】的優惠!", + visitPartner: "前往合作夥伴網站", + getApiKey: "取得 API Key", providerStatusActive: "目前", providerStatusConfigured: "已設定", switchProvider: "切換服務商", diff --git a/apps/desktop/src/lib/settings/__tests__/settingsPageNavigation.spec.ts b/apps/desktop/src/lib/settings/__tests__/settingsPageNavigation.spec.ts index ac2d37963e..37d7aedb61 100644 --- a/apps/desktop/src/lib/settings/__tests__/settingsPageNavigation.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/settingsPageNavigation.spec.ts @@ -29,6 +29,7 @@ describe("settings page navigation", () => { expect(appSource).toContain(':ai-config-draft="settingsAiConfigDraft"'); expect(appSource).toContain(':ai-config-request-id="settingsAiConfigRequestId"'); expect(settingsDialogSource).toContain("aiEnterEditMode();"); + expect(handlerSource).toContain("aiEditProviderPresetId.value = getAiProviderPresetId(draft.provider, draft.endpoint);"); expect(settingsDialogSource).toContain('aiEditApiKey.value = "";'); expect(settingsDialogSource).toContain("importClipboardApiKeyAfterConfirmation"); expect(handlerStart).toBeGreaterThanOrEqual(0); diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 25b68404de..731cb4aa73 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -170,7 +170,17 @@ export function normalizeDuckDbWorkerMaxProcesses(value: unknown): number { export interface AiProviderPreset extends Omit { label: string; iconSlug?: string; + iconPath?: string; requiresApiKey: boolean; + group?: "builtin" | "partner"; +} + +export interface AiPartnerProviderPreset extends AiProviderPreset { + id: string; + group: "partner"; + websiteUrl: string; + apiKeyUrl: string; + descriptionKey: string; } export const AI_PROVIDER_PRESETS: Record = { @@ -360,9 +370,51 @@ export function aiProviderLabel(provider: AiProvider, t: (key: string) => string return AI_PROVIDER_PRESETS[provider].label; } +export const AI_PROVIDER_PARTNER_PRESETS: readonly AiPartnerProviderPreset[] = [ + { + id: "jalapeno-cloud", + label: "Jalapeno Cloud", + iconPath: "/icons/ai/jalapeno-cloud.png", + group: "partner", + provider: "openai-compatible", + endpoint: "https://api.jalapeno-cloud.ai/v1", + model: "GLM-5.2", + models: [{ name: "GLM-5.2" }, { name: "DeepSeek-V4-Pro" }, { name: "MiniMax-M3" }], + apiStyle: "completions", + authMethod: "bearer", + requiresApiKey: true, + websiteUrl: "https://www.jalapeno-cloud.ai/dbx", + apiKeyUrl: "https://www.jalapeno-cloud.ai/dbx", + descriptionKey: "ai.jalapenoDescription", + }, +]; + +function normalizeAiProviderEndpoint(endpoint: string): string { + return endpoint.trim().replace(/\/+$/, "").toLowerCase(); +} + +export function getAiProviderPreset(provider: AiProvider, endpoint = ""): AiProviderPreset | AiPartnerProviderPreset { + const normalizedEndpoint = normalizeAiProviderEndpoint(endpoint); + const partnerPreset = AI_PROVIDER_PARTNER_PRESETS.find((preset) => preset.provider === provider && normalizeAiProviderEndpoint(preset.endpoint) === normalizedEndpoint); + return partnerPreset ?? AI_PROVIDER_PRESETS[provider]; +} + +export function getAiProviderPresetOption(id: string): AiProviderPreset | AiPartnerProviderPreset { + return AI_PROVIDER_PARTNER_PRESETS.find((preset) => preset.id === id) ?? AI_PROVIDER_PRESETS[id as AiProvider] ?? AI_PROVIDER_PRESETS.custom; +} + +export function getAiProviderPresetId(provider: AiProvider, endpoint = ""): string { + const preset = getAiProviderPreset(provider, endpoint); + return "id" in preset ? preset.id : provider; +} + +export function isAiPartnerProviderPreset(preset: AiProviderPreset | AiPartnerProviderPreset): preset is AiPartnerProviderPreset { + return preset.group === "partner"; +} + const defaultConfigs: Record> = Object.fromEntries( Object.entries(AI_PROVIDER_PRESETS).map(([provider, preset]) => { - const { label: _label, iconSlug: _iconSlug, requiresApiKey: _requiresApiKey, ...config } = preset; + const { label: _label, iconSlug: _iconSlug, iconPath: _iconPath, group: _group, requiresApiKey: _requiresApiKey, ...config } = preset; return [provider, config]; }), ) as Record>; @@ -1814,7 +1866,7 @@ export const useSettingsStore = defineStore("settings", () => { if (!activeModel.value) return false; const config = aiConfigs.value.find((c) => c.id === activeModel.value!.configId); if (!config) return false; - const preset = AI_PROVIDER_PRESETS[config.provider]; + const preset = getAiProviderPreset(config.provider, config.endpoint); if ( config.provider === "codex-cli" || config.provider === "claude-code-cli" || diff --git a/crates/dbx-core/src/ai.rs b/crates/dbx-core/src/ai.rs index 8119629e58..5b059b8204 100644 --- a/crates/dbx-core/src/ai.rs +++ b/crates/dbx-core/src/ai.rs @@ -1557,6 +1557,16 @@ fn normalized_api_key(config: &AiConfig) -> &str { config.api_key.trim() } +fn is_jalapeno_config(config: &AiConfig) -> bool { + if !matches!(config.provider, AiProvider::OpenaiCompatible) { + return false; + } + reqwest::Url::parse(config.endpoint.trim()) + .ok() + .and_then(|url| url.host_str().map(|host| host.eq_ignore_ascii_case("api.jalapeno-cloud.ai"))) + .unwrap_or(false) +} + fn validate_config(config: &AiConfig) -> Result<(), String> { crate::ai_effort::validate_runtime_effort(config)?; if is_cli_provider(&config.provider) { @@ -1591,6 +1601,9 @@ fn validate_model_list_config(config: &AiConfig) -> Result<(), String> { pub fn maybe_bearer_headers(config: &AiConfig) -> Result { let mut headers = HeaderMap::new(); headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + if is_jalapeno_config(config) { + headers.insert("HTTP-Referer", HeaderValue::from_static("https://dbxio.com")); + } let api_key = normalized_api_key(config); if !api_key.is_empty() { headers.insert(AUTHORIZATION, HeaderValue::from_str(&format!("Bearer {api_key}")).map_err(|e| e.to_string())?); @@ -6148,6 +6161,22 @@ mod tests { assert!(maybe_bearer_headers(&config).unwrap_err().contains("Invalid AI custom header value")); } + #[test] + fn jalapeno_requests_include_dbx_referer() { + let mut config = test_config(AiProvider::OpenaiCompatible); + config.endpoint = "https://api.jalapeno-cloud.ai/v1".to_string(); + + let headers = maybe_bearer_headers(&config).unwrap(); + assert_eq!(headers.get("HTTP-Referer").unwrap(), "https://dbxio.com"); + + config.endpoint = "https://api.jalapeno-cloud.ai.example.com/v1".to_string(); + assert!(maybe_bearer_headers(&config).unwrap().get("HTTP-Referer").is_none()); + + config.endpoint = "https://api.jalapeno-cloud.ai/v1".to_string(); + config.provider = AiProvider::Custom; + assert!(maybe_bearer_headers(&config).unwrap().get("HTTP-Referer").is_none()); + } + #[test] fn claude_system_prompt_substitutes_default_when_empty() { // Empty or whitespace-only prompts must fall back to a non-empty value, diff --git a/packages/app-tests/settingsStore.test.ts b/packages/app-tests/settingsStore.test.ts index 3ae9bbaf46..4222bab3b0 100644 --- a/packages/app-tests/settingsStore.test.ts +++ b/packages/app-tests/settingsStore.test.ts @@ -6,7 +6,7 @@ import { DEFAULT_SQL_FORMATTER_SETTINGS } from "../../apps/desktop/src/lib/sql/s import { DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS } from "../../apps/desktop/src/lib/table/tableColumnTemplates.ts"; import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, SYSTEM_UI_FONT_FAMILY } from "../../apps/desktop/src/lib/app/appFonts.ts"; import { tableOpenPageLimit } from "../../apps/desktop/src/lib/table/tableOpenPageLimit.ts"; -import { AI_PROVIDER_PRESETS, DEFAULT_EDITOR_SETTINGS, EXECUTE_MODE_CURRENT_DEFAULT_VERSION, normalizeAiConfig, normalizeEditorSettings, useSettingsStore } from "../../apps/desktop/src/stores/settingsStore.ts"; +import { AI_PROVIDER_PARTNER_PRESETS, AI_PROVIDER_PRESETS, DEFAULT_EDITOR_SETTINGS, EXECUTE_MODE_CURRENT_DEFAULT_VERSION, getAiProviderPreset, getAiProviderPresetId, normalizeAiConfig, normalizeEditorSettings, useSettingsStore } from "../../apps/desktop/src/stores/settingsStore.ts"; import { DEFAULT_SHORTCUT_SETTINGS, tabNavigationHistoryDefaultShortcut, type ShortcutSettings } from "../../apps/desktop/src/lib/editor/shortcutRegistry.ts"; const saveEditorSettingsMock = vi.hoisted(() => vi.fn()); @@ -874,6 +874,22 @@ test("AI provider presets include common hosted and local providers", () => { assert.ok(Object.keys(AI_PROVIDER_PRESETS).indexOf("codex-cli") < Object.keys(AI_PROVIDER_PRESETS).indexOf("pi-agent-cli")); }); +test("AI partner presets reuse a supported runtime adapter", () => { + const jalapeno = AI_PROVIDER_PARTNER_PRESETS.find((preset) => preset.id === "jalapeno-cloud"); + + assert.ok(jalapeno); + assert.equal(jalapeno.provider, "openai-compatible"); + assert.equal(jalapeno.endpoint, "https://api.jalapeno-cloud.ai/v1"); + assert.equal(jalapeno.model, "GLM-5.2"); + assert.deepEqual(jalapeno.models, [{ name: "GLM-5.2" }, { name: "DeepSeek-V4-Pro" }, { name: "MiniMax-M3" }]); + assert.equal(jalapeno.requiresApiKey, true); + assert.equal(jalapeno.websiteUrl, "https://www.jalapeno-cloud.ai/dbx"); + assert.equal(jalapeno.apiKeyUrl, "https://www.jalapeno-cloud.ai/dbx"); + assert.equal(getAiProviderPreset("openai-compatible", "https://api.jalapeno-cloud.ai/v1/").label, "Jalapeno Cloud"); + assert.equal(getAiProviderPresetId("openai-compatible", "https://api.jalapeno-cloud.ai/v1/"), "jalapeno-cloud"); + assert.equal(getAiProviderPreset("openai-compatible", "https://api.example.com/v1").label, "OpenAI Compatible"); +}); + test("API AI provider settings expose and persist a default model ID", () => { const source = readFileSync("apps/desktop/src/components/editor/EditorSettingsDialog.vue", "utf8"); const modelControl = source.indexOf('