feat(image): add native-read controls and autosave settings

- add per-profile native image preferences and native-capability detection
- redesign Settings -> Image around compact sections with Web-style autosave
- expose richer backend/profile status and update Gemini Flash defaults
This commit is contained in:
Tam Nhu Tran committed 2026-04-01 15:32:37 -04:00
1 parent 9d1d281e34
commit 982ffc5895
30 files changed
+1893 -708

No files matched your search

@@ -7,10 +7,12 @@ import {
isCLIProxyProvider,
mapExternalProviderName,
} from '../../cliproxy/provider-capabilities';
import { getProviderCatalog, supportsNativeImageInput } from '../../cliproxy/model-catalog';
import { extractProviderFromPathname } from '../../cliproxy/model-id-normalizer';
import type { CliproxyBridgeMetadata } from '../../api/services/profile-types';
import type { Settings } from '../../types/config';
import type { ProfileType } from '../../types/profile';
import { stripModelConfigurationSuffixes } from '../../shared/extended-context-utils';
export type ImageAnalysisResolutionSource =
| 'cliproxy-provider'
@@ -20,6 +22,7 @@ export type ImageAnalysisResolutionSource =
| 'cliproxy-bridge'
| 'profile-backend'
| 'fallback-backend'
| 'native-compatible'
| 'disabled'
| 'unsupported-profile'
| 'unresolved'
@@ -49,7 +52,7 @@ export interface ImageAnalysisResolutionContext {
settingsPath?: string | null;
cliproxyProvider?: string | null;
isComposite?: boolean;
settings?: Pick<Settings, 'env'> | null;
settings?: Pick<Settings, 'env' | 'ccs_image'> | null;
cliproxyBridge?: CliproxyBridgeMetadata | null;
hookInstalled?: boolean;
sharedHookInstalled?: boolean;
@@ -79,8 +82,26 @@ export interface ImageAnalysisStatus {
proxyReason: string | null;
effectiveRuntimeMode: ImageAnalysisEffectiveRuntimeMode;
effectiveRuntimeReason: string | null;
profileModel: string | null;
nativeReadPreference: boolean;
nativeImageCapable: boolean | null;
nativeImageReason: string | null;
}
interface NativeImageSupportResolution {
profileModel: string | null;
nativeReadPreference: boolean;
nativeImageCapable: boolean | null;
nativeImageReason: string | null;
}
const PROFILE_MODEL_ENV_KEYS = [
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
] as const;
function resolveProviderFromBaseUrl(baseUrl: unknown): string | null {
if (typeof baseUrl !== 'string' || baseUrl.trim().length === 0) {
return null;
@@ -233,9 +254,102 @@ function getRuntimePath(backendId: string | null): string | null {
return `/api/provider/${backendId}`;
}
function resolveBackend(
function resolveNativeImageProvider(
context: ImageAnalysisResolutionContext,
knownBackends: string[]
): string | null {
return normalizeImageAnalysisBackendId(
context.cliproxyProvider ??
context.cliproxyBridge?.provider ??
resolveProviderFromBaseUrl(context.settings?.env?.ANTHROPIC_BASE_URL ?? undefined),
knownBackends
);
}
function resolveProfileModel(
context: ImageAnalysisResolutionContext,
provider: string | null
): string | null {
const env = context.settings?.env;
if (env && typeof env === 'object') {
for (const key of PROFILE_MODEL_ENV_KEYS) {
const value = env[key];
if (typeof value === 'string' && value.trim().length > 0) {
return value.trim();
}
}
}
if (provider && isCLIProxyProvider(provider)) {
return getProviderCatalog(provider)?.defaultModel ?? null;
}
return null;
}
function verifyNativeImageCapability(
provider: string | null,
modelId: string | null
): boolean | null {
if (!modelId) {
return null;
}
if (provider && isCLIProxyProvider(provider) && supportsNativeImageInput(provider, modelId)) {
return true;
}
const normalizedModel = stripModelConfigurationSuffixes(modelId).trim().toLowerCase();
if (!normalizedModel) {
return null;
}
if (
normalizedModel.startsWith('gemini-') ||
normalizedModel.startsWith('claude-') ||
normalizedModel.startsWith('gpt-4o') ||
normalizedModel.includes('vision') ||
normalizedModel.includes('multimodal') ||
/(^|[-_.])vl([-. _]|$)/.test(normalizedModel) ||
/^glm-[\d.]+v([-. _]|$)/.test(normalizedModel)
) {
return true;
}
return null;
}
function resolveNativeImageSupport(
context: ImageAnalysisResolutionContext,
config: ImageAnalysisConfig
): NativeImageSupportResolution {
const knownBackends = Object.keys(config.provider_models);
const provider = resolveNativeImageProvider(context, knownBackends);
const profileModel = resolveProfileModel(context, provider);
const nativeReadPreference = context.settings?.ccs_image?.native_read === true;
const nativeImageCapable = verifyNativeImageCapability(provider, profileModel);
let nativeImageReason: string | null = null;
if (!profileModel) {
nativeImageReason = 'No current model is configured for this profile yet.';
} else if (nativeImageCapable) {
nativeImageReason = `${profileModel} can read images natively.`;
} else {
nativeImageReason = `CCS cannot verify native image support for ${profileModel} yet.`;
}
return {
profileModel,
nativeReadPreference,
nativeImageCapable,
nativeImageReason,
};
}
function resolveBackend(
context: ImageAnalysisResolutionContext,
config: ImageAnalysisConfig,
nativeSupport: NativeImageSupportResolution
): Pick<ImageAnalysisStatus, 'backendId' | 'backendDisplayName' | 'resolutionSource' | 'reason'> {
const { profileName, profileType, cliproxyProvider, isComposite, cliproxyBridge, settings } =
context;
@@ -258,6 +372,30 @@ function resolveBackend(
};
}
if (nativeSupport.nativeReadPreference) {
return {
backendId: null,
backendDisplayName: null,
resolutionSource: 'native-compatible',
reason:
nativeSupport.nativeImageCapable === true
? 'This profile is set to use native image reading.'
: `${nativeSupport.nativeImageReason ?? 'Native image reading is enabled for this profile.'} CCS will bypass the transformer for this profile.`,
};
}
// Explicit profile mappings are the only user-authored override and must
// win before provider/bridge inference.
const mappedBackend = resolveConfiguredProfileBackend(profileName, config);
if (mappedBackend) {
return {
backendId: mappedBackend,
backendDisplayName: getBackendDisplayName(mappedBackend),
resolutionSource: 'profile-backend',
reason: null,
};
}
if (profileType === 'copilot' || profileName === 'copilot') {
const backendId = normalizeImageAnalysisBackendId('ghcp', Object.keys(config.provider_models));
return {
@@ -300,16 +438,6 @@ function resolveBackend(
};
}
const mappedBackend = resolveConfiguredProfileBackend(profileName, config);
if (mappedBackend) {
return {
backendId: mappedBackend,
backendDisplayName: getBackendDisplayName(mappedBackend),
resolutionSource: 'profile-backend',
reason: null,
};
}
const hasDirectAnthropicApiKey = Boolean(settings?.env?.ANTHROPIC_API_KEY?.trim());
const hasBaseUrl = Boolean(settings?.env?.ANTHROPIC_BASE_URL?.trim());
if (hasDirectAnthropicApiKey && !hasBaseUrl) {
@@ -347,7 +475,8 @@ export function resolveImageAnalysisStatus(
rawConfig: ImageAnalysisConfig = DEFAULT_IMAGE_ANALYSIS_CONFIG
): ImageAnalysisStatus {
const config = canonicalizeImageAnalysisConfig(rawConfig);
const resolution = resolveBackend(context, config);
const nativeSupport = resolveNativeImageSupport(context, config);
const resolution = resolveBackend(context, config, nativeSupport);
const model = resolution.backendId
? (config.provider_models[resolution.backendId] ?? null)
: null;
@@ -444,5 +573,9 @@ export function resolveImageAnalysisStatus(
status === 'hook-missing' || !config.enabled || !resolution.backendId || !model
? reason
: null,
profileModel: nativeSupport.profileModel,
nativeReadPreference: nativeSupport.nativeReadPreference,
nativeImageCapable: nativeSupport.nativeImageCapable,
nativeImageReason: nativeSupport.nativeImageReason,
};
}