mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 05:59:24 +08:00
feat(ai): add Jalapeno Cloud partner preset
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -55,7 +55,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import CodeSnapshotDialog from "@/components/codeSnapshot/CodeSnapshotDialog.vue";
|
||||
import type { CodeSnapshotSource } from "@/lib/codeSnapshot/codeSnapshot";
|
||||
import { useSettingsStore, AI_PROVIDER_PRESETS, aiProviderLabel, normalizeAiConfig } from "@/stores/settingsStore";
|
||||
import { useSettingsStore, AI_PROVIDER_PRESETS, aiProviderLabel, getAiProviderPreset, normalizeAiConfig } from "@/stores/settingsStore";
|
||||
import AiProviderLogo from "@/components/icons/AiProviderLogo.vue";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
@@ -715,7 +715,7 @@ const activeFullConfig = computed(() => {
|
||||
});
|
||||
|
||||
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) {
|
||||
<Popover v-model:open="providerSelectorOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<button type="button" class="min-w-0 flex shrink items-center gap-1.5 max-w-[220px] rounded-[6px] border px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground">
|
||||
<AiProviderLogo :provider="activeFullConfig?.provider ?? 'claude'" :label="aiProviderLabel(activeFullConfig?.provider ?? 'claude', t)" :icon-slug="AI_PROVIDER_PRESETS[activeFullConfig?.provider ?? 'claude']?.iconSlug" class="h-3 w-3 shrink-0" />
|
||||
<AiProviderLogo
|
||||
:provider="activeFullConfig?.provider ?? 'claude'"
|
||||
:label="aiConfigProviderLabel(activeFullConfig)"
|
||||
:icon-slug="activeFullConfig ? getAiProviderPreset(activeFullConfig.provider, activeFullConfig.endpoint).iconSlug : AI_PROVIDER_PRESETS.claude.iconSlug"
|
||||
:icon-path="activeFullConfig ? getAiProviderPreset(activeFullConfig.provider, activeFullConfig.endpoint).iconPath : undefined"
|
||||
class="h-3 w-3 shrink-0"
|
||||
/>
|
||||
<span class="min-w-0 truncate">{{ activeFullConfig?.model || t("ai.selectModel") }}</span>
|
||||
<svg class="h-3 w-3 shrink-0 opacity-60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</button>
|
||||
@@ -5015,7 +5027,13 @@ async function openExternalUrl(url: string) {
|
||||
<div v-for="(config, configIndex) in configuredProviders" :key="config.id" class="relative">
|
||||
<button type="button" class="sticky top-0 z-10 flex w-full items-center gap-2 rounded-sm bg-popover px-2 py-1.5 text-left text-xs text-foreground hover:bg-muted" :aria-expanded="!isModelConfigCollapsed(config.id)" @click="toggleModelConfig(config.id)">
|
||||
<ChevronRight class="h-3.5 w-3.5 shrink-0 transition-transform" :class="{ 'rotate-90': !isModelConfigCollapsed(config.id) }" />
|
||||
<AiProviderLogo :provider="config.provider" :label="AI_PROVIDER_PRESETS[config.provider]?.label ?? config.provider" :icon-slug="AI_PROVIDER_PRESETS[config.provider]?.iconSlug" class="h-3.5 w-3.5 shrink-0" />
|
||||
<AiProviderLogo
|
||||
:provider="config.provider"
|
||||
:label="getAiProviderPreset(config.provider, config.endpoint).label"
|
||||
:icon-slug="getAiProviderPreset(config.provider, config.endpoint).iconSlug"
|
||||
:icon-path="getAiProviderPreset(config.provider, config.endpoint).iconPath"
|
||||
class="h-3.5 w-3.5 shrink-0"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 truncate font-medium">{{ config.name }}</span>
|
||||
<Loader2 v-if="getModelCatalog(config.id).status === 'loading'" class="h-3 w-3 shrink-0 animate-spin text-muted-foreground" />
|
||||
<span v-if="config.isDefault" class="ml-auto text-[10px] text-muted-foreground">{{ t("ai.default") }}</span>
|
||||
|
||||
@@ -45,7 +45,7 @@ import { Input } from "@/components/ui/input";
|
||||
import PasswordInput from "@/components/ui/PasswordInput.vue";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
@@ -53,7 +53,7 @@ import { HelpTooltip, Tooltip, TooltipContent, TooltipTrigger, TooltipProvider }
|
||||
import {
|
||||
useSettingsStore,
|
||||
AI_PROVIDER_PRESETS,
|
||||
aiProviderLabel,
|
||||
AI_PROVIDER_PARTNER_PRESETS,
|
||||
EDITOR_THEMES,
|
||||
DEFAULT_EDITOR_SETTINGS,
|
||||
DEFAULT_DESKTOP_SETTINGS,
|
||||
@@ -63,6 +63,10 @@ import {
|
||||
normalizeDuckDbWorkerMaxProcesses,
|
||||
normalizeAiEnv,
|
||||
normalizeAiHeaders,
|
||||
getAiProviderPreset,
|
||||
getAiProviderPresetId,
|
||||
getAiProviderPresetOption,
|
||||
isAiPartnerProviderPreset,
|
||||
type AiProvider,
|
||||
type AiApiStyle,
|
||||
type AiAuthMethod,
|
||||
@@ -3381,10 +3385,11 @@ const aiDeleteConfigId = ref<string | null>(null);
|
||||
const CLI_AI_PROVIDERS = new Set<AiProvider>(["claude-code-cli", "codex-cli", "opencode-cli", "pi-agent-cli", "cursor-cli", "grok-cli", "codebuddy-cli", "qoder-cli"]);
|
||||
const OPENCODE_CONTROL_ENV = new Set(["OPENCODE_CONFIG", "OPENCODE_CONFIG_CONTENT", "OPENCODE_CONFIG_DIR", "OPENCODE_DB", "OPENCODE_PERMISSION", "OPENCODE_DISABLE_PROJECT_CONFIG"]);
|
||||
const CURSOR_CONTROL_ENV = new Set(["CURSOR_CONFIG_DIR", "CURSOR_DATA_DIR"]);
|
||||
const aiProviderOptions = computed(() => Object.values(AI_PROVIDER_PRESETS).filter((provider) => !isWeb || !CLI_AI_PROVIDERS.has(provider.provider)));
|
||||
const selectedAiProviderPreset = computed(() => AI_PROVIDER_PRESETS[aiEditProvider.value]);
|
||||
const builtinAiProviderOptions = computed(() => Object.values(AI_PROVIDER_PRESETS).filter((provider) => !isWeb || !CLI_AI_PROVIDERS.has(provider.provider)));
|
||||
const partnerAiProviderOptions = computed(() => AI_PROVIDER_PARTNER_PRESETS.filter((provider) => !isWeb || !CLI_AI_PROVIDERS.has(provider.provider)));
|
||||
|
||||
const aiEditProvider = ref<AiProvider>("claude");
|
||||
const aiEditProviderPresetId = ref("claude");
|
||||
const aiEditApiKey = ref("");
|
||||
const aiEditAuthMethod = ref<AiAuthMethod>("api-key");
|
||||
const aiEditEndpoint = ref("");
|
||||
@@ -3415,6 +3420,8 @@ const aiEditQoderCliPath = ref("");
|
||||
const aiEditQoderCliEnvRows = ref<AiEnvRow[]>([]);
|
||||
|
||||
const aiAnthropicMessagesMode = computed(() => aiEditApiStyle.value === "anthropic-messages");
|
||||
const selectedAiProviderPreset = computed(() => getAiProviderPresetOption(aiEditProviderPresetId.value));
|
||||
const selectedAiPartnerPreset = computed(() => (isAiPartnerProviderPreset(selectedAiProviderPreset.value) ? selectedAiProviderPreset.value : null));
|
||||
|
||||
const aiTesting = ref(false);
|
||||
const aiTestResult = ref<"" | "success" | "error">("");
|
||||
@@ -3515,7 +3522,7 @@ const aiEditCliEnvRows = computed(() => {
|
||||
watch(aiIsCliProvider, (isCliProvider) => {
|
||||
if (isCliProvider) void ensureCliMcpStatus();
|
||||
});
|
||||
const aiRequiresApiKey = computed(() => AI_PROVIDER_PRESETS[aiEditProvider.value].requiresApiKey);
|
||||
const aiRequiresApiKey = computed(() => selectedAiProviderPreset.value.requiresApiKey);
|
||||
const aiUsesConfigurableAnthropicAuth = computed(() => aiEditProvider.value === "claude" || aiEditProvider.value === "anthropic-compatible" || (aiEditProvider.value === "custom" && aiAnthropicMessagesMode.value));
|
||||
const aiUsesCompatibleAnthropicApi = computed(() => aiEditProvider.value === "anthropic-compatible" || (aiEditProvider.value === "custom" && aiAnthropicMessagesMode.value));
|
||||
const aiSupportsAuthMethod = computed(() => aiUsesConfigurableAnthropicAuth.value);
|
||||
@@ -3707,18 +3714,20 @@ function syncAiEditState() {
|
||||
aiTestErrorCopied.value = false;
|
||||
}
|
||||
|
||||
function aiSelectProvider(provider: AiProvider) {
|
||||
function aiSelectProvider(presetId: string) {
|
||||
const preset = getAiProviderPresetOption(presetId);
|
||||
const provider = preset.provider;
|
||||
if (isWeb && CLI_AI_PROVIDERS.has(provider)) return;
|
||||
if (provider === aiEditProvider.value) return;
|
||||
if (presetId === aiEditProviderPresetId.value) return;
|
||||
|
||||
// Apply new provider's preset defaults to edit state
|
||||
const preset = AI_PROVIDER_PRESETS[provider];
|
||||
aiEditProviderPresetId.value = presetId;
|
||||
aiEditProvider.value = provider;
|
||||
aiEditApiKey.value = "";
|
||||
aiEditAuthMethod.value = preset.authMethod;
|
||||
aiEditEndpoint.value = preset.endpoint;
|
||||
aiEditModel.value = "";
|
||||
aiEditLegacyModels.value = [];
|
||||
aiEditModel.value = preset.group === "partner" ? preset.model : "";
|
||||
aiEditLegacyModels.value = preset.group === "partner" ? [...(preset.models ?? [])] : [];
|
||||
aiEditApiStyle.value = preset.apiStyle;
|
||||
aiEditCustomHeaderRows.value = [];
|
||||
aiEditEnableThinking.value = true;
|
||||
@@ -3747,6 +3756,7 @@ function aiEnterEditMode(configId?: string) {
|
||||
if (config) {
|
||||
aiEditConfigName.value = config.name;
|
||||
aiEditProvider.value = config.provider;
|
||||
aiEditProviderPresetId.value = getAiProviderPresetId(config.provider, config.endpoint);
|
||||
aiEditApiKey.value = config.apiKey;
|
||||
aiEditAuthMethod.value = config.authMethod;
|
||||
aiEditEndpoint.value = config.endpoint;
|
||||
@@ -3782,6 +3792,7 @@ function aiEnterEditMode(configId?: string) {
|
||||
} else {
|
||||
aiEditConfigName.value = "";
|
||||
aiEditProvider.value = "claude";
|
||||
aiEditProviderPresetId.value = "claude";
|
||||
aiEditApiKey.value = "";
|
||||
aiEditAuthMethod.value = AI_PROVIDER_PRESETS["claude"].authMethod;
|
||||
aiEditEndpoint.value = AI_PROVIDER_PRESETS["claude"].endpoint;
|
||||
@@ -3823,6 +3834,7 @@ async function applyPendingAiConfigDeepLinkDraft() {
|
||||
aiEnterEditMode();
|
||||
aiEditConfigName.value = draft.name;
|
||||
aiEditProvider.value = draft.provider;
|
||||
aiEditProviderPresetId.value = getAiProviderPresetId(draft.provider, draft.endpoint);
|
||||
aiEditApiKey.value = "";
|
||||
aiEditAuthMethod.value = draft.authMethod;
|
||||
aiEditEndpoint.value = draft.endpoint;
|
||||
@@ -7057,7 +7069,7 @@ onUnmounted(() => {
|
||||
<div v-else class="space-y-2">
|
||||
<div v-for="config in displayedAiConfigs" :key="config.id" class="flex items-center justify-between rounded-md border p-3" :class="{ 'border-primary bg-primary/5': config.isDefault }">
|
||||
<div class="flex items-center gap-3">
|
||||
<AiProviderLogo :provider="config.provider" :label="config.provider" :icon-slug="AI_PROVIDER_PRESETS[config.provider].iconSlug" />
|
||||
<AiProviderLogo :provider="config.provider" :label="getAiProviderPreset(config.provider, config.endpoint).label" :icon-slug="getAiProviderPreset(config.provider, config.endpoint).iconSlug" :icon-path="getAiProviderPreset(config.provider, config.endpoint).iconPath" />
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium">{{ config.name }}</span>
|
||||
@@ -7066,7 +7078,7 @@ onUnmounted(() => {
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ aiProviderLabel(config.provider, t) }}
|
||||
{{ getAiProviderPreset(config.provider, config.endpoint).label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7309,28 +7321,51 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Provider Selection -->
|
||||
<div class="grid grid-cols-3 items-center gap-3">
|
||||
<div class="grid grid-cols-3 items-start gap-3">
|
||||
<Label class="text-right text-xs">{{ t("ai.provider") }}</Label>
|
||||
<Select :model-value="aiEditProvider" @update:model-value="(v: any) => aiSelectProvider(v)">
|
||||
<SelectTrigger class="col-span-2" inputClass="h-8 text-xs">
|
||||
<SelectValue>
|
||||
<span class="flex items-center gap-2">
|
||||
<AiProviderLogo :provider="selectedAiProviderPreset.provider" :label="aiProviderLabel(selectedAiProviderPreset.provider, t)" :icon-slug="selectedAiProviderPreset.iconSlug" />
|
||||
<span>{{ aiProviderLabel(selectedAiProviderPreset.provider, t) }}</span>
|
||||
</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="provider in aiProviderOptions" :key="provider.provider" :value="provider.provider">
|
||||
<span class="flex w-full items-center justify-between gap-4">
|
||||
<div class="col-span-2 space-y-2">
|
||||
<Select :model-value="aiEditProviderPresetId" @update:model-value="(v: any) => aiSelectProvider(String(v))">
|
||||
<SelectTrigger inputClass="h-8 text-xs">
|
||||
<SelectValue>
|
||||
<span class="flex items-center gap-2">
|
||||
<AiProviderLogo :provider="provider.provider" :label="aiProviderLabel(provider.provider, t)" :icon-slug="provider.iconSlug" />
|
||||
<span>{{ aiProviderLabel(provider.provider, t) }}</span>
|
||||
<AiProviderLogo :provider="selectedAiProviderPreset.provider" :label="selectedAiProviderPreset.label" :icon-slug="selectedAiProviderPreset.iconSlug" :icon-path="selectedAiProviderPreset.iconPath" />
|
||||
<span>{{ selectedAiProviderPreset.label }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent class="w-[32rem] max-w-[calc(100vw-2rem)]">
|
||||
<div class="grid grid-cols-1 gap-1 sm:grid-cols-2">
|
||||
<SelectGroup class="min-w-0">
|
||||
<SelectLabel>{{ t("ai.builtinProviders") }}</SelectLabel>
|
||||
<SelectItem v-for="provider in builtinAiProviderOptions" :key="provider.provider" :value="provider.provider">
|
||||
<span class="flex w-full min-w-0 items-center gap-2">
|
||||
<AiProviderLogo :provider="provider.provider" :label="provider.label" :icon-slug="provider.iconSlug" :icon-path="provider.iconPath" />
|
||||
<span class="truncate">{{ provider.label }}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectGroup v-if="partnerAiProviderOptions.length" class="min-w-0 border-border/60 sm:border-l">
|
||||
<SelectLabel>{{ t("ai.partnerProviders") }}</SelectLabel>
|
||||
<SelectItem v-for="provider in partnerAiProviderOptions" :key="provider.id" :value="provider.id">
|
||||
<span class="flex w-full min-w-0 items-center gap-2">
|
||||
<AiProviderLogo :provider="provider.provider" :label="provider.label" :icon-slug="provider.iconSlug" :icon-path="provider.iconPath" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ provider.label }}</span>
|
||||
<Badge variant="outline" class="h-5 shrink-0 px-1.5 text-[10px] font-normal">{{ t("ai.jalapenoSponsored") }}</Badge>
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div v-if="selectedAiPartnerPreset" class="flex items-center gap-3 rounded-md border border-primary/20 bg-primary/5 px-3 py-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[11px] leading-4 text-muted-foreground">{{ t(selectedAiPartnerPreset.descriptionKey) }}</p>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" size="icon" class="h-7 w-7 shrink-0" :title="t('ai.visitPartner')" :aria-label="t('ai.visitPartner')" @click="openExternalUrl(selectedAiPartnerPreset.websiteUrl)">
|
||||
<ExternalLink class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CLI MCP Status -->
|
||||
@@ -7388,7 +7423,13 @@ onUnmounted(() => {
|
||||
<!-- API Key -->
|
||||
<div v-if="!aiIsCliProvider" class="grid grid-cols-3 items-center gap-3">
|
||||
<Label class="text-right text-xs">{{ aiCredentialLabel }}</Label>
|
||||
<PasswordInput v-model="aiEditApiKey" autocomplete="off" class="col-span-2" inputClass="h-8 text-xs" :placeholder="aiCredentialPlaceholder" />
|
||||
<div class="col-span-2 flex min-w-0 items-center gap-2">
|
||||
<PasswordInput v-model="aiEditApiKey" autocomplete="off" class="min-w-0 flex-1" inputClass="h-8 text-xs" :placeholder="aiCredentialPlaceholder" />
|
||||
<Button v-if="selectedAiPartnerPreset" type="button" variant="outline" size="sm" class="h-8 shrink-0 gap-1.5 px-3 text-xs" :title="t('ai.getApiKey')" :aria-label="t('ai.getApiKey')" @click="openExternalUrl(selectedAiPartnerPreset.apiKeyUrl)">
|
||||
{{ t("ai.getApiKey") }}
|
||||
<ExternalLink class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Endpoint -->
|
||||
|
||||
@@ -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`) : "";
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "プロバイダーを切替",
|
||||
|
||||
@@ -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: "공급자 전환",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "切换服务商",
|
||||
|
||||
@@ -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: "切換服務商",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -170,7 +170,17 @@ export function normalizeDuckDbWorkerMaxProcesses(value: unknown): number {
|
||||
export interface AiProviderPreset extends Omit<AiConfig, "apiKey"> {
|
||||
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<AiProvider, AiProviderPreset> = {
|
||||
@@ -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<AiProvider, Omit<AiConfig, "apiKey">> = 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<AiProvider, Omit<AiConfig, "apiKey">>;
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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<HeaderMap, String> {
|
||||
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,
|
||||
|
||||
+17
-1
@@ -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('<Input v-model="aiEditModel"');
|
||||
|
||||
Reference in New Issue
Block a user