fix(cliproxy): keep provider sections consistent for variants

This commit is contained in:
Tam Nhu Tran
2026-04-22 20:02:54 -04:00
parent f6c86b79fc
commit 254abd504f
10 changed files with 149 additions and 51 deletions
@@ -206,7 +206,7 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
>
<option value="">{t('cliproxyDialog.selectProvider')}</option>
{CLIPROXY_PROVIDER_SECTIONS.map((section) => (
<optgroup key={section.id} label={section.label}>
<optgroup key={section.id} label={t(section.labelKey)}>
{providerOptions
.filter((opt) => section.providers.includes(opt.value))
.map((opt) => (
@@ -224,9 +224,9 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
)}
{selectedProviderSection && (
<p className="mt-2 text-xs text-muted-foreground">
{selectedProviderSection.hint}
{t(selectedProviderSection.hintKey)}
{isPlusExtraProvider(selectedProvider)
? ' Requires the optional Plus backend while that track remains community-maintained.'
? ` ${t('providerConfig.plusTrackNote')}`
: ''}
</p>
)}
@@ -320,7 +320,7 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
{CLIPROXY_PROVIDER_SECTIONS.map((section) => (
<optgroup key={section.id} label={section.label}>
<optgroup key={section.id} label={t(section.labelKey)}>
{providerOptions
.filter((opt) => section.providers.includes(opt.value))
.map((opt) => (
@@ -333,9 +333,9 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
</select>
{compositeTiers?.[tier]?.provider && (
<p className="mt-2 text-xs text-muted-foreground">
{getProviderSection(compositeTiers[tier].provider)?.hint}
{t(getProviderSection(compositeTiers[tier].provider)?.hintKey || '')}
{isPlusExtraProvider(compositeTiers[tier].provider)
? ' Requires the optional Plus backend while that track remains community-maintained.'
? ` ${t('providerConfig.plusTrackNote')}`
: ''}
</p>
)}
@@ -327,7 +327,7 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
{CLIPROXY_PROVIDER_SECTIONS.map((section) => (
<optgroup key={section.id} label={section.label}>
<optgroup key={section.id} label={t(section.labelKey)}>
{providerOptions
.filter((opt) => section.providers.includes(opt.value))
.map((opt) => (
@@ -340,9 +340,9 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
</select>
{compositeTiers?.[tier]?.provider && (
<p className="mt-2 text-xs text-muted-foreground">
{getProviderSection(compositeTiers[tier].provider)?.hint}
{t(getProviderSection(compositeTiers[tier].provider)?.hintKey || '')}
{isPlusExtraProvider(compositeTiers[tier].provider)
? ' Requires the optional Plus backend while that track remains community-maintained.'
? ` ${t('providerConfig.plusTrackNote')}`
: ''}
</p>
)}
@@ -422,7 +422,7 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
{CLIPROXY_PROVIDER_SECTIONS.map((section) => (
<optgroup key={section.id} label={section.label}>
<optgroup key={section.id} label={t(section.labelKey)}>
{providerOptions
.filter((opt) => section.providers.includes(opt.value))
.map((opt) => (
@@ -435,9 +435,9 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
</select>
{selectedProvider && (
<p className="mt-2 text-xs text-muted-foreground">
{getProviderSection(selectedProvider)?.hint}
{t(getProviderSection(selectedProvider)?.hintKey || '')}
{isPlusExtraProvider(selectedProvider)
? ' Requires the optional Plus backend while that track remains community-maintained.'
? ` ${t('providerConfig.plusTrackNote')}`
: ''}
</p>
)}
@@ -304,6 +304,7 @@ export function ProviderEditor({
<ProviderInfoTab
provider={provider}
displayName={displayName}
baseProvider={baseProvider}
defaultTarget={defaultTarget}
data={data}
authStatus={authStatus}
@@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next';
interface ProviderInfoTabProps {
provider: string;
displayName: string;
baseProvider?: string;
defaultTarget?: CliTarget;
data?: SettingsResponse;
authStatus: AuthStatus;
@@ -25,6 +26,7 @@ interface ProviderInfoTabProps {
export function ProviderInfoTab({
provider,
displayName,
baseProvider,
defaultTarget,
data,
authStatus,
@@ -34,7 +36,8 @@ export function ProviderInfoTab({
const resolvedTarget = defaultTarget || 'claude';
const isDroidTarget = resolvedTarget === 'droid';
const isCodexProvider = provider === 'codex';
const providerSection = getProviderSection(provider);
const sectionProvider = baseProvider || authStatus.provider || provider;
const providerSection = getProviderSection(sectionProvider);
const managementPrefix =
resolvedTarget === 'claude' ? `ccs ${provider}` : `ccs ${provider} --target claude`;
const changeModelCommand = `${managementPrefix} --config`;
@@ -105,13 +108,15 @@ export function ProviderInfoTab({
</div>
{providerSection && (
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-start">
<span className="font-medium text-muted-foreground">Track</span>
<span className="font-medium text-muted-foreground">
{t('providerConfig.trackLabel')}
</span>
<div className="space-y-1">
<span className="font-mono">{providerSection.label}</span>
<span className="font-mono">{t(providerSection.labelKey)}</span>
<p className="text-xs text-muted-foreground">
{providerSection.hint}
{isPlusExtraProvider(provider)
? ' Requires the optional Plus backend while that track remains community-maintained.'
{t(providerSection.hintKey)}
{isPlusExtraProvider(sectionProvider)
? ` ${t('providerConfig.plusTrackNote')}`
: ''}
</p>
</div>
+50 -12
View File
@@ -977,10 +977,13 @@ const resources = {
backendBinary: 'Backend Binary',
stopProxyToSwitch: 'Stop the running proxy in Instance Status to switch backend.',
default: 'Default',
plusDesc: 'Full provider support including Kiro and GitHub Copilot',
originalDesc: 'Original binary (Gemini, Codex, Antigravity only)',
plusDesc:
'Optional track for extra providers. Still supported, but currently community-maintained instead of upstream-maintained.',
originalDesc: 'Default, always-available backend for the core provider track.',
plusFallbackNotice:
'The Plus provider track is not deprecated, but local CLIProxy still falls back to the original backend while the maintained fork path is being brought back.',
variantsIncompatible:
'Existing Kiro/Copilot variants will not work with CLIProxyAPI. Switch to CLIProxyAPIPlus or remove those variants.',
'Existing plus-extra variants ({{providers}}) will not run on the original backend. Keep them visible for reference, but switch to Plus before using them.',
safety: 'Safety',
agyModeTitle: 'Antigravity + Gemini Power User Mode',
agyModeDesc:
@@ -2234,6 +2237,13 @@ const resources = {
},
providerConfig: {
defaultDeviceCodeInstruction: 'Complete the authorization in your browser.',
trackLabel: 'Track',
sectionCoreLabel: 'Core / original backend',
sectionCoreHint: 'Default, always-available provider track',
sectionPlusLabel: 'Plus extras / community-maintained',
sectionPlusHint: 'Still supported, but separated from the default backend for now',
plusTrackNote:
'Requires the optional Plus backend while that track remains community-maintained.',
},
// ========================================
@@ -3443,10 +3453,12 @@ const resources = {
backendBinary: '后端二进制',
stopProxyToSwitch: '请先在实例状态中停止正在运行的代理,再切换后端。',
default: '默认',
plusDesc: '完整支持包括 Kiro 和 GitHub Copilot 在内的提供商',
originalDesc: '原版二进制(仅 Gemini、Codex、Antigravity',
plusDesc: '额外提供商的可选线路。仍受支持,但目前由社区维护而非上游维护。',
originalDesc: '核心提供商线路的默认、始终可用后端。',
plusFallbackNotice:
'Plus 提供商线路并未弃用,但在受维护的 fork 恢复之前,本地 CLIProxy 仍会回退到原始后端。',
variantsIncompatible:
'现有 Kiro/Copilot 变体与 CLIProxyAPI 不兼容。请切换到 CLIProxyAPIPlus 或移除这些变体。',
'现有 plus 扩展变体({{providers}})无法在原始后端上运行。可以保留作参考,但使用前请切换到 Plus。',
safety: '安全',
agyModeTitle: 'Antigravity + Gemini 高级模式',
agyModeDesc: '跳过 AGY 责任确认清单,以及 Gemini Dashboard 中输入风险短语的步骤。',
@@ -4656,6 +4668,12 @@ const resources = {
},
providerConfig: {
defaultDeviceCodeInstruction: '请在浏览器中完成授权。',
trackLabel: '分组',
sectionCoreLabel: '核心 / 原始后端',
sectionCoreHint: '默认且始终可用的提供商线路',
sectionPlusLabel: 'Plus 扩展 / 社区维护',
sectionPlusHint: '仍然受支持,但目前与默认后端分开显示',
plusTrackNote: '需要可选的 Plus 后端;当前这条线路由社区维护。',
},
profileEditorSections: {
imageAnalysis: '图片分析',
@@ -5934,10 +5952,13 @@ const resources = {
stopProxyToSwitch:
'Dừng proxy đang chạy trong Trạng thái phiên bản trước khi chuyển backend.',
default: 'Mặc định',
plusDesc: 'Hỗ trợ đầy đủ nhà cung cấp, bao gồm Kiro và GitHub Copilot',
originalDesc: 'Binary gốc (chỉ Gemini, Codex, Antigravity)',
plusDesc:
'Nhánh tùy chọn cho các nhà cung cấp bổ sung. Vẫn được hỗ trợ nhưng hiện do cộng đồng duy trì thay vì upstream.',
originalDesc: 'Backend mặc định, luôn sẵn sàng cho nhóm nhà cung cấp cốt lõi.',
plusFallbackNotice:
'Nhánh nhà cung cấp Plus chưa bị khai tử, nhưng CLIProxy cục bộ vẫn quay về backend gốc cho tới khi đường dẫn fork được duy trì được bật lại.',
variantsIncompatible:
'Các biến thể Kiro/Copilot hiện tại sẽ không hoạt động với CLIProxyAPI. Chuyển sang CLIProxyAPIPlus hoặc xóa các biến thể đó.',
'Các biến thể plus-extra hiện có ({{providers}}) sẽ không chạy trên backend gốc. Có thể giữ lại để tham chiếu, nhưng hãy chuyển sang Plus trước khi dùng.',
safety: 'An toàn',
agyModeTitle: 'Chế độ power user Antigravity + Gemini',
agyModeDesc:
@@ -7170,6 +7191,12 @@ const resources = {
},
providerConfig: {
defaultDeviceCodeInstruction: 'Hoàn tất việc cấp quyền trong trình duyệt của bạn.',
trackLabel: 'Nhóm',
sectionCoreLabel: 'Core / backend gốc',
sectionCoreHint: 'Nhóm nhà cung cấp mặc định, luôn sẵn sàng',
sectionPlusLabel: 'Plus extras / cộng đồng duy trì',
sectionPlusHint: 'Vẫn được hỗ trợ nhưng hiện được tách khỏi backend mặc định',
plusTrackNote: 'Cần backend Plus tùy chọn; hiện tại nhánh này do cộng đồng duy trì.',
},
profileEditorSections: {
imageAnalysis: 'Phân tích hình ảnh',
@@ -8461,10 +8488,13 @@ const resources = {
stopProxyToSwitch:
'バックエンドを切り替える前に、インスタンス状態から実行中のプロキシを停止してください。',
default: 'デフォルト',
plusDesc: 'Kiro と GitHub Copilot を含むすべてのプロバイダーをサポート',
originalDesc: '元のバイナリ(Gemini、Codex、Antigravity のみ)',
plusDesc:
'追加プロバイダー向けのオプショントラックです。引き続きサポートされていますが、現在は upstream ではなくコミュニティ保守です。',
originalDesc: 'コアプロバイダートラック向けの既定かつ常時利用可能な backend。',
plusFallbackNotice:
'Plus プロバイダートラックは廃止ではありませんが、保守中の fork が戻るまではローカル CLIProxy は引き続きオリジナル backend にフォールバックします。',
variantsIncompatible:
'既存の Kiro/Copilot バリアントは CLIProxyAPI では動作しません。CLIProxyAPIPlus 切り替えるか、それらのバリアントを削除してください。',
'既存の plus-extra バリアント({{providers}})はオリジナル backend では動作しません。参照用に残すことはできますが、使用前に Plus 切り替えてください。',
safety: '安全設定',
agyModeTitle: 'Antigravity + Gemini パワーユーザーモード',
agyModeDesc:
@@ -9984,6 +10014,14 @@ const resources = {
},
providerConfig: {
defaultDeviceCodeInstruction: 'ブラウザーで認証を完了してください。',
trackLabel: 'トラック',
sectionCoreLabel: 'コア / オリジナル backend',
sectionCoreHint: '既定で常に利用できるプロバイダートラック',
sectionPlusLabel: 'Plus 拡張 / コミュニティ保守',
sectionPlusHint:
'引き続きサポートされていますが、当面は既定の backend から分離されています',
plusTrackNote:
'このトラックはオプションの Plus backend が必要で、現在はコミュニティ保守です。',
},
updatesSpotlight: {
openUpdatesCenter: '更新センターを開く',
+23 -6
View File
@@ -24,8 +24,8 @@ export type CLIProxyProviderSectionId = 'core' | 'plus-extra';
export interface CLIProxyProviderSection {
id: CLIProxyProviderSectionId;
label: string;
hint: string;
labelKey: string;
hintKey: string;
providers: readonly CLIProxyProvider[];
}
@@ -42,14 +42,14 @@ export const PLUS_EXTRA_CLIPROXY_PROVIDERS: readonly CLIProxyProvider[] = Object
export const CLIPROXY_PROVIDER_SECTIONS: readonly CLIProxyProviderSection[] = Object.freeze([
{
id: 'core',
label: 'Core / original backend',
hint: 'Default, always-available provider track',
labelKey: 'providerConfig.sectionCoreLabel',
hintKey: 'providerConfig.sectionCoreHint',
providers: CORE_CLIPROXY_PROVIDERS,
},
{
id: 'plus-extra',
label: 'Plus extras / community-maintained',
hint: 'Still supported, but separated from the default backend for now',
labelKey: 'providerConfig.sectionPlusLabel',
hintKey: 'providerConfig.sectionPlusHint',
providers: PLUS_EXTRA_CLIPROXY_PROVIDERS,
},
]);
@@ -309,6 +309,23 @@ export function getProviderSection(provider: unknown): CLIProxyProviderSection |
);
}
interface VariantLike {
provider?: unknown;
tiers?: Record<string, { provider?: unknown } | undefined> | null;
}
export function variantUsesPlusExtraProvider(variant: VariantLike | null | undefined): boolean {
if (!variant) {
return false;
}
if (variant.tiers) {
return Object.values(variant.tiers).some((tier) => isPlusExtraProvider(tier?.provider));
}
return isPlusExtraProvider(variant.provider);
}
export function groupProvidersBySection<T>(
items: readonly T[],
getProvider: (item: T) => unknown
+2 -2
View File
@@ -406,10 +406,10 @@ export function CliproxyPage() {
<div key={section.id} className="space-y-1">
<div className="px-3">
<div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
{section.label}
{t(section.labelKey)}
</div>
<p className="mt-1 text-[11px] leading-relaxed text-muted-foreground">
{section.hint}
{t(section.hintKey)}
</p>
</div>
<div className="space-y-1">
+8 -13
View File
@@ -33,7 +33,7 @@ import {
CORE_CLIPROXY_PROVIDERS,
PLUS_EXTRA_CLIPROXY_PROVIDERS,
getProviderDisplayName,
isPlusExtraProvider,
variantUsesPlusExtraProvider,
} from '@/lib/provider-config';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
@@ -216,7 +216,9 @@ export default function ProxySection() {
const checkPlusOnlyVariants = useCallback(async () => {
try {
const result = await api.cliproxy.list();
const hasIncompatible = result.variants.some((v) => isPlusExtraProvider(v.provider));
const hasIncompatible = result.variants.some((variant) =>
variantUsesPlusExtraProvider(variant)
);
setHasKiroGhcpVariants(hasIncompatible);
} catch (err) {
console.error('[Proxy] Failed to check variants:', err);
@@ -496,10 +498,7 @@ export default function ProxySection() {
<div className="flex items-center gap-3 mb-2">
<span className="font-medium">{t('settingsProxy.backendPlusApi')}</span>
</div>
<p className="text-xs text-muted-foreground">
Optional track for extra providers. Still supported, but currently
community-maintained instead of upstream-maintained.
</p>
<p className="text-xs text-muted-foreground">{t('settingsProxy.plusDesc')}</p>
<p className="mt-2 text-[11px] leading-relaxed text-muted-foreground">
{plusProviderNames}
</p>
@@ -521,9 +520,7 @@ export default function ProxySection() {
{t('settingsProxy.default')}
</span>
</div>
<p className="text-xs text-muted-foreground">
Default, always-available backend for the core provider track.
</p>
<p className="text-xs text-muted-foreground">{t('settingsProxy.originalDesc')}</p>
<p className="mt-2 text-[11px] leading-relaxed text-muted-foreground">
{coreProviderNames}
</p>
@@ -533,8 +530,7 @@ export default function ProxySection() {
<Alert className="py-2 border-amber-200 bg-amber-50 dark:border-amber-900/50 dark:bg-amber-900/20 [&>svg]:top-2.5">
<AlertTriangle className="h-4 w-4 text-amber-600" />
<AlertDescription className="text-amber-700 dark:text-amber-400">
The Plus provider track is not deprecated, but local CLIProxy still falls back to
the original backend while the maintained fork path is being brought back.
{t('settingsProxy.plusFallbackNotice')}
</AlertDescription>
</Alert>
)}
@@ -543,8 +539,7 @@ export default function ProxySection() {
<Alert variant="destructive" className="py-2">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
Existing plus-extra variants ({plusProviderNames}) will not run on the original
backend. Keep them visible for reference, but switch to Plus before using them.
{t('settingsProxy.variantsIncompatible', { providers: plusProviderNames })}
</AlertDescription>
</Alert>
)}
@@ -75,4 +75,24 @@ describe('ProviderInfoTab', () => {
)
).toBeInTheDocument();
});
it('uses the base provider when rendering variant track metadata', () => {
render(
<ProviderInfoTab
provider="my-cursor"
baseProvider="cursor"
displayName="My Cursor Variant"
defaultTarget="claude"
authStatus={{
...authenticatedStatus,
provider: 'cursor',
displayName: 'Cursor',
}}
supportsModelConfig
/>
);
expect(screen.getByText('Track')).toBeInTheDocument();
expect(screen.getByText('Plus extras / community-maintained')).toBeInTheDocument();
});
});
@@ -16,6 +16,7 @@ import {
parseRequestedUpstreamModelRules,
PLUS_EXTRA_CLIPROXY_PROVIDERS,
PROVIDER_COLORS,
variantUsesPlusExtraProvider,
} from '@/lib/provider-config';
describe('provider model mapping helpers', () => {
@@ -89,6 +90,27 @@ describe('provider presentation metadata', () => {
expect(grouped[1]?.items.map((entry) => entry.provider)).toEqual(['cursor']);
});
it('detects plus-extra providers inside composite variants', () => {
expect(
variantUsesPlusExtraProvider({
provider: 'gemini',
tiers: {
opus: { provider: 'gemini' },
sonnet: { provider: 'cursor' },
},
})
).toBe(true);
expect(
variantUsesPlusExtraProvider({
provider: 'gemini',
tiers: {
opus: { provider: 'gemini' },
sonnet: { provider: 'kimi' },
},
})
).toBe(false);
});
it.each([
['cursor', 'Cursor', 'Cursor browser-authenticated provider', '/assets/sidebar/cursor.svg'],
['gitlab', 'GitLab Duo', 'GitLab Duo with OAuth or PAT auth', '/assets/providers/gitlab.svg'],