mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 11:53:10 +08:00
feat(desktop): let presets switch API protocol without rewriting the endpoint
A preset used to lock its protocol, so a dual-protocol endpoint could not be pointed at the other format. Keep the preset URL and warn when the chosen protocol no longer matches it.
This commit is contained in:
@@ -1340,6 +1340,7 @@ Row 9, all 8 cells: continuing from straight down, turning left through lower-le
|
||||
'settings.providers.multiProtocolBadge': 'Multi-protocol',
|
||||
'settings.providers.apiFormatPerModelHint': 'This preset picks the protocol per model, so the choice is fixed by it',
|
||||
'settings.providers.proxyHint': 'Requests will be translated via the local proxy',
|
||||
'settings.providers.apiFormatOverrideHint': 'Protocol changed from the preset default — point the base URL at an endpoint that serves it',
|
||||
'settings.providers.authStrategy': 'Auth Variable',
|
||||
'settings.providers.authStrategyApiKey': 'API Key (ANTHROPIC_API_KEY)',
|
||||
'settings.providers.authStrategyApiKeyDesc': 'Direct Anthropic API access using x-api-key.',
|
||||
|
||||
@@ -1342,6 +1342,7 @@ export const jp: Record<TranslationKey, string> = {
|
||||
'settings.providers.multiProtocolBadge': 'マルチプロトコル',
|
||||
'settings.providers.apiFormatPerModelHint': 'このプリセットはモデルごとにプロトコルを選ぶため、この項目は固定です',
|
||||
'settings.providers.proxyHint': 'リクエストはローカルプロキシ経由で変換されます',
|
||||
'settings.providers.apiFormatOverrideHint': 'プロトコルを変更しました。プリセットの既定 URL が対応しているとは限らないため、このプロトコルに対応したエンドポイントを指定してください',
|
||||
'settings.providers.authStrategy': '認証変数',
|
||||
'settings.providers.authStrategyApiKey': 'API キー (ANTHROPIC_API_KEY)',
|
||||
'settings.providers.authStrategyApiKeyDesc': 'x-api-key を使用した Anthropic API への直接アクセス。',
|
||||
|
||||
@@ -1344,6 +1344,7 @@ export const kr: Record<TranslationKey, string> = {
|
||||
'settings.providers.multiProtocolBadge': '멀티 프로토콜',
|
||||
'settings.providers.apiFormatPerModelHint': '이 프리셋은 모델별로 프로토콜을 선택하므로 이 항목은 고정됩니다',
|
||||
'settings.providers.proxyHint': '요청은 로컬 프록시를 통해 변환됩니다',
|
||||
'settings.providers.apiFormatOverrideHint': '프로토콜을 변경했습니다. 프리셋 기본 URL이 해당 프로토콜을 지원하지 않을 수 있으니 호환되는 엔드포인트를 지정하세요',
|
||||
'settings.providers.authStrategy': '인증 변수',
|
||||
'settings.providers.authStrategyApiKey': 'API 키 (ANTHROPIC_API_KEY)',
|
||||
'settings.providers.authStrategyApiKeyDesc': 'x-api-key를 사용한 Anthropic API 직접 액세스.',
|
||||
|
||||
@@ -1341,6 +1341,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.providers.multiProtocolBadge': '多協定',
|
||||
'settings.providers.apiFormatPerModelHint': '此預設會依模型自動選擇協定,本項由預設決定',
|
||||
'settings.providers.proxyHint': '請求將透過本地代理轉換協議格式',
|
||||
'settings.providers.apiFormatOverrideHint': '已切換協定:請確認介面位址是該協定的相容端點,預設位址可能不適用',
|
||||
'settings.providers.authStrategy': '認證變數',
|
||||
'settings.providers.authStrategyApiKey': 'API Key (ANTHROPIC_API_KEY)',
|
||||
'settings.providers.authStrategyApiKeyDesc': '直連 Anthropic 官方 API,傳送 x-api-key。',
|
||||
|
||||
@@ -1340,6 +1340,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.providers.multiProtocolBadge': '多协议',
|
||||
'settings.providers.apiFormatPerModelHint': '该预设按模型自动选择协议,此项由预设决定',
|
||||
'settings.providers.proxyHint': '请求将通过本地代理转换协议格式',
|
||||
'settings.providers.apiFormatOverrideHint': '已切换协议:请确认接口地址是该协议的兼容端点,预设默认地址可能不适用',
|
||||
'settings.providers.authStrategy': '认证变量',
|
||||
'settings.providers.authStrategyApiKey': 'API Key (ANTHROPIC_API_KEY)',
|
||||
'settings.providers.authStrategyApiKeyDesc': '直连 Anthropic 官方 API,发送 x-api-key。',
|
||||
|
||||
@@ -80,6 +80,47 @@ describe('ApiSmart sponsor provider', () => {
|
||||
})))
|
||||
})
|
||||
|
||||
it('lets a preset switch protocol while the preset endpoint stays put', async () => {
|
||||
const create = vi.spyOn(providersApi, 'create').mockImplementation(async (input) => ({
|
||||
provider: { ...input, id: 'saved-aruhub', apiFormat: input.apiFormat ?? 'anthropic' },
|
||||
}))
|
||||
render(<ProviderSettings />)
|
||||
fireEvent.click(await screen.findByRole('button', { name: /Add Model/ }))
|
||||
const dialog = within(screen.getByRole('dialog'))
|
||||
fireEvent.click(dialog.getByRole('button', { name: 'AruHub' }))
|
||||
|
||||
// AruHub is an Anthropic-endpoint preset that also serves OpenAI, so the
|
||||
// protocol starts on the preset's own value and is the user's to change.
|
||||
const formatTrigger = dialog.getByRole('button', { name: /Anthropic Messages \(native\)/ })
|
||||
expect(dialog.queryByText(/point the base URL at an endpoint that serves it/)).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(formatTrigger)
|
||||
fireEvent.click(await screen.findByRole('option', { name: /OpenAI Chat Completions/ }))
|
||||
|
||||
expect(dialog.getByText(/point the base URL at an endpoint that serves it/)).toBeInTheDocument()
|
||||
// The address is the user's to replace, so switching must not rewrite it.
|
||||
expect(dialog.getByDisplayValue('https://direct.aruhub.com:8443')).toBeInTheDocument()
|
||||
|
||||
fireEvent.change(dialog.getAllByPlaceholderText('sk-...')[0]!, { target: { value: 'fake-aruhub-key' } })
|
||||
fireEvent.click(dialog.getByRole('button', { name: 'Add' }))
|
||||
await waitFor(() => expect(create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
presetId: 'aruhub',
|
||||
baseUrl: 'https://direct.aruhub.com:8443',
|
||||
apiFormat: 'openai_chat',
|
||||
apiKey: 'fake-aruhub-key',
|
||||
})))
|
||||
})
|
||||
|
||||
it('does not warn about the endpoint while a preset keeps its own protocol', async () => {
|
||||
render(<ProviderSettings />)
|
||||
fireEvent.click(await screen.findByRole('button', { name: /Add Model/ }))
|
||||
const dialog = within(screen.getByRole('dialog'))
|
||||
fireEvent.click(dialog.getByRole('button', { name: 'ApiSmart' }))
|
||||
|
||||
expect(dialog.getByRole('button', { name: /OpenAI Chat Completions \(proxy\)/ })).toBeInTheDocument()
|
||||
expect(dialog.queryByText(/point the base URL at an endpoint that serves it/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('prefills the sponsor connection, opens its landing page, and saves the selected models', async () => {
|
||||
const open = vi.spyOn(getDesktopHost().shell, 'open').mockResolvedValue()
|
||||
const create = vi.spyOn(providersApi, 'create').mockImplementation(async (input) => ({
|
||||
|
||||
@@ -1215,7 +1215,6 @@ function ProviderFormModal({ open, onClose, mode, provider, presets, browserMode
|
||||
setTestResult(null)
|
||||
}
|
||||
|
||||
const isCustom = selectedPreset.id === 'custom'
|
||||
const requiresApiKey = selectedPreset.needsApiKey !== false
|
||||
const autoCompactWindowErrorKey = getAutoCompactWindowErrorKey(autoCompactWindow)
|
||||
const modelContextWindowErrorSlots = MODEL_SLOTS.filter((slot) => getModelContextWindowErrorKey(modelContextInputs[slot]))
|
||||
@@ -1878,8 +1877,9 @@ function ProviderFormModal({ open, onClose, mode, provider, presets, browserMode
|
||||
|
||||
<Input label={t('settings.providers.notes')} value={notes} onChange={(e) => setNotes(e.target.value)} placeholder={t('settings.providers.notesPlaceholder')} />
|
||||
|
||||
{/* API Format */}
|
||||
{(isCustom || mode === 'edit') && !presetDrivesApiFormat ? (
|
||||
{/* API Format — a preset only owns this field when it routes per model;
|
||||
every other preset starts on its own format but stays switchable. */}
|
||||
{!presetDrivesApiFormat ? (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-primary)] mb-1 block">{t('settings.providers.apiFormat')}</label>
|
||||
<Dropdown<ApiFormat>
|
||||
@@ -1898,18 +1898,21 @@ function ProviderFormModal({ open, onClose, mode, provider, presets, browserMode
|
||||
{apiFormat !== 'anthropic' && (
|
||||
<p className="text-[11px] text-[var(--color-text-tertiary)] mt-1">{t('settings.providers.proxyHint')}</p>
|
||||
)}
|
||||
{/* The preset's own endpoint is still in the field above; a custom
|
||||
preset brings none, so there is nothing to warn about. */}
|
||||
{apiFormat !== selectedPreset.apiFormat && Boolean(selectedPreset.baseUrl) && (
|
||||
<p className="text-[11px] text-[var(--color-text-tertiary)] mt-1">{t('settings.providers.apiFormatOverrideHint')}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (presetDrivesApiFormat || apiFormat !== 'anthropic') ? (
|
||||
) : (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-[var(--color-text-primary)] mb-1 block">{t('settings.providers.apiFormat')}</label>
|
||||
<div className="text-xs text-[var(--color-text-tertiary)] px-3 py-2 rounded-[var(--radius-md)] bg-[var(--color-surface-container-low)] border border-[var(--color-border)]">
|
||||
{selectedApiFormatLabel}
|
||||
</div>
|
||||
{presetDrivesApiFormat && (
|
||||
<p className="text-[11px] text-[var(--color-text-tertiary)] mt-1">{t('settings.providers.apiFormatPerModelHint')}</p>
|
||||
)}
|
||||
<p className="text-[11px] text-[var(--color-text-tertiary)] mt-1">{t('settings.providers.apiFormatPerModelHint')}</p>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
<ProviderRequestCompatibilityFields value={compatibility} apiFormat={apiFormat} onChange={handleCompatibilityChange} />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user