feat(multimodel): unify provider catalog and branding
This commit is contained in:
parent
53bcea337f
commit
35970000b6
13 changed files with 7295 additions and 277 deletions
|
|
@ -20,14 +20,16 @@ const bundledDeps = prodDeps.filter(d => d !== 'node-pty' && d !== 'agent-teams-
|
|||
// but they have pure JS fallbacks when the native module isn't available.
|
||||
function nativeModuleStub(): Plugin {
|
||||
const STUB_ID = '\0native-stub'
|
||||
const NODE_MODULE_RE = /\.node(?:\?.*)?$/
|
||||
return {
|
||||
name: 'native-module-stub',
|
||||
enforce: 'pre',
|
||||
resolveId(source) {
|
||||
if (source.endsWith('.node')) return STUB_ID
|
||||
if (NODE_MODULE_RE.test(source)) return `${STUB_ID}:${source}`
|
||||
return null
|
||||
},
|
||||
load(id) {
|
||||
if (id === STUB_ID) return 'export default {}'
|
||||
if (id.startsWith(STUB_ID) || NODE_MODULE_RE.test(id)) return 'export default {}'
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -71,6 +73,9 @@ export default defineConfig({
|
|||
externalizeDeps: {
|
||||
exclude: bundledDeps
|
||||
},
|
||||
commonjsOptions: {
|
||||
strictRequires: [/node_modules\/.*ssh2\//],
|
||||
},
|
||||
sourcemap: 'hidden',
|
||||
outDir: 'dist-electron/main',
|
||||
rollupOptions: {
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -55,7 +55,7 @@
|
|||
"standalone:build": "electron-vite build && vite build --config docker/vite.standalone.config.ts",
|
||||
"standalone:start": "node dist-standalone/index.cjs",
|
||||
"prepare": "husky",
|
||||
"postinstall": "electron-rebuild -f -o node-pty || echo 'node-pty rebuild failed (terminal will be disabled)'"
|
||||
"postinstall": "electron-rebuild -f -o node-pty,ssh2,cpu-features || echo 'native Electron rebuild failed (terminal/ssh features may be degraded)'"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{ts,tsx,js,jsx}": [
|
||||
|
|
@ -331,5 +331,11 @@
|
|||
"ignoreBinaries": [
|
||||
"pkg"
|
||||
]
|
||||
}
|
||||
},
|
||||
"workspaces": [
|
||||
"agent-teams-controller",
|
||||
"mcp-server",
|
||||
"landing",
|
||||
"packages/agent-graph"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
206
src/renderer/components/common/ProviderBrandLogo.tsx
Normal file
206
src/renderer/components/common/ProviderBrandLogo.tsx
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
import { useId } from 'react';
|
||||
|
||||
import type { CliProviderId } from '@shared/types';
|
||||
|
||||
type ProviderBrandLogoId = CliProviderId | 'opencode';
|
||||
|
||||
type BrandLogoProps = Readonly<{
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
interface ProviderBrandLogoProps {
|
||||
readonly providerId: ProviderBrandLogoId;
|
||||
readonly className?: string;
|
||||
}
|
||||
|
||||
const AnthropicBrandLogo = ({ className }: BrandLogoProps): React.JSX.Element => {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className={className} aria-hidden="true">
|
||||
<g fill="#D97757">
|
||||
{Array.from({ length: 10 }).map((_, index) => (
|
||||
<rect
|
||||
key={index}
|
||||
x="10.75"
|
||||
y="1.8"
|
||||
width="2.5"
|
||||
height="7.7"
|
||||
rx="1.2"
|
||||
transform={`rotate(${index * 36} 12 12)`}
|
||||
/>
|
||||
))}
|
||||
<circle cx="12" cy="12" r="3.1" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const CodexBrandLogo = ({ className }: BrandLogoProps): React.JSX.Element => {
|
||||
const gradientId = useId();
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className={className} aria-hidden="true">
|
||||
<rect x="1.25" y="1.25" width="21.5" height="21.5" rx="6.5" fill="#F8FAFC" />
|
||||
<path
|
||||
d="M17.6 10.2a4.95 4.95 0 0 0-8.58-2.43 3.7 3.7 0 0 0-4.25 5.73A3.46 3.46 0 0 0 6.34 20h10.12a3.65 3.65 0 0 0 1.14-7.14Z"
|
||||
fill={`url(#${gradientId})`}
|
||||
/>
|
||||
<path
|
||||
d="M9.05 9.55 11.4 12l-2.35 2.45"
|
||||
fill="none"
|
||||
stroke="#FFFFFF"
|
||||
strokeWidth="1.7"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.1 14.45h3.05"
|
||||
fill="none"
|
||||
stroke="#FFFFFF"
|
||||
strokeWidth="1.7"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={gradientId}
|
||||
x1="12"
|
||||
y1="6.4"
|
||||
x2="12"
|
||||
y2="20"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor="#A5B4FC" />
|
||||
<stop offset="0.55" stopColor="#6F8CFF" />
|
||||
<stop offset="1" stopColor="#3B46FF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const GeminiBrandLogo = ({ className }: BrandLogoProps): React.JSX.Element => {
|
||||
const gradientId = useId();
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className={className} aria-hidden="true">
|
||||
<path
|
||||
d="M12 2.25c.62 3.9 1.6 6.57 3.18 8.15 1.58 1.58 4.25 2.56 8.15 3.18-3.9.62-6.57 1.6-8.15 3.18-1.58 1.58-2.56 4.25-3.18 8.15-.62-3.9-1.6-6.57-3.18-8.15-1.58-1.58-4.25-2.56-8.15-3.18 3.9-.62 6.57-1.6 8.15-3.18C10.4 8.82 11.38 6.15 12 2.25Z"
|
||||
fill={`url(#${gradientId})`}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={gradientId}
|
||||
x1="4"
|
||||
y1="4"
|
||||
x2="20"
|
||||
y2="20"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor="#9F7AEA" />
|
||||
<stop offset="1" stopColor="#60A5FA" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const OpenCodeBrandLogo = ({ className }: BrandLogoProps): React.JSX.Element => {
|
||||
const backgroundId = useId();
|
||||
const frameId = useId();
|
||||
const frameStrokeId = useId();
|
||||
const coreId = useId();
|
||||
const coreStrokeId = useId();
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className={className} aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={backgroundId}
|
||||
x1="4"
|
||||
y1="3"
|
||||
x2="20"
|
||||
y2="21"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor="#303030" />
|
||||
<stop offset="1" stopColor="#161616" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id={frameId}
|
||||
x1="7"
|
||||
y1="4.5"
|
||||
x2="17"
|
||||
y2="19.5"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor="#f4f4f4" />
|
||||
<stop offset="0.35" stopColor="#d9d9d9" />
|
||||
<stop offset="0.68" stopColor="#a8a8a8" />
|
||||
<stop offset="1" stopColor="#ececec" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id={frameStrokeId}
|
||||
x1="7"
|
||||
y1="4.5"
|
||||
x2="17"
|
||||
y2="19.5"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor="#ffffff" stopOpacity="0.9" />
|
||||
<stop offset="1" stopColor="#5a5a5a" stopOpacity="0.9" />
|
||||
</linearGradient>
|
||||
<linearGradient id={coreId} x1="12" y1="7" x2="12" y2="17" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stopColor="#121212" />
|
||||
<stop offset="0.42" stopColor="#3e3b33" />
|
||||
<stop offset="1" stopColor="#16140f" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id={coreStrokeId}
|
||||
x1="9"
|
||||
y1="7"
|
||||
x2="15"
|
||||
y2="17"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor="#f2f2f2" stopOpacity="0.95" />
|
||||
<stop offset="1" stopColor="#6e6e6e" stopOpacity="0.85" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="1.5" y="1.5" width="21" height="21" rx="5.2" fill={`url(#${backgroundId})`} />
|
||||
<path
|
||||
d="M7 4.25h10c.3 0 .55.25.55.55v14.4c0 .3-.25.55-.55.55H7c-.3 0-.55-.25-.55-.55V4.8c0-.3.25-.55.55-.55Z"
|
||||
fill={`url(#${frameId})`}
|
||||
stroke={`url(#${frameStrokeId})`}
|
||||
strokeWidth="0.55"
|
||||
/>
|
||||
<path
|
||||
d="M8.95 7.25h6.1c.22 0 .4.18.4.4v8.7c0 .22-.18.4-.4.4h-6.1a.4.4 0 0 1-.4-.4v-8.7c0-.22.18-.4.4-.4Z"
|
||||
fill={`url(#${coreId})`}
|
||||
stroke={`url(#${coreStrokeId})`}
|
||||
strokeWidth="0.45"
|
||||
/>
|
||||
<path
|
||||
d="M9.25 7.6h5.5"
|
||||
stroke="#ffffff"
|
||||
strokeOpacity="0.18"
|
||||
strokeWidth="0.45"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProviderBrandLogo = ({
|
||||
providerId,
|
||||
className,
|
||||
}: ProviderBrandLogoProps): React.JSX.Element => {
|
||||
switch (providerId) {
|
||||
case 'anthropic':
|
||||
return <AnthropicBrandLogo className={className} />;
|
||||
case 'codex':
|
||||
return <CodexBrandLogo className={className} />;
|
||||
case 'gemini':
|
||||
return <GeminiBrandLogo className={className} />;
|
||||
case 'opencode':
|
||||
return <OpenCodeBrandLogo className={className} />;
|
||||
}
|
||||
};
|
||||
|
|
@ -11,16 +11,21 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
|
||||
import { api, isElectronMode } from '@renderer/api';
|
||||
import { confirm } from '@renderer/components/common/ConfirmDialog';
|
||||
import { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog';
|
||||
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
||||
import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
|
||||
import { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog';
|
||||
import { SettingsToggle } from '@renderer/components/settings/components';
|
||||
import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice';
|
||||
import { TerminalLogPanel } from '@renderer/components/terminal/TerminalLogPanel';
|
||||
import { TerminalModal } from '@renderer/components/terminal/TerminalModal';
|
||||
import { useCliInstaller } from '@renderer/hooks/useCliInstaller';
|
||||
import { useStore } from '@renderer/store';
|
||||
import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice';
|
||||
import { formatBytes } from '@renderer/utils/formatters';
|
||||
import { filterMainScreenCliProviders } from '@renderer/utils/geminiUiFreeze';
|
||||
import {
|
||||
getTeamModelBadgeLabel,
|
||||
getVisibleTeamProviderModels,
|
||||
} from '@renderer/utils/teamModelCatalog';
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
|
|
@ -265,32 +270,25 @@ function formatProviderStatus(provider: CliProviderStatus): string {
|
|||
}
|
||||
|
||||
function formatProviderModels(provider: CliProviderStatus): string | null {
|
||||
return provider.models.length > 0 ? provider.models.join(', ') : null;
|
||||
const visibleModels = getVisibleTeamProviderModels(provider.providerId, provider.models);
|
||||
return visibleModels.length > 0 ? visibleModels.join(', ') : null;
|
||||
}
|
||||
|
||||
function formatModelBadgeLabel(providerId: CliProviderId, model: string): string {
|
||||
if (providerId === 'anthropic') {
|
||||
return model.replace(/^claude-/, '');
|
||||
}
|
||||
if (providerId === 'codex') {
|
||||
return model.replace(/^gpt-/, '');
|
||||
}
|
||||
if (providerId === 'gemini') {
|
||||
return model.replace(/^gemini-/, '');
|
||||
}
|
||||
return model;
|
||||
return getTeamModelBadgeLabel(providerId, model) ?? model;
|
||||
}
|
||||
|
||||
function ModelBadges({
|
||||
const ModelBadges = ({
|
||||
providerId,
|
||||
models,
|
||||
}: {
|
||||
providerId: CliProviderId;
|
||||
models: string[];
|
||||
}): React.JSX.Element {
|
||||
readonly providerId: CliProviderId;
|
||||
readonly models: string[];
|
||||
}): React.JSX.Element => {
|
||||
const visibleModels = getVisibleTeamProviderModels(providerId, models);
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{models.map((model) => (
|
||||
{visibleModels.map((model) => (
|
||||
<span
|
||||
key={model}
|
||||
className="rounded-md border px-1.5 py-px font-mono text-[10px] leading-4"
|
||||
|
|
@ -305,9 +303,9 @@ function ModelBadges({
|
|||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function ProviderDetailSkeleton(): React.JSX.Element {
|
||||
const ProviderDetailSkeleton = (): React.JSX.Element => {
|
||||
return (
|
||||
<div className="mt-1 space-y-2">
|
||||
<div
|
||||
|
|
@ -329,7 +327,7 @@ function ProviderDetailSkeleton(): React.JSX.Element {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function isProviderCardLoading(provider: CliProviderStatus, providerLoading: boolean): boolean {
|
||||
return (
|
||||
|
|
@ -535,14 +533,23 @@ const InstalledBanner = ({
|
|||
return (
|
||||
<div
|
||||
key={provider.providerId}
|
||||
className="grid min-h-[132px] grid-cols-[minmax(0,1fr)_auto] gap-x-3 gap-y-2 rounded-md px-2 py-2"
|
||||
className="grid min-h-[132px] grid-cols-[minmax(0,1fr)_auto] gap-x-3 gap-y-2 rounded-md p-2"
|
||||
style={{ backgroundColor: 'rgba(255, 255, 255, 0.02)' }}
|
||||
>
|
||||
<div className="col-span-2 flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium" style={{ color: 'var(--color-text)' }}>
|
||||
{provider.displayName}
|
||||
<span className="flex items-center gap-2">
|
||||
<ProviderBrandLogo
|
||||
providerId={provider.providerId}
|
||||
className="size-4 shrink-0"
|
||||
/>
|
||||
<span
|
||||
className="text-xs font-medium"
|
||||
style={{ color: 'var(--color-text)' }}
|
||||
>
|
||||
{provider.displayName}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="text-xs"
|
||||
|
|
@ -557,7 +564,7 @@ const InstalledBanner = ({
|
|||
<ProviderDetailSkeleton />
|
||||
) : (
|
||||
<div
|
||||
className="mt-1 flex min-h-[2.75rem] flex-wrap items-center gap-x-3 gap-y-1 text-[11px]"
|
||||
className="mt-1 flex min-h-11 flex-wrap items-center gap-x-3 gap-y-1 text-[11px]"
|
||||
style={{ color: 'var(--color-text-muted)' }}
|
||||
>
|
||||
{provider.backend?.label && (
|
||||
|
|
@ -1058,7 +1065,7 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
|
|||
}
|
||||
|
||||
// ── Completed ──────────────────────────────────────────────────────────
|
||||
if (installerState === 'completed' && (!cliStatus || !cliStatus.installed)) {
|
||||
if (installerState === 'completed' && !cliStatus?.installed) {
|
||||
return <InstallCompletedNotice version={completedVersion} />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,14 +9,19 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
|
||||
import { isElectronMode } from '@renderer/api';
|
||||
import { confirm } from '@renderer/components/common/ConfirmDialog';
|
||||
import { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog';
|
||||
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
||||
import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
|
||||
import { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog';
|
||||
import { SettingsToggle } from '@renderer/components/settings/components';
|
||||
import { TerminalModal } from '@renderer/components/terminal/TerminalModal';
|
||||
import { useCliInstaller } from '@renderer/hooks/useCliInstaller';
|
||||
import { useStore } from '@renderer/store';
|
||||
import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice';
|
||||
import { formatBytes } from '@renderer/utils/formatters';
|
||||
import {
|
||||
getTeamModelBadgeLabel,
|
||||
getVisibleTeamProviderModels,
|
||||
} from '@renderer/utils/teamModelCatalog';
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
|
|
@ -35,28 +40,20 @@ import { SettingsSectionHeader } from '../components';
|
|||
import type { CliProviderId, CliProviderStatus } from '@shared/types';
|
||||
|
||||
function formatModelBadgeLabel(providerId: CliProviderId, model: string): string {
|
||||
if (providerId === 'anthropic') {
|
||||
return model.replace(/^claude-/, '');
|
||||
}
|
||||
if (providerId === 'codex') {
|
||||
return model.replace(/^gpt-/, '');
|
||||
}
|
||||
if (providerId === 'gemini') {
|
||||
return model.replace(/^gemini-/, '');
|
||||
}
|
||||
return model;
|
||||
return getTeamModelBadgeLabel(providerId, model) ?? model;
|
||||
}
|
||||
|
||||
function ModelBadges({
|
||||
const ModelBadges = ({
|
||||
providerId,
|
||||
models,
|
||||
}: {
|
||||
providerId: CliProviderId;
|
||||
models: string[];
|
||||
}): React.JSX.Element {
|
||||
readonly providerId: CliProviderId;
|
||||
readonly models: string[];
|
||||
}): React.JSX.Element => {
|
||||
const visibleModels = getVisibleTeamProviderModels(providerId, models);
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{models.map((model) => (
|
||||
{visibleModels.map((model) => (
|
||||
<span
|
||||
key={model}
|
||||
className="rounded-md border px-1.5 py-px font-mono text-[10px] leading-4"
|
||||
|
|
@ -71,9 +68,9 @@ function ModelBadges({
|
|||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function ProviderDetailSkeleton(): React.JSX.Element {
|
||||
const ProviderDetailSkeleton = (): React.JSX.Element => {
|
||||
return (
|
||||
<div className="mt-1 space-y-2">
|
||||
<div
|
||||
|
|
@ -95,7 +92,7 @@ function ProviderDetailSkeleton(): React.JSX.Element {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function isProviderCardLoading(provider: CliProviderStatus, providerLoading: boolean): boolean {
|
||||
return (
|
||||
|
|
@ -458,11 +455,17 @@ export const CliStatusSection = (): React.JSX.Element | null => {
|
|||
<div className="col-span-2 flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span
|
||||
className="font-medium"
|
||||
style={{ color: 'var(--color-text-secondary)' }}
|
||||
>
|
||||
{provider.displayName}
|
||||
<span className="flex items-center gap-2">
|
||||
<ProviderBrandLogo
|
||||
providerId={provider.providerId}
|
||||
className="size-4 shrink-0"
|
||||
/>
|
||||
<span
|
||||
className="font-medium"
|
||||
style={{ color: 'var(--color-text-secondary)' }}
|
||||
>
|
||||
{provider.displayName}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
|
|
@ -482,7 +485,7 @@ export const CliStatusSection = (): React.JSX.Element | null => {
|
|||
<ProviderDetailSkeleton />
|
||||
) : (
|
||||
<div
|
||||
className="mt-1 flex min-h-[2.75rem] flex-wrap gap-x-3 gap-y-1 text-[11px]"
|
||||
className="mt-1 flex min-h-11 flex-wrap gap-x-3 gap-y-1 text-[11px]"
|
||||
style={{ color: 'var(--color-text-muted)' }}
|
||||
>
|
||||
{provider.backend?.label && (
|
||||
|
|
|
|||
|
|
@ -15,10 +15,14 @@ import {
|
|||
isGeminiUiFrozen,
|
||||
} from '@renderer/utils/geminiUiFreeze';
|
||||
import {
|
||||
doesTeamModelCarryProviderBrand,
|
||||
getTeamModelLabel as getCatalogTeamModelLabel,
|
||||
getTeamProviderLabel as getCatalogTeamProviderLabel,
|
||||
getTeamProviderModelOptions,
|
||||
getTeamModelUiDisabledReason,
|
||||
normalizeTeamModelForUi,
|
||||
TEAM_MODEL_UI_DISABLED_BADGE_LABEL,
|
||||
} from '@renderer/utils/teamModelAvailability';
|
||||
} from '@renderer/utils/teamModelCatalog';
|
||||
import { Check, ChevronDown, Info } from 'lucide-react';
|
||||
|
||||
// --- Provider SVG Icons (real brand logos from Simple Icons, monochrome currentColor) ---
|
||||
|
|
@ -145,65 +149,12 @@ const PROVIDERS: ProviderDef[] = [
|
|||
|
||||
const OPENCODE_UI_DISABLED_REASON = 'OpenCode in development';
|
||||
|
||||
const ANTHROPIC_MODEL_OPTIONS = [
|
||||
{ value: '', label: 'Default' },
|
||||
{ value: 'opus', label: 'Opus 4.6' },
|
||||
{ value: 'sonnet', label: 'Sonnet 4.6' },
|
||||
{ value: 'haiku', label: 'Haiku 4.5' },
|
||||
] as const;
|
||||
|
||||
const CODEX_MODEL_OPTIONS = [
|
||||
{ value: '', label: 'Default' },
|
||||
{ value: 'gpt-5.4', label: 'GPT-5.4' },
|
||||
{ value: 'gpt-5.4-mini', label: 'GPT-5.4 Mini' },
|
||||
{ value: 'gpt-5.3-codex', label: 'GPT-5.3 Codex' },
|
||||
{ value: 'gpt-5.3-codex-spark', label: 'GPT-5.3 Codex Spark' },
|
||||
{ value: 'gpt-5.2', label: 'GPT-5.2' },
|
||||
{ value: 'gpt-5.2-codex', label: 'GPT-5.2 Codex' },
|
||||
{ value: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' },
|
||||
{ value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max' },
|
||||
] as const;
|
||||
|
||||
const GEMINI_MODEL_OPTIONS = [
|
||||
{ value: '', label: 'Default' },
|
||||
{ value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
|
||||
{ value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
|
||||
{ value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash Lite' },
|
||||
] as const;
|
||||
|
||||
const MODEL_LABEL_OVERRIDES: Record<string, string> = {
|
||||
'claude-sonnet-4-6': 'Sonnet 4.6',
|
||||
'claude-sonnet-4-6[1m]': 'Sonnet 4.6 (1M)',
|
||||
'claude-opus-4-6': 'Opus 4.6',
|
||||
'claude-opus-4-6[1m]': 'Opus 4.6 (1M)',
|
||||
'claude-haiku-4-5-20251001': 'Haiku 4.5',
|
||||
'gpt-5.4': 'GPT-5.4',
|
||||
'gpt-5.4-mini': 'GPT-5.4 Mini',
|
||||
'gpt-5.3-codex': 'GPT-5.3 Codex',
|
||||
'gpt-5.3-codex-spark': 'GPT-5.3 Spark',
|
||||
'gpt-5.2-codex': 'GPT-5.2 Codex',
|
||||
'gpt-5.2': 'GPT-5.2',
|
||||
'gpt-5.1-codex-mini': 'GPT-5.1 Mini',
|
||||
'gpt-5.1-codex-max': 'GPT-5.1 Max',
|
||||
'gemini-2.5-pro': 'Gemini 2.5 Pro',
|
||||
'gemini-2.5-flash': 'Gemini 2.5 Flash',
|
||||
'gemini-2.5-flash-lite': 'Gemini 2.5 Flash Lite',
|
||||
};
|
||||
|
||||
export function getTeamModelLabel(model: string): string {
|
||||
return MODEL_LABEL_OVERRIDES[model] ?? model;
|
||||
return getCatalogTeamModelLabel(model) ?? model;
|
||||
}
|
||||
|
||||
export function getTeamProviderLabel(providerId: 'anthropic' | 'codex' | 'gemini'): string {
|
||||
switch (providerId) {
|
||||
case 'codex':
|
||||
return 'Codex';
|
||||
case 'gemini':
|
||||
return 'Gemini';
|
||||
case 'anthropic':
|
||||
default:
|
||||
return 'Anthropic';
|
||||
}
|
||||
return getCatalogTeamProviderLabel(providerId) ?? 'Anthropic';
|
||||
}
|
||||
|
||||
export function getTeamEffortLabel(effort: string): string {
|
||||
|
|
@ -221,16 +172,7 @@ export function formatTeamModelSummary(
|
|||
const modelLabel = model.trim() ? getTeamModelLabel(model.trim()) : 'Default';
|
||||
const effortLabel = effort?.trim() ? getTeamEffortLabel(effort) : '';
|
||||
|
||||
const normalizedProvider = providerLabel.trim().toLowerCase();
|
||||
const normalizedModel = modelLabel.trim().toLowerCase();
|
||||
const modelAlreadyCarriesProviderBrand =
|
||||
modelLabel !== 'Default' &&
|
||||
(normalizedModel.startsWith(normalizedProvider) ||
|
||||
(providerId === 'anthropic' && normalizedModel.startsWith('claude')) ||
|
||||
(providerId === 'codex' && normalizedModel.startsWith('codex')) ||
|
||||
(providerId === 'codex' && normalizedModel.startsWith('gpt')) ||
|
||||
(providerId === 'gemini' && normalizedModel.startsWith('gemini')));
|
||||
|
||||
const modelAlreadyCarriesProviderBrand = doesTeamModelCarryProviderBrand(providerId, modelLabel);
|
||||
const providerActsAsBackendOnly =
|
||||
providerId !== 'anthropic' && modelLabel !== 'Default' && !modelAlreadyCarriesProviderBrand;
|
||||
|
||||
|
|
@ -336,12 +278,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
}, [normalizedValue, onValueChange, value]);
|
||||
|
||||
const modelOptions = useMemo(() => {
|
||||
const fallback =
|
||||
effectiveProviderId === 'codex'
|
||||
? CODEX_MODEL_OPTIONS
|
||||
: effectiveProviderId === 'gemini'
|
||||
? GEMINI_MODEL_OPTIONS
|
||||
: ANTHROPIC_MODEL_OPTIONS;
|
||||
const fallback = getTeamProviderModelOptions(effectiveProviderId);
|
||||
if (effectiveProviderId === 'anthropic' || runtimeModels.length === 0) {
|
||||
return [...fallback];
|
||||
}
|
||||
|
|
@ -469,10 +406,6 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
Codex and Gemini require Multimodel mode.
|
||||
</p>
|
||||
)}
|
||||
{disableGeminiOption && isGeminiUiFrozen() && (
|
||||
<p className="text-[11px] text-[var(--color-text-muted)]">{GEMINI_UI_DISABLED_REASON}.</p>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="grid gap-1.5 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1.5"
|
||||
style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))' }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { displayMemberName } from '@renderer/utils/memberHelpers';
|
||||
import {
|
||||
doesTeamModelCarryProviderBrand,
|
||||
getTeamModelLabel,
|
||||
getTeamProviderLabel,
|
||||
} from '@renderer/utils/teamModelCatalog';
|
||||
|
||||
import type { InboxMessage } from '@shared/types';
|
||||
|
||||
|
|
@ -36,58 +41,6 @@ function parseProviderId(value: string | undefined): TeamProviderId | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
function getTeamModelLabel(model: string): string {
|
||||
const trimmed = model.trim();
|
||||
switch (trimmed) {
|
||||
case 'gemini-2.5-pro':
|
||||
return 'Gemini 2.5 Pro';
|
||||
case 'gemini-2.5-flash':
|
||||
return 'Gemini 2.5 Flash';
|
||||
case 'gemini-2.5-flash-lite':
|
||||
return 'Gemini 2.5 Flash Lite';
|
||||
case 'gpt-5.4':
|
||||
return 'GPT-5.4';
|
||||
case 'gpt-5.4-mini':
|
||||
return 'GPT-5.4 Mini';
|
||||
case 'gpt-5.3-codex':
|
||||
return 'GPT-5.3 Codex';
|
||||
case 'gpt-5.3-codex-spark':
|
||||
return 'GPT-5.3 Codex Spark';
|
||||
case 'gpt-5.2':
|
||||
return 'GPT-5.2';
|
||||
case 'gpt-5.2-codex':
|
||||
return 'GPT-5.2 Codex';
|
||||
case 'gpt-5.1-codex-mini':
|
||||
return 'GPT-5.1 Codex Mini';
|
||||
case 'gpt-5.1-codex-max':
|
||||
return 'GPT-5.1 Codex Max';
|
||||
case 'claude-sonnet-4-6':
|
||||
return 'Sonnet 4.6';
|
||||
case 'claude-sonnet-4-6[1m]':
|
||||
return 'Sonnet 4.6 (1M)';
|
||||
case 'claude-opus-4-6':
|
||||
return 'Opus 4.6';
|
||||
case 'claude-opus-4-6[1m]':
|
||||
return 'Opus 4.6 (1M)';
|
||||
case 'claude-haiku-4-5-20251001':
|
||||
return 'Haiku 4.5';
|
||||
default:
|
||||
return trimmed || 'Default';
|
||||
}
|
||||
}
|
||||
|
||||
function getTeamProviderLabel(providerId: TeamProviderId): string {
|
||||
switch (providerId) {
|
||||
case 'codex':
|
||||
return 'Codex';
|
||||
case 'gemini':
|
||||
return 'Gemini';
|
||||
case 'anthropic':
|
||||
default:
|
||||
return 'Anthropic';
|
||||
}
|
||||
}
|
||||
|
||||
function getTeamEffortLabel(effort: string | undefined): string {
|
||||
const trimmed = effort?.trim() ?? '';
|
||||
return trimmed ? trimmed.charAt(0).toUpperCase() + trimmed.slice(1) : 'Default';
|
||||
|
|
@ -105,18 +58,13 @@ function buildRuntimeSummary(
|
|||
effort: string | undefined
|
||||
): string | undefined {
|
||||
if (providerId) {
|
||||
const providerLabel = getTeamProviderLabel(providerId);
|
||||
const modelLabel = model ? getTeamModelLabel(model) : 'Default';
|
||||
const providerLabel = getTeamProviderLabel(providerId) ?? 'Anthropic';
|
||||
const modelLabel = model ? (getTeamModelLabel(model) ?? model) : 'Default';
|
||||
const effortLabel = getTeamEffortLabel(effort);
|
||||
const normalizedProvider = providerLabel.trim().toLowerCase();
|
||||
const normalizedModel = modelLabel.trim().toLowerCase();
|
||||
const modelAlreadyCarriesProviderBrand =
|
||||
modelLabel !== 'Default' &&
|
||||
(normalizedModel.startsWith(normalizedProvider) ||
|
||||
(providerId === 'anthropic' && normalizedModel.startsWith('claude')) ||
|
||||
(providerId === 'codex' &&
|
||||
(normalizedModel.startsWith('codex') || normalizedModel.startsWith('gpt'))) ||
|
||||
(providerId === 'gemini' && normalizedModel.startsWith('gemini')));
|
||||
const modelAlreadyCarriesProviderBrand = doesTeamModelCarryProviderBrand(
|
||||
providerId,
|
||||
modelLabel
|
||||
);
|
||||
|
||||
const providerActsAsBackendOnly =
|
||||
providerId !== 'anthropic' && modelLabel !== 'Default' && !modelAlreadyCarriesProviderBrand;
|
||||
|
|
@ -129,9 +77,9 @@ function buildRuntimeSummary(
|
|||
return parts.filter(Boolean).join(' · ');
|
||||
}
|
||||
|
||||
const modelLabel = model ? getTeamModelLabel(model) : '';
|
||||
const modelLabel = model ? (getTeamModelLabel(model) ?? model) : '';
|
||||
const effortLabel = effort ? getTeamEffortLabel(effort) : '';
|
||||
const providerLabel = providerId ? getTeamProviderLabel(providerId) : '';
|
||||
const providerLabel = providerId ? (getTeamProviderLabel(providerId) ?? '') : '';
|
||||
return [providerLabel, modelLabel, effortLabel].filter(Boolean).join(' · ') || undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +1,10 @@
|
|||
import type { TeamProviderId } from '@shared/types';
|
||||
|
||||
export const TEAM_MODEL_UI_DISABLED_BADGE_LABEL = 'Disabled';
|
||||
export const GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL = 'gpt-5.1-codex-mini';
|
||||
export const GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL = 'gpt-5.3-codex-spark';
|
||||
export const GPT_5_1_CODEX_MINI_UI_DISABLED_REASON =
|
||||
'Temporarily disabled for team agents - this model has been less reliable with task and reply tool contracts.';
|
||||
export const GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON =
|
||||
'Temporarily disabled for team agents - this model has been less reliable with bootstrap, task, and reply tool contracts.';
|
||||
|
||||
export function getTeamModelUiDisabledReason(
|
||||
providerId: TeamProviderId | undefined,
|
||||
model: string | undefined
|
||||
): string | null {
|
||||
if (providerId === 'codex' && model === GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL) {
|
||||
return GPT_5_1_CODEX_MINI_UI_DISABLED_REASON;
|
||||
}
|
||||
if (providerId === 'codex' && model === GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL) {
|
||||
return GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isTeamModelUiDisabled(
|
||||
providerId: TeamProviderId | undefined,
|
||||
model: string | undefined
|
||||
): boolean {
|
||||
return getTeamModelUiDisabledReason(providerId, model) !== null;
|
||||
}
|
||||
|
||||
export function normalizeTeamModelForUi(
|
||||
providerId: TeamProviderId | undefined,
|
||||
model: string | undefined
|
||||
): string {
|
||||
return isTeamModelUiDisabled(providerId, model) ? '' : (model ?? '');
|
||||
}
|
||||
export {
|
||||
GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL,
|
||||
GPT_5_1_CODEX_MINI_UI_DISABLED_REASON,
|
||||
GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL,
|
||||
GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON,
|
||||
TEAM_MODEL_UI_DISABLED_BADGE_LABEL,
|
||||
getTeamModelUiDisabledReason,
|
||||
isTeamModelUiDisabled,
|
||||
normalizeTeamModelForUi,
|
||||
} from './teamModelCatalog';
|
||||
|
|
|
|||
231
src/renderer/utils/teamModelCatalog.ts
Normal file
231
src/renderer/utils/teamModelCatalog.ts
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import type { CliProviderId, TeamProviderId } from '@shared/types';
|
||||
|
||||
type SupportedProviderId = CliProviderId | TeamProviderId;
|
||||
|
||||
export interface TeamProviderModelOption {
|
||||
value: string;
|
||||
label: string;
|
||||
badgeLabel?: string;
|
||||
uiDisabledReason?: string;
|
||||
}
|
||||
|
||||
export const TEAM_MODEL_UI_DISABLED_BADGE_LABEL = 'Disabled';
|
||||
export const GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL = 'gpt-5.1-codex-mini';
|
||||
export const GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL = 'gpt-5.3-codex-spark';
|
||||
export const GPT_5_1_CODEX_MINI_UI_DISABLED_REASON =
|
||||
'Temporarily disabled for team agents - this model has been less reliable with task and reply tool contracts.';
|
||||
export const GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON =
|
||||
'Temporarily disabled for team agents - this model has been less reliable with bootstrap, task, and reply tool contracts.';
|
||||
|
||||
const TEAM_PROVIDER_LABELS: Record<SupportedProviderId, string> = {
|
||||
anthropic: 'Anthropic',
|
||||
codex: 'Codex',
|
||||
gemini: 'Gemini',
|
||||
};
|
||||
|
||||
const TEAM_MODEL_LABEL_OVERRIDES: Record<string, string> = {
|
||||
default: 'Default',
|
||||
opus: 'Opus 4.6',
|
||||
sonnet: 'Sonnet 4.6',
|
||||
haiku: 'Haiku 4.5',
|
||||
'claude-sonnet-4-6': 'Sonnet 4.6',
|
||||
'claude-sonnet-4-6[1m]': 'Sonnet 4.6 (1M)',
|
||||
'claude-opus-4-6': 'Opus 4.6',
|
||||
'claude-opus-4-6[1m]': 'Opus 4.6 (1M)',
|
||||
'claude-haiku-4-5-20251001': 'Haiku 4.5',
|
||||
'gpt-5.4': 'GPT-5.4',
|
||||
'gpt-5.4-mini': 'GPT-5.4 Mini',
|
||||
'gpt-5.3-codex': 'GPT-5.3 Codex',
|
||||
'gpt-5.3-codex-spark': 'GPT-5.3 Codex Spark',
|
||||
'gpt-5.2': 'GPT-5.2',
|
||||
'gpt-5.2-codex': 'GPT-5.2 Codex',
|
||||
'gpt-5.1-codex-mini': 'GPT-5.1 Codex Mini',
|
||||
'gpt-5.1-codex-max': 'GPT-5.1 Codex Max',
|
||||
'gemini-2.5-pro': 'Gemini 2.5 Pro',
|
||||
'gemini-2.5-flash': 'Gemini 2.5 Flash',
|
||||
'gemini-2.5-flash-lite': 'Gemini 2.5 Flash Lite',
|
||||
};
|
||||
|
||||
const TEAM_PROVIDER_MODEL_OPTIONS: Record<SupportedProviderId, readonly TeamProviderModelOption[]> =
|
||||
{
|
||||
anthropic: [
|
||||
{ value: '', label: 'Default', badgeLabel: 'Default' },
|
||||
{ value: 'opus', label: 'Opus 4.6', badgeLabel: 'Opus 4.6' },
|
||||
{ value: 'sonnet', label: 'Sonnet 4.6', badgeLabel: 'Sonnet 4.6' },
|
||||
{ value: 'haiku', label: 'Haiku 4.5', badgeLabel: 'Haiku 4.5' },
|
||||
],
|
||||
codex: [
|
||||
{ value: '', label: 'Default', badgeLabel: 'Default' },
|
||||
{ value: 'gpt-5.4', label: 'GPT-5.4', badgeLabel: '5.4' },
|
||||
{ value: 'gpt-5.4-mini', label: 'GPT-5.4 Mini', badgeLabel: '5.4-mini' },
|
||||
{ value: 'gpt-5.3-codex', label: 'GPT-5.3 Codex', badgeLabel: '5.3-codex' },
|
||||
{
|
||||
value: 'gpt-5.3-codex-spark',
|
||||
label: 'GPT-5.3 Codex Spark',
|
||||
badgeLabel: '5.3-codex-spark',
|
||||
uiDisabledReason: GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON,
|
||||
},
|
||||
{ value: 'gpt-5.2', label: 'GPT-5.2', badgeLabel: '5.2' },
|
||||
{ value: 'gpt-5.2-codex', label: 'GPT-5.2 Codex', badgeLabel: '5.2-codex' },
|
||||
{
|
||||
value: 'gpt-5.1-codex-mini',
|
||||
label: 'GPT-5.1 Codex Mini',
|
||||
badgeLabel: '5.1-codex-mini',
|
||||
uiDisabledReason: GPT_5_1_CODEX_MINI_UI_DISABLED_REASON,
|
||||
},
|
||||
{ value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max', badgeLabel: '5.1-codex-max' },
|
||||
],
|
||||
gemini: [
|
||||
{ value: '', label: 'Default', badgeLabel: 'Default' },
|
||||
{ value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', badgeLabel: '2.5-pro' },
|
||||
{ value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', badgeLabel: '2.5-flash' },
|
||||
{
|
||||
value: 'gemini-2.5-flash-lite',
|
||||
label: 'Gemini 2.5 Flash Lite',
|
||||
badgeLabel: '2.5-flash-lite',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const TEAM_PROVIDER_MODEL_ORDER: Record<SupportedProviderId, Map<string, number>> = {
|
||||
anthropic: new Map(
|
||||
TEAM_PROVIDER_MODEL_OPTIONS.anthropic.map((option, index) => [option.value, index])
|
||||
),
|
||||
codex: new Map(TEAM_PROVIDER_MODEL_OPTIONS.codex.map((option, index) => [option.value, index])),
|
||||
gemini: new Map(TEAM_PROVIDER_MODEL_OPTIONS.gemini.map((option, index) => [option.value, index])),
|
||||
};
|
||||
|
||||
function getKnownTeamProviderModelOption(
|
||||
providerId: SupportedProviderId | undefined,
|
||||
model: string | undefined
|
||||
): TeamProviderModelOption | undefined {
|
||||
const trimmed = model?.trim();
|
||||
if (!providerId || !trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return TEAM_PROVIDER_MODEL_OPTIONS[providerId].find((option) => option.value === trimmed);
|
||||
}
|
||||
|
||||
export function getTeamProviderModelOptions(
|
||||
providerId: SupportedProviderId
|
||||
): readonly TeamProviderModelOption[] {
|
||||
return TEAM_PROVIDER_MODEL_OPTIONS[providerId];
|
||||
}
|
||||
|
||||
export function getTeamProviderLabel(
|
||||
providerId: SupportedProviderId | undefined
|
||||
): string | undefined {
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
return TEAM_PROVIDER_LABELS[providerId];
|
||||
}
|
||||
|
||||
export function getTeamModelLabel(model: string | undefined): string | undefined {
|
||||
const trimmed = model?.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return TEAM_MODEL_LABEL_OVERRIDES[trimmed] ?? trimmed;
|
||||
}
|
||||
|
||||
export function getTeamModelBadgeLabel(
|
||||
providerId: SupportedProviderId,
|
||||
model: string | undefined
|
||||
): string | undefined {
|
||||
const trimmed = model?.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const knownOption = getKnownTeamProviderModelOption(providerId, trimmed);
|
||||
if (knownOption?.badgeLabel) {
|
||||
return knownOption.badgeLabel;
|
||||
}
|
||||
|
||||
if (providerId === 'anthropic') {
|
||||
return trimmed.replace(/^claude-/, '');
|
||||
}
|
||||
if (providerId === 'codex') {
|
||||
return trimmed.replace(/^gpt-/, '');
|
||||
}
|
||||
if (providerId === 'gemini') {
|
||||
return trimmed.replace(/^gemini-/, '');
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function sortTeamProviderModels(
|
||||
providerId: SupportedProviderId,
|
||||
models: readonly string[]
|
||||
): string[] {
|
||||
const seen = new Set<string>();
|
||||
const deduped = models.filter((model) => {
|
||||
const trimmed = model.trim();
|
||||
if (!trimmed || seen.has(trimmed)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(trimmed);
|
||||
return true;
|
||||
});
|
||||
const order = TEAM_PROVIDER_MODEL_ORDER[providerId];
|
||||
|
||||
return [...deduped].sort((left, right) => {
|
||||
const leftRank = order.get(left) ?? Number.MAX_SAFE_INTEGER;
|
||||
const rightRank = order.get(right) ?? Number.MAX_SAFE_INTEGER;
|
||||
if (leftRank !== rightRank) {
|
||||
return leftRank - rightRank;
|
||||
}
|
||||
return left.localeCompare(right);
|
||||
});
|
||||
}
|
||||
|
||||
export function getVisibleTeamProviderModels(
|
||||
providerId: SupportedProviderId,
|
||||
models: readonly string[]
|
||||
): string[] {
|
||||
return sortTeamProviderModels(providerId, models).filter(
|
||||
(model) => !isTeamModelUiDisabled(providerId, model)
|
||||
);
|
||||
}
|
||||
|
||||
export function getTeamModelUiDisabledReason(
|
||||
providerId: SupportedProviderId | undefined,
|
||||
model: string | undefined
|
||||
): string | null {
|
||||
return getKnownTeamProviderModelOption(providerId, model)?.uiDisabledReason ?? null;
|
||||
}
|
||||
|
||||
export function isTeamModelUiDisabled(
|
||||
providerId: SupportedProviderId | undefined,
|
||||
model: string | undefined
|
||||
): boolean {
|
||||
return getTeamModelUiDisabledReason(providerId, model) !== null;
|
||||
}
|
||||
|
||||
export function normalizeTeamModelForUi(
|
||||
providerId: SupportedProviderId | undefined,
|
||||
model: string | undefined
|
||||
): string {
|
||||
return isTeamModelUiDisabled(providerId, model) ? '' : (model ?? '');
|
||||
}
|
||||
|
||||
export function doesTeamModelCarryProviderBrand(
|
||||
providerId: SupportedProviderId | undefined,
|
||||
modelLabel: string | undefined
|
||||
): boolean {
|
||||
const providerLabel = getTeamProviderLabel(providerId);
|
||||
const normalizedProvider = providerLabel?.trim().toLowerCase();
|
||||
const normalizedModel = modelLabel?.trim().toLowerCase();
|
||||
if (!providerId || !normalizedProvider || !normalizedModel || modelLabel === 'Default') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
normalizedModel.startsWith(normalizedProvider) ||
|
||||
(providerId === 'anthropic' && normalizedModel.startsWith('claude')) ||
|
||||
(providerId === 'codex' &&
|
||||
(normalizedModel.startsWith('codex') || normalizedModel.startsWith('gpt'))) ||
|
||||
(providerId === 'gemini' && normalizedModel.startsWith('gemini'))
|
||||
);
|
||||
}
|
||||
|
|
@ -1,44 +1,18 @@
|
|||
import type { TeamProviderId } from '@shared/types';
|
||||
|
||||
const MODEL_LABEL_OVERRIDES: Record<string, string> = {
|
||||
default: 'Default',
|
||||
'claude-sonnet-4-6': 'Sonnet 4.6',
|
||||
'claude-sonnet-4-6[1m]': 'Sonnet 4.6 (1M)',
|
||||
'claude-opus-4-6': 'Opus 4.6',
|
||||
'claude-opus-4-6[1m]': 'Opus 4.6 (1M)',
|
||||
'claude-haiku-4-5-20251001': 'Haiku 4.5',
|
||||
'gpt-5.4': 'GPT-5.4',
|
||||
'gpt-5.4-mini': 'GPT-5.4 Mini',
|
||||
'gpt-5.3-codex': 'GPT-5.3 Codex',
|
||||
'gpt-5.3-codex-spark': 'GPT-5.3 Codex Spark',
|
||||
'gpt-5.2': 'GPT-5.2',
|
||||
'gpt-5.2-codex': 'GPT-5.2 Codex',
|
||||
'gpt-5.1-codex-mini': 'GPT-5.1 Codex Mini',
|
||||
'gpt-5.1-codex-max': 'GPT-5.1 Codex Max',
|
||||
'gemini-2.5-pro': 'Gemini 2.5 Pro',
|
||||
'gemini-2.5-flash': 'Gemini 2.5 Flash',
|
||||
'gemini-2.5-flash-lite': 'Gemini 2.5 Flash Lite',
|
||||
};
|
||||
import {
|
||||
doesTeamModelCarryProviderBrand,
|
||||
getTeamModelLabel,
|
||||
getTeamProviderLabel,
|
||||
} from './teamModelCatalog';
|
||||
|
||||
export function getTeamRuntimeModelLabel(model: string | undefined): string | undefined {
|
||||
const trimmed = model?.trim();
|
||||
if (!trimmed) return undefined;
|
||||
return MODEL_LABEL_OVERRIDES[trimmed] ?? trimmed;
|
||||
return getTeamModelLabel(model);
|
||||
}
|
||||
|
||||
export function getTeamRuntimeProviderLabel(
|
||||
providerId: TeamProviderId | undefined
|
||||
): string | undefined {
|
||||
switch (providerId) {
|
||||
case 'codex':
|
||||
return 'Codex';
|
||||
case 'gemini':
|
||||
return 'Gemini';
|
||||
case 'anthropic':
|
||||
return 'Anthropic';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
return getTeamProviderLabel(providerId);
|
||||
}
|
||||
|
||||
export function getTeamRuntimeEffortLabel(effort: string | undefined): string | undefined {
|
||||
|
|
@ -60,17 +34,7 @@ export function formatTeamRuntimeSummary(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const normalizedProvider = providerLabel?.trim().toLowerCase();
|
||||
const normalizedModel = modelLabel?.trim().toLowerCase();
|
||||
const modelAlreadyCarriesProviderBrand =
|
||||
Boolean(modelLabel) &&
|
||||
Boolean(normalizedProvider) &&
|
||||
Boolean(normalizedModel) &&
|
||||
(normalizedModel!.startsWith(normalizedProvider!) ||
|
||||
(providerId === 'anthropic' && normalizedModel!.startsWith('claude')) ||
|
||||
(providerId === 'codex' &&
|
||||
(normalizedModel!.startsWith('codex') || normalizedModel!.startsWith('gpt'))) ||
|
||||
(providerId === 'gemini' && normalizedModel!.startsWith('gemini')));
|
||||
const modelAlreadyCarriesProviderBrand = doesTeamModelCarryProviderBrand(providerId, modelLabel);
|
||||
|
||||
const providerActsAsBackendOnly =
|
||||
providerId !== 'anthropic' && Boolean(modelLabel) && !modelAlreadyCarriesProviderBrand;
|
||||
|
|
|
|||
|
|
@ -171,7 +171,8 @@ describe('TeamModelSelector disabled Codex models', () => {
|
|||
});
|
||||
|
||||
expect(host.textContent).toContain('OpenCode');
|
||||
expect(host.textContent?.match(/In development/g)?.length ?? 0).toBeGreaterThanOrEqual(2);
|
||||
expect(host.textContent).not.toContain('Gemini in development');
|
||||
expect(host.textContent?.match(/In development/g)?.length ?? 0).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const buttons = Array.from(host.querySelectorAll('button'));
|
||||
const openCodeButton = buttons.find((button) => button.textContent?.includes('OpenCode'));
|
||||
|
|
|
|||
27
test/renderer/utils/teamModelCatalog.test.ts
Normal file
27
test/renderer/utils/teamModelCatalog.test.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getVisibleTeamProviderModels } from '@renderer/utils/teamModelCatalog';
|
||||
|
||||
describe('teamModelCatalog', () => {
|
||||
it('filters UI-disabled Codex models from provider badge lists', () => {
|
||||
expect(
|
||||
getVisibleTeamProviderModels('codex', [
|
||||
'gpt-5.4',
|
||||
'gpt-5.4-mini',
|
||||
'gpt-5.3-codex',
|
||||
'gpt-5.3-codex-spark',
|
||||
'gpt-5.2',
|
||||
'gpt-5.2-codex',
|
||||
'gpt-5.1-codex-mini',
|
||||
'gpt-5.1-codex-max',
|
||||
])
|
||||
).toEqual([
|
||||
'gpt-5.4',
|
||||
'gpt-5.4-mini',
|
||||
'gpt-5.3-codex',
|
||||
'gpt-5.2',
|
||||
'gpt-5.2-codex',
|
||||
'gpt-5.1-codex-max',
|
||||
]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue