mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 18:18:21 +00:00
fix(cliproxy): preserve explicit Claude long-context intent
- centralize Anthropic model-key [1m] handling in shared helpers - keep dashboard preset and editor writes consistent across all Claude tiers - add explicit api create and help guidance for --1m/--no-1m Refs #789
This commit is contained in:
1 parent
63b291785c
commit
c05189b4b1
12 files changed
+329
-134
No files matched your search
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* Extended Context Toggle Component
|
||||
* Shows toggle for models that support 1M token context window.
|
||||
* Only visible when selected model has extendedContext: true.
|
||||
* Shows toggle when any selected mapping supports 1M token context.
|
||||
*/
|
||||
|
||||
import { Zap, Info } from 'lucide-react';
|
||||
@@ -12,8 +11,8 @@ import { isNativeGeminiModel } from '@/lib/extended-context-utils';
|
||||
import type { ModelEntry } from './provider-model-selector';
|
||||
|
||||
interface ExtendedContextToggleProps {
|
||||
/** Currently selected model */
|
||||
model: ModelEntry | undefined;
|
||||
/** Compatible selected models */
|
||||
models: ModelEntry[];
|
||||
/** Provider name for display */
|
||||
provider: string;
|
||||
/** Whether extended context is enabled */
|
||||
@@ -27,19 +26,28 @@ interface ExtendedContextToggleProps {
|
||||
}
|
||||
|
||||
export function ExtendedContextToggle({
|
||||
model,
|
||||
provider,
|
||||
models,
|
||||
provider: _provider,
|
||||
enabled,
|
||||
onToggle,
|
||||
disabled,
|
||||
className,
|
||||
}: ExtendedContextToggleProps) {
|
||||
// Only show if model supports extended context
|
||||
if (!model?.extendedContext) {
|
||||
if (models.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAutoEnabled = isNativeGeminiModel(model.id);
|
||||
const hasGeminiModels = models.some((model) => isNativeGeminiModel(model.id));
|
||||
const hasClaudeModels = models.some((model) => !isNativeGeminiModel(model.id));
|
||||
|
||||
let behaviorHint = 'Compatible mappings stay on standard context unless you turn this on.';
|
||||
if (hasGeminiModels && hasClaudeModels) {
|
||||
behaviorHint =
|
||||
'Gemini-compatible mappings can use 1M automatically. Claude mappings stay plain unless you turn this on.';
|
||||
} else if (hasGeminiModels) {
|
||||
behaviorHint =
|
||||
'Gemini-compatible mappings can use 1M by default. Turn this off to keep standard context.';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -65,19 +73,12 @@ export function ExtendedContextToggle({
|
||||
<div className="flex items-start gap-2 text-xs text-muted-foreground">
|
||||
<Info className="w-3.5 h-3.5 mt-0.5 shrink-0" />
|
||||
<div className="space-y-1">
|
||||
<p>Enables 1M token context window instead of default 200K.</p>
|
||||
<p className="text-[10px]">
|
||||
{isAutoEnabled ? (
|
||||
<span className="text-primary">Auto-enabled for {provider} Gemini models</span>
|
||||
) : (
|
||||
<span>Opt-in for {provider} Claude models via --1m flag</span>
|
||||
)}
|
||||
<p>Applies the explicit <code>[1m]</code> long-context suffix to compatible saved mappings.</p>
|
||||
<p className="text-[10px]">{behaviorHint}</p>
|
||||
<p className="text-amber-600 dark:text-amber-500">
|
||||
CCS only saves <code>[1m]</code>. Provider pricing and entitlement are separate, and
|
||||
some accounts can still return 429 extra-usage errors for long-context requests.
|
||||
</p>
|
||||
{enabled && (
|
||||
<p className="text-amber-600 dark:text-amber-500">
|
||||
Note: 2x input pricing applies for tokens beyond 200K
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Sparkles, Zap, Star, X, Plus } from 'lucide-react';
|
||||
import { FlexibleModelSelector } from '../provider-model-selector';
|
||||
import { ExtendedContextToggle } from '../extended-context-toggle';
|
||||
import { stripExtendedContextSuffix } from '@/lib/extended-context-utils';
|
||||
import { findCatalogModel } from '@/lib/model-catalogs';
|
||||
import type { ModelConfigSectionProps } from './types';
|
||||
|
||||
type CatalogPresetModel = NonNullable<ModelConfigSectionProps['catalog']>['models'][number];
|
||||
@@ -48,13 +49,18 @@ export function ModelConfigSection({
|
||||
onDeletePreset,
|
||||
isDeletePending,
|
||||
}: ModelConfigSectionProps) {
|
||||
// Find current model entry to check for extended context support
|
||||
// Strip [1m] suffix when looking up in catalog since catalog IDs don't have suffix
|
||||
const currentModelEntry = useMemo(() => {
|
||||
if (!catalog || !currentModel) return undefined;
|
||||
const baseModelId = stripExtendedContextSuffix(currentModel);
|
||||
return catalog.models.find((m) => m.id === baseModelId);
|
||||
}, [catalog, currentModel]);
|
||||
const extendedContextModels = useMemo(() => {
|
||||
if (!catalog) return [];
|
||||
|
||||
const selectedModels = [currentModel, opusModel, sonnetModel, haikuModel]
|
||||
.filter((modelId): modelId is string => Boolean(modelId))
|
||||
.map((modelId) => stripExtendedContextSuffix(modelId));
|
||||
|
||||
const uniqueIds = [...new Set(selectedModels)];
|
||||
return uniqueIds
|
||||
.map((modelId) => findCatalogModel(catalog.provider, modelId))
|
||||
.filter((model): model is NonNullable<typeof model> => Boolean(model?.extendedContext));
|
||||
}, [catalog, currentModel, opusModel, sonnetModel, haikuModel]);
|
||||
|
||||
const presetGroups = useMemo(() => {
|
||||
const presetModels = (catalog?.models ?? []).filter((model) => model.presetMapping);
|
||||
@@ -199,10 +205,10 @@ export function ModelConfigSection({
|
||||
catalog={catalog}
|
||||
allModels={providerModels}
|
||||
/>
|
||||
{/* Extended Context Toggle - only shows for models that support it */}
|
||||
{currentModelEntry?.extendedContext && onExtendedContextToggle && (
|
||||
{/* Extended Context Toggle - shows when any saved mapping supports it */}
|
||||
{extendedContextModels.length > 0 && onExtendedContextToggle && (
|
||||
<ExtendedContextToggle
|
||||
model={currentModelEntry}
|
||||
models={extendedContextModels}
|
||||
provider={provider}
|
||||
enabled={extendedContextEnabled ?? false}
|
||||
onToggle={onExtendedContextToggle}
|
||||
|
||||
@@ -8,13 +8,11 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import type { SettingsResponse, UseProviderEditorReturn } from './types';
|
||||
import {
|
||||
applyExtendedContextSuffix,
|
||||
stripExtendedContextSuffix,
|
||||
hasExtendedContextSuffix,
|
||||
applyExtendedContextPreferenceToAnthropicModels,
|
||||
hasAnthropicExtendedContextEnabled,
|
||||
isAnthropicModelEnvKey,
|
||||
} from '@/lib/extended-context-utils';
|
||||
|
||||
/** Model env keys that should have [1m] suffix applied */
|
||||
const MODEL_ENV_KEYS = ['ANTHROPIC_MODEL'] as const;
|
||||
import { supportsExtendedContext } from '@/lib/model-catalogs';
|
||||
|
||||
/** Required env vars for CLIProxy providers (informational only - runtime fills defaults) */
|
||||
const REQUIRED_ENV_KEYS = ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN'] as const;
|
||||
@@ -78,56 +76,59 @@ export function useProviderEditor(provider: string): UseProviderEditorReturn {
|
||||
|
||||
// Extended context is enabled if any model has [1m] suffix
|
||||
const extendedContextEnabled = useMemo(() => {
|
||||
const env = currentSettings?.env || {};
|
||||
return MODEL_ENV_KEYS.some((key) => {
|
||||
const value = env[key];
|
||||
return value && hasExtendedContextSuffix(value);
|
||||
});
|
||||
return hasAnthropicExtendedContextEnabled(currentSettings?.env || {});
|
||||
}, [currentSettings]);
|
||||
|
||||
const applySavedLongContextIntent = useCallback(
|
||||
(env: Record<string, string>, enabled: boolean) =>
|
||||
applyExtendedContextPreferenceToAnthropicModels(env, enabled, {
|
||||
supportsExtendedContext: (modelId) => supportsExtendedContext(provider, modelId),
|
||||
}),
|
||||
[provider]
|
||||
);
|
||||
|
||||
// Update a single setting value
|
||||
const updateEnvValue = useCallback(
|
||||
(key: string, value: string) => {
|
||||
const newEnv = { ...(currentSettings?.env || {}), [key]: value };
|
||||
const newSettings = { ...currentSettings, env: newEnv };
|
||||
const envWithIntent = isAnthropicModelEnvKey(key)
|
||||
? applySavedLongContextIntent(newEnv, extendedContextEnabled)
|
||||
: newEnv;
|
||||
delete envWithIntent['CCS_EXTENDED_CONTEXT'];
|
||||
|
||||
const newSettings = { ...currentSettings, env: envWithIntent };
|
||||
setRawJsonEdits(JSON.stringify(newSettings, null, 2));
|
||||
},
|
||||
[currentSettings]
|
||||
[applySavedLongContextIntent, currentSettings, extendedContextEnabled]
|
||||
);
|
||||
|
||||
// Toggle extended context - applies/strips [1m] suffix to all model env vars
|
||||
const toggleExtendedContext = useCallback(
|
||||
(enabled: boolean) => {
|
||||
const env = currentSettings?.env || {};
|
||||
const updates: Record<string, string> = {};
|
||||
|
||||
for (const key of MODEL_ENV_KEYS) {
|
||||
const value = env[key];
|
||||
if (value) {
|
||||
updates[key] = enabled
|
||||
? applyExtendedContextSuffix(value)
|
||||
: stripExtendedContextSuffix(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the legacy flag if present
|
||||
const newEnv = { ...env, ...updates };
|
||||
const newEnv = applySavedLongContextIntent(env, enabled);
|
||||
delete newEnv['CCS_EXTENDED_CONTEXT'];
|
||||
|
||||
const newSettings = { ...currentSettings, env: newEnv };
|
||||
setRawJsonEdits(JSON.stringify(newSettings, null, 2));
|
||||
},
|
||||
[currentSettings]
|
||||
[applySavedLongContextIntent, currentSettings]
|
||||
);
|
||||
|
||||
// Batch update multiple env values at once
|
||||
const updateEnvValues = useCallback(
|
||||
(updates: Record<string, string>) => {
|
||||
const newEnv = { ...(currentSettings?.env || {}), ...updates };
|
||||
const newSettings = { ...currentSettings, env: newEnv };
|
||||
const touchesAnthropicModel = Object.keys(updates).some(isAnthropicModelEnvKey);
|
||||
const envWithIntent = touchesAnthropicModel
|
||||
? applySavedLongContextIntent(newEnv, extendedContextEnabled)
|
||||
: newEnv;
|
||||
delete envWithIntent['CCS_EXTENDED_CONTEXT'];
|
||||
|
||||
const newSettings = { ...currentSettings, env: envWithIntent };
|
||||
setRawJsonEdits(JSON.stringify(newSettings, null, 2));
|
||||
},
|
||||
[currentSettings]
|
||||
[applySavedLongContextIntent, currentSettings, extendedContextEnabled]
|
||||
);
|
||||
|
||||
// Check if JSON is valid
|
||||
|
||||
@@ -3,9 +3,16 @@
|
||||
*/
|
||||
|
||||
export {
|
||||
ANTHROPIC_MODEL_ENV_KEYS,
|
||||
EXTENDED_CONTEXT_SUFFIX,
|
||||
applyExtendedContextPreferenceToAnthropicModels,
|
||||
isNativeGeminiModel,
|
||||
isAnthropicModelEnvKey,
|
||||
hasAnthropicExtendedContextEnabled,
|
||||
hasExtendedContextSuffix,
|
||||
applyExtendedContextSuffix,
|
||||
isClaudeModelId,
|
||||
likelySupportsClaudeExtendedContext,
|
||||
stripModelConfigurationSuffixes,
|
||||
stripExtendedContextSuffix,
|
||||
} from '../../../src/shared/extended-context-utils';
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { ProviderCatalog } from '@/components/cliproxy/provider-model-selector';
|
||||
import { stripModelConfigurationSuffixes } from '@/lib/extended-context-utils';
|
||||
|
||||
/** Model catalog data - mirrors src/cliproxy/model-catalog.ts */
|
||||
export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
||||
@@ -553,3 +554,15 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function findCatalogModel(provider: string, modelId: string) {
|
||||
const catalog = MODEL_CATALOGS[provider.toLowerCase()];
|
||||
if (!catalog) return undefined;
|
||||
|
||||
const normalizedModelId = stripModelConfigurationSuffixes(modelId);
|
||||
return catalog.models.find((model) => model.id === normalizedModelId);
|
||||
}
|
||||
|
||||
export function supportsExtendedContext(provider: string, modelId: string): boolean {
|
||||
return findCatalogModel(provider, modelId)?.extendedContext === true;
|
||||
}
|
||||
Reference in new issue
Block a user