diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index c61ebf6050..12afc93a11 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -428,6 +428,26 @@ + +
+
+
+ +

+ {{ t('admin.accounts.grokClientToolCache.hint') }} +

+
+ +
+
+
([]) const DEFAULT_POOL_MODE_RETRY_COUNT = 3 const MAX_POOL_MODE_RETRY_COUNT = 10 const DEFAULT_POOL_MODE_RETRY_STATUS_CODES = [401, 403, 429] +const GROK_CLIENT_TOOL_CACHE_EXTRA_KEY = 'grok_client_tool_cache_enabled' const poolModeEnabled = ref(false) const poolModeRetryCount = ref(DEFAULT_POOL_MODE_RETRY_COUNT) const poolModeRetryStatusCodesInput = ref('') @@ -2747,6 +2768,7 @@ const headerOverrideCapable = computed( // Grok OAuth 自定义上游地址(仅转发端点;OAuth 授权/令牌刷新不受影响) const grokOAuthCustomBaseUrlEnabled = ref(false) const grokOAuthBaseUrl = ref('') +const grokClientToolCacheEnabled = ref(false) const interceptWarmupRequests = ref(false) const autoPauseOnExpired = ref(false) @@ -3402,6 +3424,10 @@ const syncFormFromAccount = (newAccount: Account | null) => { // Load Grok OAuth custom upstream URL state(存储的官方地址视同未定制) grokOAuthCustomBaseUrlEnabled.value = false grokOAuthBaseUrl.value = '' + grokClientToolCacheEnabled.value = + newAccount.platform === 'grok' && + newAccount.type === 'oauth' && + newAccount.extra?.[GROK_CLIENT_TOOL_CACHE_EXTRA_KEY] === true if (newAccount.platform === 'grok' && newAccount.type === 'oauth' && newAccount.credentials) { const grokCreds = newAccount.credentials as Record if (isCustomGrokBaseUrl(grokCreds.base_url)) { @@ -4288,6 +4314,16 @@ const handleSubmit = async () => { applyHeaderOverride(newCredentials, headerOverrideEnabled.value, headerOverrideRows.value, 'edit') updatePayload.credentials = newCredentials + + const newExtra: Record = { + ...((props.account.extra as Record) || {}) + } + if (grokClientToolCacheEnabled.value) { + newExtra[GROK_CLIENT_TOOL_CACHE_EXTRA_KEY] = true + } else { + delete newExtra[GROK_CLIENT_TOOL_CACHE_EXTRA_KEY] + } + updatePayload.extra = newExtra } // OpenAI: 手动覆盖订阅档位 plan_type(Plus/Pro/Free)。仅 OAuth 非影子账号: diff --git a/frontend/src/components/account/__tests__/EditAccountModal.grokUpstream.spec.ts b/frontend/src/components/account/__tests__/EditAccountModal.grokUpstream.spec.ts index 310a370889..068d7ac1a4 100644 --- a/frontend/src/components/account/__tests__/EditAccountModal.grokUpstream.spec.ts +++ b/frontend/src/components/account/__tests__/EditAccountModal.grokUpstream.spec.ts @@ -67,7 +67,10 @@ const BaseDialogStub = defineComponent({ template: '
' }) -function buildGrokOAuthAccount(credentials: Record = {}) { +function buildGrokOAuthAccount( + credentials: Record = {}, + extra: Record = {} +) { return { id: 5, name: 'Grok OAuth', @@ -80,7 +83,7 @@ function buildGrokOAuthAccount(credentials: Record = {}) { ...credentials }, credentials_status: { has_access_token: true, has_refresh_token: true }, - extra: {}, + extra, proxy_id: null, concurrency: 1, priority: 1, @@ -225,4 +228,61 @@ describe('EditAccountModal Grok OAuth upstream config', () => { 'x-xai-token-auth': 'xai-grok-cli' }) }) + + it('shows the client-tool cache switch only for Grok OAuth accounts', () => { + const grokOAuthWrapper = mountModal(buildGrokOAuthAccount()) + expect(grokOAuthWrapper.find('[data-testid="grok-client-tool-cache-toggle"]').exists()).toBe(true) + + const grokAPIKeyWrapper = mountModal({ + ...buildGrokOAuthAccount(), + type: 'apikey', + credentials: { api_key: 'xai-test', base_url: 'https://api.x.ai/v1' } + }) + expect(grokAPIKeyWrapper.find('[data-testid="grok-client-tool-cache-toggle"]').exists()).toBe(false) + + const openAIOAuthWrapper = mountModal({ + ...buildGrokOAuthAccount(), + platform: 'openai' + }) + expect(openAIOAuthWrapper.find('[data-testid="grok-client-tool-cache-toggle"]').exists()).toBe(false) + }) + + it('loads and disables client-tool caching while preserving unrelated extra fields', async () => { + const account = buildGrokOAuthAccount({}, { + grok_client_tool_cache_enabled: true, + custom_setting: 'keep-me' + }) + updateAccountMock.mockResolvedValue(account) + + const wrapper = mountModal(account) + const toggle = wrapper.get('[data-testid="grok-client-tool-cache-toggle"]') + expect(toggle.attributes('aria-checked')).toBe('true') + + await toggle.trigger('click') + await wrapper.get('form#edit-account-form').trigger('submit.prevent') + await vi.waitFor(() => expect(updateAccountMock).toHaveBeenCalledTimes(1)) + + const payload = updateAccountMock.mock.calls[0]?.[1] + expect(payload?.extra?.custom_setting).toBe('keep-me') + expect(payload?.extra).not.toHaveProperty('grok_client_tool_cache_enabled') + }) + + it('enables client-tool caching while preserving unrelated extra fields', async () => { + const account = buildGrokOAuthAccount({}, { custom_setting: 'keep-me' }) + updateAccountMock.mockResolvedValue(account) + + const wrapper = mountModal(account) + const toggle = wrapper.get('[data-testid="grok-client-tool-cache-toggle"]') + expect(toggle.attributes('aria-checked')).toBe('false') + + await toggle.trigger('click') + await wrapper.get('form#edit-account-form').trigger('submit.prevent') + await vi.waitFor(() => expect(updateAccountMock).toHaveBeenCalledTimes(1)) + + const payload = updateAccountMock.mock.calls[0]?.[1] + expect(payload?.extra).toMatchObject({ + grok_client_tool_cache_enabled: true, + custom_setting: 'keep-me' + }) + }) }) diff --git a/frontend/src/i18n/locales/en/admin/accounts.ts b/frontend/src/i18n/locales/en/admin/accounts.ts index 206c7c5172..772161f6c8 100644 --- a/frontend/src/i18n/locales/en/admin/accounts.ts +++ b/frontend/src/i18n/locales/en/admin/accounts.ts @@ -641,6 +641,10 @@ export default { official: 'Official API' } }, + grokClientToolCache: { + title: 'Client Tool Cache (May Change Automatic Tool Selection)', + hint: 'For detected Grok Free OAuth accounts, enable upstream prompt caching for client function tools such as Codex and Trae. Enable only after confirming the automatic tool-selection behavior is acceptable.' + }, autoPauseOnExpired: 'Auto Pause On Expired', autoPauseOnExpiredDesc: 'When enabled, the account will auto pause scheduling after it expires', autoPause5hThreshold: '5h Usage Threshold (%)', diff --git a/frontend/src/i18n/locales/zh/admin/accounts.ts b/frontend/src/i18n/locales/zh/admin/accounts.ts index 48da50175d..9cfa1ee17c 100644 --- a/frontend/src/i18n/locales/zh/admin/accounts.ts +++ b/frontend/src/i18n/locales/zh/admin/accounts.ts @@ -734,6 +734,10 @@ export default { official: '官方 API' } }, + grokClientToolCache: { + title: '客户端工具缓存(可能改变自动工具选择)', + hint: '仅对已识别为 Free 的 Grok OAuth 账号生效,为 Codex、Trae 等客户端函数工具请求启用上游提示缓存;请确认自动工具选择行为可接受后开启。' + }, autoPauseOnExpired: '过期自动暂停调度', autoPauseOnExpiredDesc: '启用后,账号过期将自动暂停调度', autoPause5hThreshold: '5h 用量阈值(%)',