mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +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:
@@ -13,9 +13,10 @@ import type { CLIProxyProvider } from '../types';
|
||||
import { supportsExtendedContext } from '../model-catalog';
|
||||
import { warn } from '../../utils/ui';
|
||||
import {
|
||||
applyExtendedContextPreferenceToAnthropicModels,
|
||||
applyExtendedContextSuffix as applyExtendedContextSuffixShared,
|
||||
isNativeGeminiModel,
|
||||
stripExtendedContextSuffix,
|
||||
stripModelConfigurationSuffixes,
|
||||
} from '../../shared/extended-context-utils';
|
||||
|
||||
// Backward-compatible export retained for tests/importers that reference this module.
|
||||
@@ -72,57 +73,20 @@ export function applyExtendedContextConfig(
|
||||
provider: CLIProxyProvider,
|
||||
extendedContextOverride?: boolean
|
||||
): void {
|
||||
// Get base model to check support (strip any existing suffixes for lookup)
|
||||
const baseModel = envVars.ANTHROPIC_MODEL || '';
|
||||
const cleanModelId = stripModelSuffixes(baseModel);
|
||||
|
||||
// Tier model env vars to apply/strip extended context suffix
|
||||
const tierModels = [
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||
] as const;
|
||||
|
||||
if (!shouldApplyExtendedContext(provider, cleanModelId, extendedContextOverride)) {
|
||||
// Strip [1m] suffix from models that no longer support extended context
|
||||
// (e.g., user had it enabled before backend dropped support)
|
||||
if (envVars.ANTHROPIC_MODEL?.toLowerCase().endsWith('[1m]')) {
|
||||
envVars.ANTHROPIC_MODEL = envVars.ANTHROPIC_MODEL.replace(/\[1m\]$/i, '');
|
||||
}
|
||||
for (const tierVar of tierModels) {
|
||||
const model = envVars[tierVar];
|
||||
if (model?.toLowerCase().endsWith('[1m]')) {
|
||||
envVars[tierVar] = model.replace(/\[1m\]$/i, '');
|
||||
}
|
||||
}
|
||||
if (extendedContextOverride === false) {
|
||||
Object.assign(envVars, applyExtendedContextPreferenceToAnthropicModels(envVars, false));
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply suffix to main model
|
||||
if (envVars.ANTHROPIC_MODEL) {
|
||||
envVars.ANTHROPIC_MODEL = applyExtendedContextSuffixShared(envVars.ANTHROPIC_MODEL);
|
||||
}
|
||||
|
||||
// Apply to tier models if they support extended context
|
||||
|
||||
for (const tierVar of tierModels) {
|
||||
const model = envVars[tierVar];
|
||||
if (model) {
|
||||
const tierCleanId = stripModelSuffixes(model);
|
||||
if (shouldApplyExtendedContext(provider, tierCleanId, extendedContextOverride)) {
|
||||
envVars[tierVar] = applyExtendedContextSuffixShared(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip thinking and extended context suffixes from model ID for catalog lookup.
|
||||
* Examples:
|
||||
* "gemini-2.5-pro(high)[1m]" -> "gemini-2.5-pro"
|
||||
* "gemini-2.5-pro(8192)" -> "gemini-2.5-pro"
|
||||
* "gemini-2.5-pro" -> "gemini-2.5-pro"
|
||||
*/
|
||||
function stripModelSuffixes(modelId: string): string {
|
||||
return stripExtendedContextSuffix(modelId.trim()).replace(/\([^)]+\)$/, '');
|
||||
Object.assign(
|
||||
envVars,
|
||||
applyExtendedContextPreferenceToAnthropicModels(envVars, true, {
|
||||
supportsExtendedContext: (modelId) =>
|
||||
shouldApplyExtendedContext(
|
||||
provider,
|
||||
stripModelConfigurationSuffixes(modelId),
|
||||
extendedContextOverride
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,14 +6,10 @@
|
||||
*/
|
||||
|
||||
import type { CLIProxyProvider } from './types';
|
||||
import { ANTHROPIC_MODEL_ENV_KEYS } from '../shared/extended-context-utils';
|
||||
|
||||
/** Env vars that carry model identifiers. */
|
||||
export const MODEL_ENV_VAR_KEYS = [
|
||||
'ANTHROPIC_MODEL',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||
] as const;
|
||||
export const MODEL_ENV_VAR_KEYS = ANTHROPIC_MODEL_ENV_KEYS;
|
||||
|
||||
type ProviderLike = CLIProxyProvider | string | null | undefined;
|
||||
|
||||
|
||||
@@ -25,7 +25,13 @@ import { syncToLocalConfig } from '../../cliproxy/sync/local-config-sync';
|
||||
import type { TargetType } from '../../targets/target-adapter';
|
||||
import { color, dim, fail, header, info, infoBox, initUI, warn } from '../../utils/ui';
|
||||
import { InteractivePrompt } from '../../utils/prompt';
|
||||
import { exitOnApiCommandErrors, parseApiCommandArgs } from './shared';
|
||||
import {
|
||||
applyClaudeExtendedContextPreference,
|
||||
exitOnApiCommandErrors,
|
||||
hasClaudeModelMapping,
|
||||
hasExplicitClaudeExtendedContext,
|
||||
parseApiCommandArgs,
|
||||
} from './shared';
|
||||
|
||||
function resolvePresetOrExit(presetId?: string): ProviderPreset | null {
|
||||
if (!presetId) {
|
||||
@@ -276,6 +282,37 @@ async function resolveDefaultTarget(
|
||||
return useDroidByDefault ? 'droid' : 'claude';
|
||||
}
|
||||
|
||||
async function resolveClaudeLongContextPreference(
|
||||
models: ModelMapping,
|
||||
explicitPreference: boolean | undefined,
|
||||
yes: boolean | undefined
|
||||
): Promise<boolean> {
|
||||
if (explicitPreference !== undefined) {
|
||||
return explicitPreference;
|
||||
}
|
||||
|
||||
if (hasExplicitClaudeExtendedContext(models)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (yes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(info('Claude long context is explicit in CCS.'));
|
||||
console.log(dim(' Plain Claude model IDs stay on standard context unless you opt into [1m].'));
|
||||
console.log(
|
||||
dim(' Some providers/accounts still require extra usage or PAYG for long-context requests.')
|
||||
);
|
||||
console.log(dim(' If that entitlement is missing, upstream requests can still return 429.'));
|
||||
console.log('');
|
||||
|
||||
return InteractivePrompt.confirm('Enable [1m] for compatible Claude mappings?', {
|
||||
default: false,
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleApiCreateCommand(args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
const parsedArgs = parseApiCommandArgs(args);
|
||||
@@ -403,17 +440,31 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
const apiKey = await resolveApiKey(parsedArgs.apiKey, preset);
|
||||
const { model, models } = await resolveModelConfiguration(
|
||||
const { models } = await resolveModelConfiguration(
|
||||
baseUrl,
|
||||
preset,
|
||||
parsedArgs.model,
|
||||
parsedArgs.yes
|
||||
);
|
||||
const hasClaudeMappings = hasClaudeModelMapping(models);
|
||||
const shouldEnableClaudeLongContext = hasClaudeMappings
|
||||
? await resolveClaudeLongContextPreference(models, parsedArgs.extendedContext, parsedArgs.yes)
|
||||
: false;
|
||||
const finalModels = hasClaudeMappings
|
||||
? applyClaudeExtendedContextPreference(models, shouldEnableClaudeLongContext)
|
||||
: models;
|
||||
const target = await resolveDefaultTarget(parsedArgs.target, parsedArgs.yes);
|
||||
|
||||
if (parsedArgs.extendedContext !== undefined && !hasClaudeMappings) {
|
||||
console.log('');
|
||||
console.log(
|
||||
dim('No compatible Claude mappings were detected, so --1m/--no-1m did not change models.')
|
||||
);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(info('Creating API profile...'));
|
||||
const result = createApiProfile(name, baseUrl || '', apiKey, models, target);
|
||||
const result = createApiProfile(name, baseUrl || '', apiKey, finalModels, target);
|
||||
if (!result.success) {
|
||||
console.log(fail(`Failed to create API profile: ${result.error}`));
|
||||
process.exit(1);
|
||||
@@ -427,25 +478,43 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
const hasCustomMapping =
|
||||
models.opus !== model || models.sonnet !== model || models.haiku !== model;
|
||||
finalModels.opus !== finalModels.default ||
|
||||
finalModels.sonnet !== finalModels.default ||
|
||||
finalModels.haiku !== finalModels.default;
|
||||
let details =
|
||||
`API: ${name}\n` +
|
||||
`Config: ${isUsingUnifiedConfig() ? '~/.ccs/config.yaml' : '~/.ccs/config.json'}\n` +
|
||||
`Settings: ${result.settingsFile}\n` +
|
||||
`Base URL: ${baseUrl}\n` +
|
||||
`Model: ${model}\n` +
|
||||
`Model: ${finalModels.default}\n` +
|
||||
`Target: ${target}`;
|
||||
|
||||
if (hasClaudeMappings) {
|
||||
details += `\nLongCtx: ${
|
||||
shouldEnableClaudeLongContext ? 'compatible Claude mappings use [1m]' : 'standard Claude context'
|
||||
}`;
|
||||
}
|
||||
|
||||
if (hasCustomMapping) {
|
||||
details +=
|
||||
`\n\nModel Mapping:\n` +
|
||||
` Opus: ${models.opus}\n` +
|
||||
` Sonnet: ${models.sonnet}\n` +
|
||||
` Haiku: ${models.haiku}`;
|
||||
` Opus: ${finalModels.opus}\n` +
|
||||
` Sonnet: ${finalModels.sonnet}\n` +
|
||||
` Haiku: ${finalModels.haiku}`;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(infoBox(details, 'API Profile Created'));
|
||||
if (hasClaudeMappings) {
|
||||
console.log('');
|
||||
console.log(
|
||||
dim(
|
||||
shouldEnableClaudeLongContext
|
||||
? 'CCS saved [1m] on compatible Claude mappings. Provider-side extra-usage requirements can still reject long-context requests.'
|
||||
: 'Claude mappings were saved with standard context. Use --1m later if you want CCS to write [1m] explicitly.'
|
||||
)
|
||||
);
|
||||
}
|
||||
console.log('');
|
||||
console.log(header('Usage'));
|
||||
if (target === 'droid') {
|
||||
|
||||
@@ -55,6 +55,9 @@ export async function showApiCommandHelp(): Promise<void> {
|
||||
console.log(` ${color('--base-url <url>', 'command')} API base URL (create)`);
|
||||
console.log(` ${color('--api-key <key>', 'command')} API key (create)`);
|
||||
console.log(` ${color('--model <model>', 'command')} Default model (create)`);
|
||||
console.log(
|
||||
` ${color('--1m / --no-1m', 'command')} Write or clear [1m] on compatible Claude mappings`
|
||||
);
|
||||
console.log(
|
||||
` ${color('--target <cli>', 'command')} Default target: claude or droid (create)`
|
||||
);
|
||||
@@ -84,11 +87,23 @@ export async function showApiCommandHelp(): Promise<void> {
|
||||
console.log('');
|
||||
console.log(` ${dim('# Quick setup with preset')}`);
|
||||
console.log(` ${color('ccs api create --preset anthropic', 'command')}`);
|
||||
console.log(
|
||||
` ${color('ccs api create --preset anthropic --1m', 'command')} ${dim('# explicit Claude [1m] opt-in')}`
|
||||
);
|
||||
console.log(` ${color('ccs api create --preset openrouter', 'command')}`);
|
||||
console.log(` ${color('ccs api create --preset alibaba-coding-plan', 'command')}`);
|
||||
console.log(` ${color('ccs api create --preset alibaba', 'command')} ${dim('# alias')}`);
|
||||
console.log(` ${color('ccs api create --preset glm', 'command')}`);
|
||||
console.log('');
|
||||
console.log(subheader('Claude Long Context'));
|
||||
console.log(` ${dim('Plain Claude model IDs stay on standard context by default.')}`);
|
||||
console.log(
|
||||
` ${dim('Use --1m during create to append [1m] to compatible Claude mappings, or --no-1m to force plain IDs.')}`
|
||||
);
|
||||
console.log(
|
||||
` ${dim('CCS controls only the saved [1m] suffix. Provider pricing/entitlement stay upstream, and some accounts can still return 429 for long-context requests.')}`
|
||||
);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Create routed profile from existing CLIProxy provider config')}`);
|
||||
console.log(` ${color('ccs api create --cliproxy-provider gemini', 'command')}`);
|
||||
console.log(
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import type { ModelMapping } from '../../api/services';
|
||||
import type { TargetType } from '../../targets/target-adapter';
|
||||
import {
|
||||
applyExtendedContextSuffix,
|
||||
hasExtendedContextSuffix,
|
||||
isClaudeModelId,
|
||||
likelySupportsClaudeExtendedContext,
|
||||
stripExtendedContextSuffix,
|
||||
} from '../../shared/extended-context-utils';
|
||||
import { fail } from '../../utils/ui';
|
||||
import { extractOption, hasAnyFlag, scanCommandArgs } from '../arg-extractor';
|
||||
|
||||
@@ -11,12 +19,15 @@ export interface ApiCommandArgs {
|
||||
preset?: string;
|
||||
cliproxyProvider?: string;
|
||||
target?: TargetType;
|
||||
extendedContext?: boolean;
|
||||
force?: boolean;
|
||||
yes?: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export const API_BOOLEAN_FLAGS = ['--force', '--yes', '-y'] as const;
|
||||
const MODEL_MAPPING_KEYS = ['default', 'opus', 'sonnet', 'haiku'] as const;
|
||||
|
||||
export const API_BOOLEAN_FLAGS = ['--force', '--yes', '-y', '--1m', '--no-1m'] as const;
|
||||
export const API_VALUE_FLAGS = [
|
||||
'--base-url',
|
||||
'--api-key',
|
||||
@@ -155,6 +166,8 @@ export function parseApiCommandArgs(
|
||||
args: string[],
|
||||
options: ParseApiCommandArgsOptions = {}
|
||||
): ApiCommandArgs {
|
||||
const enableExtendedContext = hasAnyFlag(args, ['--1m']);
|
||||
const disableExtendedContext = hasAnyFlag(args, ['--no-1m']);
|
||||
const result: ApiCommandArgs = {
|
||||
positionals: [],
|
||||
force: hasAnyFlag(args, ['--force']),
|
||||
@@ -162,6 +175,14 @@ export function parseApiCommandArgs(
|
||||
errors: [],
|
||||
};
|
||||
|
||||
if (enableExtendedContext && disableExtendedContext) {
|
||||
result.errors.push('Cannot combine --1m and --no-1m');
|
||||
} else if (enableExtendedContext) {
|
||||
result.extendedContext = true;
|
||||
} else if (disableExtendedContext) {
|
||||
result.extendedContext = false;
|
||||
}
|
||||
|
||||
let remaining = [...args];
|
||||
|
||||
remaining = applyRepeatedOption(
|
||||
@@ -251,6 +272,36 @@ export function parseApiCommandArgs(
|
||||
return result;
|
||||
}
|
||||
|
||||
export function hasClaudeModelMapping(models: ModelMapping): boolean {
|
||||
return MODEL_MAPPING_KEYS.some((key) => isClaudeModelId(models[key]));
|
||||
}
|
||||
|
||||
export function hasExplicitClaudeExtendedContext(models: ModelMapping): boolean {
|
||||
return MODEL_MAPPING_KEYS.some(
|
||||
(key) => isClaudeModelId(models[key]) && hasExtendedContextSuffix(models[key])
|
||||
);
|
||||
}
|
||||
|
||||
export function applyClaudeExtendedContextPreference(
|
||||
models: ModelMapping,
|
||||
enabled: boolean
|
||||
): ModelMapping {
|
||||
const nextModels = { ...models };
|
||||
|
||||
for (const key of MODEL_MAPPING_KEYS) {
|
||||
const value = nextModels[key];
|
||||
if (!isClaudeModelId(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
nextModels[key] = enabled && likelySupportsClaudeExtendedContext(value)
|
||||
? applyExtendedContextSuffix(value)
|
||||
: stripExtendedContextSuffix(value);
|
||||
}
|
||||
|
||||
return nextModels;
|
||||
}
|
||||
|
||||
export function exitOnApiCommandErrors(errors: string[]): void {
|
||||
if (errors.length === 0) {
|
||||
return;
|
||||
|
||||
@@ -220,8 +220,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
'Set thinking budget (low/medium/high/xhigh/auto/off or number)',
|
||||
],
|
||||
['ccs codex --effort <level>', 'Set codex reasoning effort (medium/high/xhigh)'],
|
||||
['ccs <provider> --1m', 'Enable 1M token context window'],
|
||||
['ccs <provider> --no-1m', 'Disable 1M context (use 200K default)'],
|
||||
['ccs <provider> --1m', 'Request explicit 1M context when the selected model supports [1m]'],
|
||||
['ccs <provider> --no-1m', 'Force standard context / clear [1m]'],
|
||||
['ccs <provider> --logout', 'Clear authentication'],
|
||||
['ccs <provider> --headless', 'Headless auth (for SSH)'],
|
||||
['ccs <provider> --port-forward', 'Force port-forwarding mode (skip prompt)'],
|
||||
@@ -449,14 +449,15 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
|
||||
// Extended Context (1M)
|
||||
printSubSection('Extended Context (--1m)', [
|
||||
['--1m', 'Enable 1M token context window'],
|
||||
['--no-1m', 'Disable 1M context (use 200K default)'],
|
||||
['--1m', 'Request 1M token context when the selected model supports [1m]'],
|
||||
['--no-1m', 'Force standard context (Claude default stays plain)'],
|
||||
['', ''],
|
||||
['Auto behavior:', 'Gemini models: auto-enabled by default'],
|
||||
['', 'Claude models: opt-in with --1m flag'],
|
||||
['Auto behavior:', 'Gemini models: CCS auto-adds [1m] when supported'],
|
||||
['', 'Claude models: plain by default, opt-in with --1m or saved [1m]'],
|
||||
['', ''],
|
||||
['Note:', 'Extended context enables 1M token window via [1m] suffix.'],
|
||||
['', 'Premium pricing: 2x input for >200K tokens.'],
|
||||
['Note:', 'CCS only controls the saved [1m] suffix.'],
|
||||
['', 'Provider pricing and entitlement stay upstream.'],
|
||||
['', 'Some accounts/providers can still return 429 extra-usage errors for long-context requests.'],
|
||||
]);
|
||||
|
||||
// Image Analysis
|
||||
|
||||
@@ -4,6 +4,16 @@
|
||||
|
||||
/** Extended context suffix recognized by Claude Code. */
|
||||
export const EXTENDED_CONTEXT_SUFFIX = '[1m]';
|
||||
export const ANTHROPIC_MODEL_ENV_KEYS = [
|
||||
'ANTHROPIC_MODEL',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||
] as const;
|
||||
|
||||
export type AnthropicModelEnvKey = (typeof ANTHROPIC_MODEL_ENV_KEYS)[number];
|
||||
|
||||
const ANTHROPIC_MODEL_ENV_KEY_SET = new Set<string>(ANTHROPIC_MODEL_ENV_KEYS);
|
||||
|
||||
/** Check if model is a native Gemini model (auto-enabled behavior). */
|
||||
export function isNativeGeminiModel(modelId: string): boolean {
|
||||
@@ -27,3 +37,64 @@ export function stripExtendedContextSuffix(model: string): string {
|
||||
if (!model) return model;
|
||||
return hasExtendedContextSuffix(model) ? model.slice(0, -EXTENDED_CONTEXT_SUFFIX.length) : model;
|
||||
}
|
||||
|
||||
/** True when key belongs to Anthropic model mapping state. */
|
||||
export function isAnthropicModelEnvKey(key: string): key is AnthropicModelEnvKey {
|
||||
return ANTHROPIC_MODEL_ENV_KEY_SET.has(key);
|
||||
}
|
||||
|
||||
/** Strip transient config suffixes so model IDs can be checked against catalogs. */
|
||||
export function stripModelConfigurationSuffixes(modelId: string): string {
|
||||
return stripExtendedContextSuffix(modelId.trim()).replace(/\([^)]+\)$/, '');
|
||||
}
|
||||
|
||||
/** Whether any saved Anthropic model mapping explicitly requests [1m]. */
|
||||
export function hasAnthropicExtendedContextEnabled(
|
||||
env: Partial<Record<string, string | undefined>>
|
||||
): boolean {
|
||||
return ANTHROPIC_MODEL_ENV_KEYS.some((key) => {
|
||||
const value = env[key];
|
||||
return typeof value === 'string' && hasExtendedContextSuffix(value);
|
||||
});
|
||||
}
|
||||
|
||||
/** Apply or strip [1m] across Anthropic model mappings while honoring compatibility. */
|
||||
export function applyExtendedContextPreferenceToAnthropicModels<
|
||||
T extends Record<string, string | undefined>,
|
||||
>(
|
||||
env: T,
|
||||
enabled: boolean,
|
||||
options: {
|
||||
supportsExtendedContext?: (modelId: string, key: AnthropicModelEnvKey) => boolean;
|
||||
} = {}
|
||||
): T {
|
||||
const nextEnv: Record<string, string | undefined> = { ...env };
|
||||
|
||||
for (const key of ANTHROPIC_MODEL_ENV_KEYS) {
|
||||
const value = nextEnv[key];
|
||||
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const modelId = stripModelConfigurationSuffixes(value);
|
||||
const supported = options.supportsExtendedContext?.(modelId, key) ?? true;
|
||||
nextEnv[key] =
|
||||
enabled && supported ? applyExtendedContextSuffix(value) : stripExtendedContextSuffix(value);
|
||||
}
|
||||
|
||||
return nextEnv as T;
|
||||
}
|
||||
|
||||
/** Detect Claude model identifiers, regardless of transient [1m]/(thinking) suffixes. */
|
||||
export function isClaudeModelId(modelId: string): boolean {
|
||||
return stripModelConfigurationSuffixes(modelId).toLowerCase().startsWith('claude-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Conservative Claude long-context support heuristic for generic API profile flows.
|
||||
* Haiku stays plain; Opus/Sonnet default to opt-in [1m].
|
||||
*/
|
||||
export function likelySupportsClaudeExtendedContext(modelId: string): boolean {
|
||||
const baseModel = stripModelConfigurationSuffixes(modelId).toLowerCase();
|
||||
return baseModel.startsWith('claude-') && !baseModel.startsWith('claude-haiku-');
|
||||
}
|
||||
|
||||
@@ -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