diff --git a/backend/cmd/server/VERSION b/backend/cmd/server/VERSION index e534f2aa..061bed0b 100644 --- a/backend/cmd/server/VERSION +++ b/backend/cmd/server/VERSION @@ -1 +1 @@ -0.1.108.140 +0.1.110.11 diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index d5d8ff89..e0dbce61 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -2325,9 +2325,9 @@ - +
@@ -2998,6 +2998,12 @@ const openaiAPIKeyResponsesWebSocketV2Mode = ref(OPENAI_WS_MODE_OF const codexCLIOnlyEnabled = ref(false) const anthropicPassthroughEnabled = ref(false) const webSearchEmulationEnabled = ref(false) +const webSearchGlobalEnabled = ref(false) + +// Load web search global state once +adminAPI.settings.getWebSearchEmulationConfig().then(cfg => { + webSearchGlobalEnabled.value = cfg?.enabled === true && (cfg?.providers?.length ?? 0) > 0 +}).catch(() => { webSearchGlobalEnabled.value = false }) const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling const allowOverages = ref(false) // For antigravity accounts: enable AI Credits overages const antigravityAccountType = ref<'oauth' | 'upstream'>('oauth') // For antigravity: oauth or upstream diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 54dae5c2..086575e6 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -1149,9 +1149,9 @@
- +
@@ -1975,6 +1975,12 @@ const openaiAPIKeyResponsesWebSocketV2Mode = ref(OPENAI_WS_MODE_OF const codexCLIOnlyEnabled = ref(false) const anthropicPassthroughEnabled = ref(false) const webSearchEmulationEnabled = ref(false) +const webSearchGlobalEnabled = ref(false) + +// Load web search global state once +adminAPI.settings.getWebSearchEmulationConfig().then(cfg => { + webSearchGlobalEnabled.value = cfg?.enabled === true && (cfg?.providers?.length ?? 0) > 0 +}).catch(() => { webSearchGlobalEnabled.value = false }) const editQuotaLimit = ref(null) const editQuotaDailyLimit = ref(null) const editQuotaWeeklyLimit = ref(null) diff --git a/frontend/src/views/admin/ChannelsView.vue b/frontend/src/views/admin/ChannelsView.vue index a49e1694..639ace4a 100644 --- a/frontend/src/views/admin/ChannelsView.vue +++ b/frontend/src/views/admin/ChannelsView.vue @@ -306,6 +306,21 @@
+ +
+
+
+ +

+ {{ t('admin.channels.form.webSearchEmulationHint') }} +

+
+ +
+
+
@@ -560,6 +575,7 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue' import { useI18n } from 'vue-i18n' import { useAppStore } from '@/stores/app' +import { extractApiErrorMessage } from '@/utils/apiError' import { adminAPI } from '@/api/admin' import type { Channel, ChannelModelPricing, CreateChannelRequest, UpdateChannelRequest, AccountStatsPricingRule } from '@/api/admin/channels' import type { PricingFormEntry } from '@/components/admin/channel/types' @@ -583,6 +599,18 @@ import { getPersistedPageSize } from '@/composables/usePersistedPageSize' const { t } = useI18n() const appStore = useAppStore() +// Web Search global enabled state (loaded once on mount) +const webSearchGlobalEnabled = ref(false) +async function loadWebSearchGlobalState() { + try { + const cfg = await adminAPI.settings.getWebSearchEmulationConfig() + webSearchGlobalEnabled.value = cfg?.enabled === true && (cfg?.providers?.length ?? 0) > 0 + } catch (err: unknown) { + console.warn('Failed to load web search global state:', err) + webSearchGlobalEnabled.value = false + } +} + // ── Platform Section type ── interface PlatformSection { platform: GroupPlatform @@ -591,6 +619,7 @@ interface PlatformSection { group_ids: number[] model_mapping: Record model_pricing: PricingFormEntry[] + web_search_emulation: boolean } // ── Table columns ── @@ -709,7 +738,8 @@ function addPlatformSection(platform: GroupPlatform) { collapsed: false, group_ids: [], model_mapping: {}, - model_pricing: [] + model_pricing: [], + web_search_emulation: false, }) } @@ -901,10 +931,14 @@ function accountStatsRulesToAPI(): AccountStatsPricingRule[] { } // ── Form ↔ API conversion ── -function formToAPI(): { group_ids: number[], model_pricing: ChannelModelPricing[], model_mapping: Record> } { +function formToAPI(): { group_ids: number[], model_pricing: ChannelModelPricing[], model_mapping: Record>, features_config: Record } { const group_ids: number[] = [] const model_pricing: ChannelModelPricing[] = [] const model_mapping: Record> = {} + // Preserve existing features_config fields not managed by the form + const featuresConfig: Record = editingChannel.value?.features_config + ? { ...editingChannel.value.features_config } + : {} for (const section of form.platforms) { if (!section.enabled) continue @@ -933,7 +967,19 @@ function formToAPI(): { group_ids: number[], model_pricing: ChannelModelPricing[ } } - return { group_ids, model_pricing, model_mapping } + // Collect web_search_emulation (only anthropic platform supports it) + const wsEmulation: Record = {} + for (const section of form.platforms) { + if (!section.enabled) continue + if (section.web_search_emulation && section.platform === 'anthropic') { + wsEmulation[section.platform] = true + } + } + if (Object.keys(wsEmulation).length > 0) { + featuresConfig.web_search_emulation = wsEmulation + } + + return { group_ids, model_pricing, model_mapping, features_config: featuresConfig } } function apiToForm(channel: Channel): PlatformSection[] { @@ -977,13 +1023,19 @@ function apiToForm(channel: Channel): PlatformSection[] { intervals: apiIntervalsToForm(p.intervals || []) } as PricingFormEntry)) + // Read web_search_emulation from features_config + const fc = channel.features_config + const wsEmulation = fc?.web_search_emulation as Record | undefined + const webSearchEnabled = wsEmulation?.[platform] === true + sections.push({ platform, enabled: true, collapsed: false, group_ids: groupIds, model_mapping: { ...mapping }, - model_pricing: pricing + model_pricing: pricing, + web_search_emulation: webSearchEnabled, }) } @@ -1008,10 +1060,10 @@ async function loadChannels() { if (ctrl.signal.aborted || abortController !== ctrl) return channels.value = response.items || [] pagination.total = response.total - } catch (error: any) { - if (error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return - appStore.showError(t('admin.channels.loadError', 'Failed to load channels')) - console.error('Error loading channels:', error) + } catch (error: unknown) { + const e = error as { name?: string; code?: string } + if (e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return + appStore.showError(extractApiErrorMessage(error, t('admin.channels.loadError', 'Failed to load channels'))) } finally { if (abortController === ctrl) { loading.value = false @@ -1210,7 +1262,7 @@ async function handleSubmit() { } } - const { group_ids, model_pricing, model_mapping } = formToAPI() + const { group_ids, model_pricing, model_mapping, features_config } = formToAPI() submitting.value = true try { @@ -1224,6 +1276,7 @@ async function handleSubmit() { model_mapping: Object.keys(model_mapping).length > 0 ? model_mapping : {}, billing_model_source: form.billing_model_source, restrict_models: form.restrict_models, + features_config, apply_pricing_to_account_stats: form.apply_pricing_to_account_stats, account_stats_pricing_rules: accountStatsRulesToAPI() } @@ -1238,6 +1291,7 @@ async function handleSubmit() { model_mapping: Object.keys(model_mapping).length > 0 ? model_mapping : {}, billing_model_source: form.billing_model_source, restrict_models: form.restrict_models, + features_config, apply_pricing_to_account_stats: form.apply_pricing_to_account_stats, account_stats_pricing_rules: accountStatsRulesToAPI() } @@ -1246,12 +1300,10 @@ async function handleSubmit() { } closeDialog() loadChannels() - } catch (error: any) { - const msg = error.response?.data?.detail || (editingChannel.value + } catch (error: unknown) { + appStore.showError(extractApiErrorMessage(error, editingChannel.value ? t('admin.channels.updateError', 'Failed to update channel') - : t('admin.channels.createError', 'Failed to create channel')) - appStore.showError(msg) - console.error('Error saving channel:', error) + : t('admin.channels.createError', 'Failed to create channel'))) } finally { submitting.value = false } @@ -1289,9 +1341,8 @@ async function confirmDelete() { showDeleteDialog.value = false deletingChannel.value = null loadChannels() - } catch (error: any) { - appStore.showError(error.response?.data?.detail || t('admin.channels.deleteError', 'Failed to delete channel')) - console.error('Error deleting channel:', error) + } catch (error: unknown) { + appStore.showError(extractApiErrorMessage(error, t('admin.channels.deleteError', 'Failed to delete channel'))) } } @@ -1299,6 +1350,7 @@ async function confirmDelete() { onMounted(() => { loadChannels() loadGroups() + loadWebSearchGlobalState() }) onUnmounted(() => { diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index faab49fe..b5181ccc 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -1789,40 +1789,42 @@
- +
-
+
- - +
+ + +
@@ -1858,44 +1860,19 @@ {{ provider.quota_used ?? 0 }} / {{ provider.quota_limit }}
- -
- - -
- - -
-
- - -
- -
-

- {{ t('admin.settings.webSearchEmulation.testResultProvider') }}: {{ wsTestResult.provider }} -

-
- {{ t('admin.settings.webSearchEmulation.testNoResults') }} -
-
- {{ r.title }} -

{{ r.snippet && r.snippet.length > 120 ? r.snippet.slice(0, 120) + '...' : r.snippet }}

-
+ +
+
+ +
+
@@ -1903,6 +1880,50 @@
+ +
+
+

+ {{ t('admin.settings.webSearchEmulation.testResultTitle') }} +

+
+ + +
+ +
+

+ {{ t('admin.settings.webSearchEmulation.testResultProvider') }}: {{ wsTestResult.provider }} +

+
+ {{ t('admin.settings.webSearchEmulation.testNoResults') }} +
+
+ {{ r.title }} +

{{ r.snippet }}

+
+
+
+ +
+
+
+
@@ -3016,6 +3037,12 @@ const apiKeyVisible = reactive>({}) const wsTestQuery = ref('') const wsTestLoading = ref(false) const wsTestResult = ref(null) +const wsTestDialogOpen = ref(false) + +function openTestDialog() { + wsTestResult.value = null + wsTestDialogOpen.value = true +} function toggleProviderExpand(idx: number) { expandedProviders[idx] = !expandedProviders[idx]