mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 12:14:19 +00:00
feat(ui): extend reasoning control to xai grok models
This commit is contained in:
1 parent
e81fd80944
commit
30b54a0966
4 files changed
+260
-36
No files matched your search
@@ -21,19 +21,13 @@ import { Skeleton } from '@/components/ui/skeleton';
|
|||||||
import type { CliproxyProviderRoutingHints } from '@/lib/api-client';
|
import type { CliproxyProviderRoutingHints } from '@/lib/api-client';
|
||||||
import type { CodexEffort, CodexServiceTier } from '@/lib/codex-effort';
|
import type { CodexEffort, CodexServiceTier } from '@/lib/codex-effort';
|
||||||
import {
|
import {
|
||||||
applyCodexEffortSuffix,
|
|
||||||
getCodexEffortDisplay,
|
getCodexEffortDisplay,
|
||||||
getCodexEffortVariants,
|
getCodexEffortVariants,
|
||||||
getSelectableCodexEfforts,
|
|
||||||
parseCodexEffort,
|
parseCodexEffort,
|
||||||
parseCodexServiceTier,
|
parseCodexServiceTier,
|
||||||
stripCodexEffortSuffix,
|
|
||||||
} from '@/lib/codex-effort';
|
} from '@/lib/codex-effort';
|
||||||
import {
|
import { getResolvedCatalogModels, getSupplementalCatalogModels } from '@/lib/model-catalogs';
|
||||||
findCatalogModel,
|
import { getReasoningAdapter } from '@/lib/reasoning-control';
|
||||||
getResolvedCatalogModels,
|
|
||||||
getSupplementalCatalogModels,
|
|
||||||
} from '@/lib/model-catalogs';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
/** Model entry from catalog */
|
/** Model entry from catalog */
|
||||||
@@ -61,6 +55,8 @@ export interface ModelEntry {
|
|||||||
codexEfforts?: CodexEffort[];
|
codexEfforts?: CodexEffort[];
|
||||||
/** Additional Codex service-tier suffixes supported by this model in the dashboard UI. */
|
/** Additional Codex service-tier suffixes supported by this model in the dashboard UI. */
|
||||||
codexServiceTiers?: CodexServiceTier[];
|
codexServiceTiers?: CodexServiceTier[];
|
||||||
|
/** Reasoning-level suffixes this model supports via paren syntax in the dashboard UI. */
|
||||||
|
reasoningLevels?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provider catalog */
|
/** Provider catalog */
|
||||||
@@ -575,22 +571,21 @@ export function FlexibleModelSelector({
|
|||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
const hasAvailableModels = recommendedOptions.length + allModelOptions.length > 0;
|
const hasAvailableModels = recommendedOptions.length + allModelOptions.length > 0;
|
||||||
const currentEffort = isCodexProvider ? parseCodexEffort(value) : undefined;
|
const reasoningAdapter = getReasoningAdapter(catalog?.provider);
|
||||||
const baseModelId = isCodexProvider
|
const currentReasoningLevel = reasoningAdapter?.parse(value);
|
||||||
? stripCodexEffortSuffix(normalizeModelValue(value, routing))
|
const selectableReasoningLevels =
|
||||||
: '';
|
reasoningAdapter && catalog
|
||||||
const selectedCatalogModel =
|
? reasoningAdapter.getOptions(
|
||||||
isCodexProvider && baseModelId && catalog
|
reasoningAdapter.strip(normalizeModelValue(value, routing)),
|
||||||
? findCatalogModel(catalog.provider, baseModelId, catalog)
|
catalog
|
||||||
: undefined;
|
)
|
||||||
const selectableEfforts = getSelectableCodexEfforts(
|
: null;
|
||||||
selectedCatalogModel?.codexMaxEffort,
|
const reasoningOptions =
|
||||||
selectedCatalogModel?.codexEfforts
|
selectableReasoningLevels &&
|
||||||
);
|
currentReasoningLevel &&
|
||||||
const effortOptions =
|
!selectableReasoningLevels.includes(currentReasoningLevel)
|
||||||
currentEffort && !selectableEfforts.includes(currentEffort)
|
? [...selectableReasoningLevels, currentReasoningLevel]
|
||||||
? [...selectableEfforts, currentEffort]
|
: selectableReasoningLevels;
|
||||||
: selectableEfforts;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
@@ -683,16 +678,11 @@ export function FlexibleModelSelector({
|
|||||||
...allModelOptions,
|
...allModelOptions,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{isCodexProvider && (
|
{reasoningAdapter && reasoningOptions && (
|
||||||
<Select
|
<Select
|
||||||
value={currentEffort ?? 'auto'}
|
value={currentReasoningLevel ?? 'auto'}
|
||||||
onValueChange={(effort) =>
|
onValueChange={(level) =>
|
||||||
onChange(
|
onChange(reasoningAdapter.apply(value, level === 'auto' ? undefined : level))
|
||||||
applyCodexEffortSuffix(
|
|
||||||
value,
|
|
||||||
effort === 'auto' ? undefined : (effort as CodexEffort)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
disabled={disabled || !value}
|
disabled={disabled || !value}
|
||||||
>
|
>
|
||||||
@@ -704,9 +694,9 @@ export function FlexibleModelSelector({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="auto">{t('providerModelSelector.effortAuto')}</SelectItem>
|
<SelectItem value="auto">{t('providerModelSelector.effortAuto')}</SelectItem>
|
||||||
{effortOptions.map((effort) => (
|
{reasoningOptions.map((level) => (
|
||||||
<SelectItem key={effort} value={effort}>
|
<SelectItem key={level} value={level}>
|
||||||
{effort}
|
{level}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -402,11 +402,13 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
|||||||
id: 'grok-4.5',
|
id: 'grok-4.5',
|
||||||
name: 'Grok 4.5',
|
name: 'Grok 4.5',
|
||||||
description: 'Frontier model for coding, engineering, and agentic workflows',
|
description: 'Frontier model for coding, engineering, and agentic workflows',
|
||||||
|
reasoningLevels: ['low', 'medium', 'high'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'grok-4.3',
|
id: 'grok-4.3',
|
||||||
name: 'Grok 4.3',
|
name: 'Grok 4.3',
|
||||||
description: 'General-purpose Grok model with a one-million-token context window',
|
description: 'General-purpose Grok model with a one-million-token context window',
|
||||||
|
reasoningLevels: ['none', 'low', 'medium', 'high'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'grok-4.20-0309-reasoning',
|
id: 'grok-4.20-0309-reasoning',
|
||||||
@@ -1147,6 +1149,7 @@ export function buildUiCatalog(
|
|||||||
? undefined
|
? undefined
|
||||||
: (model.extendedContext ?? staticModel?.extendedContext),
|
: (model.extendedContext ?? staticModel?.extendedContext),
|
||||||
presetMapping: staticModel?.presetMapping,
|
presetMapping: staticModel?.presetMapping,
|
||||||
|
reasoningLevels: model.reasoningLevels ?? staticModel?.reasoningLevels,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import type { ProviderCatalog } from '@/components/cliproxy/provider-model-selector';
|
||||||
|
import {
|
||||||
|
applyCodexEffortSuffix,
|
||||||
|
getSelectableCodexEfforts,
|
||||||
|
parseCodexEffort,
|
||||||
|
stripCodexEffortSuffix,
|
||||||
|
type CodexEffort,
|
||||||
|
} from '@/lib/codex-effort';
|
||||||
|
import { findCatalogModel } from '@/lib/model-catalogs';
|
||||||
|
|
||||||
|
export interface ReasoningAdapter {
|
||||||
|
parse(value: string | undefined): string | undefined;
|
||||||
|
strip(value: string | undefined): string;
|
||||||
|
apply(value: string | undefined, level?: string): string;
|
||||||
|
getOptions(baseId: string, catalog: ProviderCatalog): string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const XAI_LEVEL_SUFFIX_REGEX = /\(([a-z]+)\)$/i;
|
||||||
|
|
||||||
|
export function parseXaiReasoningLevel(modelId: string | undefined): string | undefined {
|
||||||
|
const match = modelId?.trim().match(XAI_LEVEL_SUFFIX_REGEX);
|
||||||
|
return match?.[1]?.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripXaiReasoningSuffix(modelId: string | undefined): string {
|
||||||
|
return modelId?.trim().replace(XAI_LEVEL_SUFFIX_REGEX, '') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyXaiReasoningSuffix(modelId: string | undefined, level?: string): string {
|
||||||
|
const baseModelId = stripXaiReasoningSuffix(modelId);
|
||||||
|
if (!baseModelId || !level) return baseModelId;
|
||||||
|
return `${baseModelId}(${level})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const codexAdapter: ReasoningAdapter = {
|
||||||
|
parse: (value) => parseCodexEffort(value),
|
||||||
|
strip: (value) => stripCodexEffortSuffix(value),
|
||||||
|
apply: (value, level) => applyCodexEffortSuffix(value, level as CodexEffort | undefined),
|
||||||
|
getOptions: (baseId, catalog) => {
|
||||||
|
const model = baseId ? findCatalogModel(catalog.provider, baseId, catalog) : undefined;
|
||||||
|
return getSelectableCodexEfforts(model?.codexMaxEffort, model?.codexEfforts);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const xaiAdapter: ReasoningAdapter = {
|
||||||
|
parse: parseXaiReasoningLevel,
|
||||||
|
strip: stripXaiReasoningSuffix,
|
||||||
|
apply: applyXaiReasoningSuffix,
|
||||||
|
getOptions: (baseId, catalog) => {
|
||||||
|
if (!baseId) return null;
|
||||||
|
const levels = findCatalogModel(catalog.provider, baseId, catalog)?.reasoningLevels;
|
||||||
|
return levels?.length ? [...levels] : null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const REASONING_ADAPTERS: Record<string, ReasoningAdapter> = {
|
||||||
|
codex: codexAdapter,
|
||||||
|
xai: xaiAdapter,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getReasoningAdapter(provider: string | undefined): ReasoningAdapter | undefined {
|
||||||
|
return provider ? REASONING_ADAPTERS[provider.toLowerCase()] : undefined;
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import type { ProviderCatalog } from '@/components/cliproxy/provider-model-selector';
|
||||||
|
import {
|
||||||
|
applyXaiReasoningSuffix,
|
||||||
|
getReasoningAdapter,
|
||||||
|
parseXaiReasoningLevel,
|
||||||
|
stripXaiReasoningSuffix,
|
||||||
|
} from '@/lib/reasoning-control';
|
||||||
|
|
||||||
|
const xaiCatalog: ProviderCatalog = {
|
||||||
|
provider: 'xai',
|
||||||
|
displayName: 'xAI (Grok)',
|
||||||
|
defaultModel: 'grok-build-0.1',
|
||||||
|
models: [
|
||||||
|
{ id: 'grok-build-0.1', name: 'Grok Build 0.1' },
|
||||||
|
{ id: 'grok-4.5', name: 'Grok 4.5', reasoningLevels: ['low', 'medium', 'high'] },
|
||||||
|
{ id: 'grok-4.3', name: 'Grok 4.3', reasoningLevels: ['none', 'low', 'medium', 'high'] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const codexCatalog: ProviderCatalog = {
|
||||||
|
provider: 'codex',
|
||||||
|
displayName: 'Codex',
|
||||||
|
defaultModel: 'gpt-5.3-codex',
|
||||||
|
models: [
|
||||||
|
{ id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex', codexMaxEffort: 'xhigh' },
|
||||||
|
{ id: 'gpt-5.4-mini', name: 'GPT-5.4 Mini', codexMaxEffort: 'high' },
|
||||||
|
{
|
||||||
|
id: 'gpt-5.6-sol',
|
||||||
|
name: 'GPT-5.6 Sol',
|
||||||
|
codexEfforts: ['low', 'medium', 'high', 'xhigh'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('xai paren codec', () => {
|
||||||
|
it('parses paren level suffixes', () => {
|
||||||
|
expect(parseXaiReasoningLevel('grok-4.5(high)')).toBe('high');
|
||||||
|
expect(parseXaiReasoningLevel('grok-4.3(none)')).toBe('none');
|
||||||
|
expect(parseXaiReasoningLevel('grok-4.5(HIGH)')).toBe('high');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined for unsuffixed or non-level values', () => {
|
||||||
|
expect(parseXaiReasoningLevel('grok-4.5')).toBeUndefined();
|
||||||
|
expect(parseXaiReasoningLevel('grok-4(1m)')).toBeUndefined();
|
||||||
|
expect(parseXaiReasoningLevel(undefined)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only matches the suffix at end-of-string', () => {
|
||||||
|
expect(parseXaiReasoningLevel('grok(high)-fast')).toBeUndefined();
|
||||||
|
expect(stripXaiReasoningSuffix('grok(high)-fast')).toBe('grok(high)-fast');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips paren suffixes for catalog lookup', () => {
|
||||||
|
expect(stripXaiReasoningSuffix('grok-4.5(high)')).toBe('grok-4.5');
|
||||||
|
expect(stripXaiReasoningSuffix('grok-4.5')).toBe('grok-4.5');
|
||||||
|
expect(stripXaiReasoningSuffix(undefined)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies and replaces paren suffixes', () => {
|
||||||
|
expect(applyXaiReasoningSuffix('grok-4.5', 'high')).toBe('grok-4.5(high)');
|
||||||
|
expect(applyXaiReasoningSuffix('grok-4.5(high)', 'low')).toBe('grok-4.5(low)');
|
||||||
|
expect(applyXaiReasoningSuffix('grok-4.3', 'none')).toBe('grok-4.3(none)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips the suffix when no level is given', () => {
|
||||||
|
expect(applyXaiReasoningSuffix('grok-4.5(high)', undefined)).toBe('grok-4.5');
|
||||||
|
expect(applyXaiReasoningSuffix('grok-4.3(none)', undefined)).toBe('grok-4.3');
|
||||||
|
expect(applyXaiReasoningSuffix(undefined, 'high')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('round-trips through parse and apply', () => {
|
||||||
|
const stored = applyXaiReasoningSuffix('grok-4.5', 'high');
|
||||||
|
expect(parseXaiReasoningLevel(stored)).toBe('high');
|
||||||
|
expect(applyXaiReasoningSuffix(stored, parseXaiReasoningLevel(stored))).toBe(stored);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getReasoningAdapter', () => {
|
||||||
|
it('returns adapters for codex and xai only', () => {
|
||||||
|
expect(getReasoningAdapter('codex')).toBeDefined();
|
||||||
|
expect(getReasoningAdapter('xai')).toBeDefined();
|
||||||
|
expect(getReasoningAdapter('XAI')).toBeDefined();
|
||||||
|
expect(getReasoningAdapter('gemini')).toBeUndefined();
|
||||||
|
expect(getReasoningAdapter(undefined)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function requireAdapter(provider: string) {
|
||||||
|
const adapter = getReasoningAdapter(provider);
|
||||||
|
if (!adapter) throw new Error(`missing reasoning adapter for ${provider}`);
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('xai adapter', () => {
|
||||||
|
const adapter = requireAdapter('xai');
|
||||||
|
|
||||||
|
it('returns per-model levels from catalog metadata', () => {
|
||||||
|
expect(adapter.getOptions('grok-4.5', xaiCatalog)).toEqual(['low', 'medium', 'high']);
|
||||||
|
expect(adapter.getOptions('grok-4.3', xaiCatalog)).toEqual(['none', 'low', 'medium', 'high']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides the control for models without levels metadata', () => {
|
||||||
|
expect(adapter.getOptions('grok-build-0.1', xaiCatalog)).toBeNull();
|
||||||
|
expect(adapter.getOptions('grok-unknown-model', xaiCatalog)).toBeNull();
|
||||||
|
expect(adapter.getOptions('', xaiCatalog)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes and clears suffixed values through apply', () => {
|
||||||
|
expect(adapter.apply('grok-4.5', 'high')).toBe('grok-4.5(high)');
|
||||||
|
expect(adapter.apply('grok-4.5(high)', undefined)).toBe('grok-4.5');
|
||||||
|
expect(adapter.apply('grok-4.3', 'none')).toBe('grok-4.3(none)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pre-selects stored suffixed values', () => {
|
||||||
|
expect(adapter.parse('grok-4.5(high)')).toBe('high');
|
||||||
|
expect(adapter.strip('grok-4.5(high)')).toBe('grok-4.5');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('codex adapter', () => {
|
||||||
|
const adapter = requireAdapter('codex');
|
||||||
|
|
||||||
|
it('delegates parse and strip to codex-effort helpers', () => {
|
||||||
|
expect(adapter.parse('gpt-5.3-codex-high')).toBe('high');
|
||||||
|
expect(adapter.parse('gpt-5.4-high-fast')).toBe('high');
|
||||||
|
expect(adapter.parse('gpt-5.3-codex')).toBeUndefined();
|
||||||
|
expect(adapter.strip('gpt-5.4-fast-high')).toBe('gpt-5.4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies dash suffixes preserving fast tier and routing prefix', () => {
|
||||||
|
expect(adapter.apply('gpt-5.4-high-fast', 'low')).toBe('gpt-5.4-low-fast');
|
||||||
|
expect(adapter.apply('gpt-5.4-high-fast', undefined)).toBe('gpt-5.4-fast');
|
||||||
|
expect(adapter.apply('codex/gpt-5.6-sol', 'high')).toBe('codex/gpt-5.6-sol-high');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns catalog-driven effort options', () => {
|
||||||
|
expect(adapter.getOptions('gpt-5.4-mini', codexCatalog)).toEqual([
|
||||||
|
'minimal',
|
||||||
|
'low',
|
||||||
|
'medium',
|
||||||
|
'high',
|
||||||
|
]);
|
||||||
|
expect(adapter.getOptions('gpt-5.6-sol', codexCatalog)).toEqual([
|
||||||
|
'low',
|
||||||
|
'medium',
|
||||||
|
'high',
|
||||||
|
'xhigh',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to all five efforts for custom or unselected models', () => {
|
||||||
|
expect(adapter.getOptions('my-org/custom-model', codexCatalog)).toEqual([
|
||||||
|
'minimal',
|
||||||
|
'low',
|
||||||
|
'medium',
|
||||||
|
'high',
|
||||||
|
'xhigh',
|
||||||
|
]);
|
||||||
|
expect(adapter.getOptions('', codexCatalog)).toEqual([
|
||||||
|
'minimal',
|
||||||
|
'low',
|
||||||
|
'medium',
|
||||||
|
'high',
|
||||||
|
'xhigh',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user