mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 20:15:40 +00:00
feat(ui): add fable tier row in model mapping
This commit is contained in:
1 parent
30b54a0966
commit
b27a007229
10 files changed
+108
-2
No files matched your search
@@ -9,6 +9,7 @@ export const ANTHROPIC_MODEL_ENV_KEYS = [
|
|||||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||||
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
||||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||||
|
'ANTHROPIC_DEFAULT_FABLE_MODEL',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type AnthropicModelEnvKey = (typeof ANTHROPIC_MODEL_ENV_KEYS)[number];
|
export type AnthropicModelEnvKey = (typeof ANTHROPIC_MODEL_ENV_KEYS)[number];
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const MODEL_ENV_KEYS = [
|
|||||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||||
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
||||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||||
|
'ANTHROPIC_DEFAULT_FABLE_MODEL',
|
||||||
] as const;
|
] as const;
|
||||||
const PRESET_MODEL_KEYS = ['default', 'opus', 'sonnet', 'haiku'] as const;
|
const PRESET_MODEL_KEYS = ['default', 'opus', 'sonnet', 'haiku'] as const;
|
||||||
const SETTINGS_IDENTIFIER_PATTERN = /^[a-zA-Z][a-zA-Z0-9._-]*$/;
|
const SETTINGS_IDENTIFIER_PATTERN = /^[a-zA-Z][a-zA-Z0-9._-]*$/;
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ANTHROPIC_MODEL_ENV_KEYS,
|
||||||
|
isAnthropicModelEnvKey,
|
||||||
|
} from '../../../src/shared/extended-context-utils';
|
||||||
|
|
||||||
|
describe('Anthropic model env keys', () => {
|
||||||
|
it('includes all five tier mapping keys', () => {
|
||||||
|
expect([...ANTHROPIC_MODEL_ENV_KEYS]).toEqual([
|
||||||
|
'ANTHROPIC_MODEL',
|
||||||
|
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||||
|
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
||||||
|
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||||
|
'ANTHROPIC_DEFAULT_FABLE_MODEL',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recognizes the fable tier key like the other tiers', () => {
|
||||||
|
expect(isAnthropicModelEnvKey('ANTHROPIC_DEFAULT_FABLE_MODEL')).toBe(true);
|
||||||
|
expect(isAnthropicModelEnvKey('ANTHROPIC_DEFAULT_HAIKU_MODEL')).toBe(true);
|
||||||
|
expect(isAnthropicModelEnvKey('ANTHROPIC_BASE_URL')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -122,6 +122,7 @@ export function ProviderEditor({
|
|||||||
opusModel,
|
opusModel,
|
||||||
sonnetModel,
|
sonnetModel,
|
||||||
haikuModel,
|
haikuModel,
|
||||||
|
fableModel,
|
||||||
extendedContextEnabled,
|
extendedContextEnabled,
|
||||||
toggleExtendedContext,
|
toggleExtendedContext,
|
||||||
handleRawJsonChange,
|
handleRawJsonChange,
|
||||||
@@ -271,6 +272,7 @@ export function ProviderEditor({
|
|||||||
opusModel={opusModel}
|
opusModel={opusModel}
|
||||||
sonnetModel={sonnetModel}
|
sonnetModel={sonnetModel}
|
||||||
haikuModel={haikuModel}
|
haikuModel={haikuModel}
|
||||||
|
fableModel={fableModel}
|
||||||
providerModels={providerModels}
|
providerModels={providerModels}
|
||||||
routing={routing}
|
routing={routing}
|
||||||
extendedContextEnabled={extendedContextEnabled}
|
extendedContextEnabled={extendedContextEnabled}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export function ModelConfigSection({
|
|||||||
opusModel,
|
opusModel,
|
||||||
sonnetModel,
|
sonnetModel,
|
||||||
haikuModel,
|
haikuModel,
|
||||||
|
fableModel,
|
||||||
providerModels,
|
providerModels,
|
||||||
routing,
|
routing,
|
||||||
provider,
|
provider,
|
||||||
@@ -67,7 +68,7 @@ export function ModelConfigSection({
|
|||||||
const extendedContextModels = useMemo(() => {
|
const extendedContextModels = useMemo(() => {
|
||||||
if (!catalog) return [];
|
if (!catalog) return [];
|
||||||
|
|
||||||
const selectedModels = [currentModel, opusModel, sonnetModel, haikuModel]
|
const selectedModels = [currentModel, opusModel, sonnetModel, haikuModel, fableModel]
|
||||||
.filter((modelId): modelId is string => Boolean(modelId))
|
.filter((modelId): modelId is string => Boolean(modelId))
|
||||||
.map((modelId) => stripExtendedContextSuffix(modelId));
|
.map((modelId) => stripExtendedContextSuffix(modelId));
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ export function ModelConfigSection({
|
|||||||
return uniqueIds
|
return uniqueIds
|
||||||
.map((modelId) => findCatalogModel(catalog.provider, modelId, catalog))
|
.map((modelId) => findCatalogModel(catalog.provider, modelId, catalog))
|
||||||
.filter((model): model is NonNullable<typeof model> => Boolean(model?.extendedContext));
|
.filter((model): model is NonNullable<typeof model> => Boolean(model?.extendedContext));
|
||||||
}, [catalog, currentModel, opusModel, sonnetModel, haikuModel]);
|
}, [catalog, currentModel, opusModel, sonnetModel, haikuModel, fableModel]);
|
||||||
|
|
||||||
const resolvedCatalogModels = useMemo(
|
const resolvedCatalogModels = useMemo(
|
||||||
() => getResolvedCatalogModels(catalog, providerModels),
|
() => getResolvedCatalogModels(catalog, providerModels),
|
||||||
@@ -253,6 +254,15 @@ export function ModelConfigSection({
|
|||||||
onToggle={onExtendedContextToggle}
|
onToggle={onExtendedContextToggle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<FlexibleModelSelector
|
||||||
|
label="Fable (Frontier)"
|
||||||
|
description="Maps the fable subagent tier; leave empty to keep the native Claude Fable model"
|
||||||
|
value={fableModel}
|
||||||
|
onChange={(model) => onUpdateEnvValue('ANTHROPIC_DEFAULT_FABLE_MODEL', model)}
|
||||||
|
catalog={catalog}
|
||||||
|
allModels={providerModels}
|
||||||
|
routing={routing}
|
||||||
|
/>
|
||||||
<FlexibleModelSelector
|
<FlexibleModelSelector
|
||||||
label="Opus (Most capable)"
|
label="Opus (Most capable)"
|
||||||
description="For complex reasoning tasks"
|
description="For complex reasoning tasks"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ interface ModelConfigTabProps {
|
|||||||
opusModel?: string;
|
opusModel?: string;
|
||||||
sonnetModel?: string;
|
sonnetModel?: string;
|
||||||
haikuModel?: string;
|
haikuModel?: string;
|
||||||
|
fableModel?: string;
|
||||||
providerModels: Array<{ id: string; owned_by: string }>;
|
providerModels: Array<{ id: string; owned_by: string }>;
|
||||||
routing?: CliproxyProviderRoutingHints;
|
routing?: CliproxyProviderRoutingHints;
|
||||||
/** Whether extended context (1M tokens) is enabled */
|
/** Whether extended context (1M tokens) is enabled */
|
||||||
@@ -72,6 +73,7 @@ export function ModelConfigTab({
|
|||||||
opusModel,
|
opusModel,
|
||||||
sonnetModel,
|
sonnetModel,
|
||||||
haikuModel,
|
haikuModel,
|
||||||
|
fableModel,
|
||||||
providerModels,
|
providerModels,
|
||||||
routing,
|
routing,
|
||||||
extendedContextEnabled,
|
extendedContextEnabled,
|
||||||
@@ -163,6 +165,7 @@ export function ModelConfigTab({
|
|||||||
opusModel={opusModel}
|
opusModel={opusModel}
|
||||||
sonnetModel={sonnetModel}
|
sonnetModel={sonnetModel}
|
||||||
haikuModel={haikuModel}
|
haikuModel={haikuModel}
|
||||||
|
fableModel={fableModel}
|
||||||
providerModels={providerModels}
|
providerModels={providerModels}
|
||||||
routing={routing}
|
routing={routing}
|
||||||
provider={provider}
|
provider={provider}
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ export interface ModelConfigSectionProps {
|
|||||||
opusModel?: string;
|
opusModel?: string;
|
||||||
sonnetModel?: string;
|
sonnetModel?: string;
|
||||||
haikuModel?: string;
|
haikuModel?: string;
|
||||||
|
fableModel?: string;
|
||||||
providerModels: Array<{ id: string; owned_by: string }>;
|
providerModels: Array<{ id: string; owned_by: string }>;
|
||||||
routing?: CliproxyProviderRoutingHints;
|
routing?: CliproxyProviderRoutingHints;
|
||||||
/** Provider name for display */
|
/** Provider name for display */
|
||||||
@@ -154,6 +155,7 @@ export interface UseProviderEditorReturn {
|
|||||||
opusModel?: string;
|
opusModel?: string;
|
||||||
sonnetModel?: string;
|
sonnetModel?: string;
|
||||||
haikuModel?: string;
|
haikuModel?: string;
|
||||||
|
fableModel?: string;
|
||||||
/** Whether extended context (1M tokens) is enabled */
|
/** Whether extended context (1M tokens) is enabled */
|
||||||
extendedContextEnabled: boolean;
|
extendedContextEnabled: boolean;
|
||||||
/** Toggle extended context on/off */
|
/** Toggle extended context on/off */
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export function useProviderEditor(
|
|||||||
const opusModel = currentSettings?.env?.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
const opusModel = currentSettings?.env?.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
||||||
const sonnetModel = currentSettings?.env?.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
const sonnetModel = currentSettings?.env?.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
||||||
const haikuModel = currentSettings?.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
const haikuModel = currentSettings?.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
||||||
|
const fableModel = currentSettings?.env?.ANTHROPIC_DEFAULT_FABLE_MODEL;
|
||||||
|
|
||||||
// Extended context is enabled if any model has [1m] suffix
|
// Extended context is enabled if any model has [1m] suffix
|
||||||
const extendedContextEnabled = useMemo(() => {
|
const extendedContextEnabled = useMemo(() => {
|
||||||
@@ -224,6 +225,7 @@ export function useProviderEditor(
|
|||||||
opusModel,
|
opusModel,
|
||||||
sonnetModel,
|
sonnetModel,
|
||||||
haikuModel,
|
haikuModel,
|
||||||
|
fableModel,
|
||||||
extendedContextEnabled,
|
extendedContextEnabled,
|
||||||
toggleExtendedContext,
|
toggleExtendedContext,
|
||||||
handleRawJsonChange,
|
handleRawJsonChange,
|
||||||
|
|||||||
@@ -13,6 +13,29 @@ import { ModelConfigSection } from '@/components/cliproxy/provider-editor/model-
|
|||||||
import { MODEL_CATALOGS } from '@/lib/model-catalogs';
|
import { MODEL_CATALOGS } from '@/lib/model-catalogs';
|
||||||
|
|
||||||
describe('ModelConfigSection presets', () => {
|
describe('ModelConfigSection presets', () => {
|
||||||
|
it('renders five tier selectors including the fable row', () => {
|
||||||
|
render(
|
||||||
|
<ModelConfigSection
|
||||||
|
catalog={MODEL_CATALOGS.codex}
|
||||||
|
savedPresets={[]}
|
||||||
|
currentModel="gpt-5-codex"
|
||||||
|
opusModel="gpt-5-codex"
|
||||||
|
sonnetModel="gpt-5-codex"
|
||||||
|
haikuModel="gpt-5-codex-mini"
|
||||||
|
fableModel="gpt-5.6-sol-xhigh"
|
||||||
|
providerModels={[]}
|
||||||
|
provider="codex"
|
||||||
|
onExtendedContextToggle={vi.fn()}
|
||||||
|
onApplyPreset={vi.fn()}
|
||||||
|
onUpdateEnvValue={vi.fn()}
|
||||||
|
onOpenCustomPreset={vi.fn()}
|
||||||
|
onDeletePreset={vi.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getAllByTestId('flexible-model-selector')).toHaveLength(5);
|
||||||
|
});
|
||||||
|
|
||||||
it('groups codex presets by free and paid tiers', async () => {
|
it('groups codex presets by free and paid tiers', async () => {
|
||||||
const onApplyPreset = vi.fn();
|
const onApplyPreset = vi.fn();
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,44 @@ describe('useProviderEditor', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('derives fableModel and round-trips ANTHROPIC_DEFAULT_FABLE_MODEL updates', async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn((input: RequestInfo | URL) => {
|
||||||
|
const url = String(input);
|
||||||
|
|
||||||
|
if (url.includes('/api/settings/codex/raw')) {
|
||||||
|
return Promise.resolve(
|
||||||
|
createJsonResponse({
|
||||||
|
profile: 'codex',
|
||||||
|
settings: {
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_MODEL: 'gpt-5.3-codex-high',
|
||||||
|
ANTHROPIC_DEFAULT_FABLE_MODEL: 'gpt-5.6-sol-xhigh',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mtime: 1,
|
||||||
|
path: '~/.ccs/codex.settings.json',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useProviderEditor('codex'), { wrapper });
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.fableModel).toBe('gpt-5.6-sol-xhigh'));
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateEnvValue('ANTHROPIC_DEFAULT_FABLE_MODEL', 'gpt-5.6-sol-max');
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextSettings = JSON.parse(result.current.rawJsonContent);
|
||||||
|
expect(nextSettings.env.ANTHROPIC_DEFAULT_FABLE_MODEL).toBe('gpt-5.6-sol-max');
|
||||||
|
});
|
||||||
|
|
||||||
it('preserves explicit codex effort suffixes in editor state updates', async () => {
|
it('preserves explicit codex effort suffixes in editor state updates', async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
'fetch',
|
'fetch',
|
||||||
|
|||||||
Reference in new issue
Block a user