feat(hooks): add ANTHROPIC_MODEL fallback for image analysis

Model resolution priority:
1. provider_models[current_provider]
2. ANTHROPIC_MODEL from profile settings
3. DEFAULT_MODEL (gemini-2.5-flash)

Allows users to override vision model via profile's ANTHROPIC_MODEL.
This commit is contained in:
kaitranntt
2026-02-04 00:19:51 -05:00
parent 3252228e5c
commit ae3eb282b4
+87 -5
View File
@@ -12,6 +12,7 @@
* CCS_CURRENT_PROVIDER - Current CLIProxy provider (e.g., agy, gemini, codex)
* CCS_IMAGE_ANALYSIS_TIMEOUT=60 - Timeout in seconds (default: 60)
* CCS_PROFILE_TYPE - Profile type (account/default skip)
* ANTHROPIC_MODEL - Fallback model if provider not in mapping
* CCS_DEBUG=1 - Enable debug output
*
* Exit codes:
@@ -107,14 +108,14 @@ function getDebugContext(filePath, stats) {
const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
const model = providerModels[currentProvider] || DEFAULT_MODEL;
const timeout = parseInt(process.env.CCS_IMAGE_ANALYSIS_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);
const isDefaultModel = !providerModels[currentProvider];
const modelsToTry = getModelsToTry();
return {
file: path.basename(filePath),
size: stats ? `${(stats.size / 1024).toFixed(1)} KB` : 'unknown',
provider: currentProvider,
model: model,
config: isDefaultModel ? 'default' : 'user-configured',
modelsToTry: modelsToTry.length > 1 ? modelsToTry.join(' -> ') : model,
timeout: `${timeout}s`,
endpoint: `http://${CLIPROXY_HOST}:${CLIPROXY_PORT}${CLIPROXY_PATH}`,
};
@@ -148,6 +149,7 @@ function parseProviderModels(envValue) {
/**
* Get model for current provider from provider_models mapping
* Returns primary model only (for display/logging)
*/
function getModelForProvider() {
const currentProvider = process.env.CCS_CURRENT_PROVIDER || '';
@@ -155,6 +157,85 @@ function getModelForProvider() {
return providerModels[currentProvider] || DEFAULT_MODEL;
}
/**
* Get list of models to try in order:
* 1. provider_models[current_provider] (if exists)
* 2. DEFAULT_MODEL
* 3. ANTHROPIC_MODEL from profile (if different and exists)
*/
function getModelsToTry() {
const currentProvider = process.env.CCS_CURRENT_PROVIDER || '';
const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
const anthropicModel = process.env.ANTHROPIC_MODEL;
const models = [];
const seen = new Set();
// 1. Provider-specific model
if (providerModels[currentProvider]) {
models.push(providerModels[currentProvider]);
seen.add(providerModels[currentProvider]);
}
// 2. Default model
if (!seen.has(DEFAULT_MODEL)) {
models.push(DEFAULT_MODEL);
seen.add(DEFAULT_MODEL);
}
// 3. ANTHROPIC_MODEL fallback
if (anthropicModel && !seen.has(anthropicModel)) {
models.push(anthropicModel);
}
return models;
}
/**
* Analyze with retry logic - tries models in order until one succeeds
*/
async function analyzeWithRetry(base64Data, mediaType, timeoutMs) {
const models = getModelsToTry();
let lastError = null;
for (let i = 0; i < models.length; i++) {
const model = models[i];
try {
debugLog(`Trying model ${i + 1}/${models.length}`, { model });
const result = await analyzeViaCliProxy(base64Data, mediaType, model, timeoutMs);
if (i > 0) {
debugLog('Retry succeeded', { model, attempt: i + 1 });
}
return { description: result, model };
} catch (err) {
lastError = err;
const isLastModel = i === models.length - 1;
// Don't retry on certain errors (auth, rate limit, timeout, file access)
const errMsg = err.message || '';
const noRetryPatterns = ['AUTH_ERROR', 'RATE_LIMIT', 'TIMEOUT', 'EACCES', 'EPERM', 'ECONNREFUSED'];
const shouldNotRetry = noRetryPatterns.some(p => errMsg.includes(p));
if (shouldNotRetry || isLastModel) {
debugLog('Analysis failed, no more retries', {
model,
error: errMsg,
reason: shouldNotRetry ? 'non-retryable error' : 'last model'
});
throw err;
}
debugLog('Model failed, trying next', {
model,
error: errMsg.substring(0, 100),
nextModel: models[i + 1]
});
}
}
throw lastError || new Error('No models available');
}
/**
* Check if file is an analyzable image or PDF
*/
@@ -707,15 +788,16 @@ async function processHook() {
base64Length: `${(base64Data.length / 1024).toFixed(1)}KB`,
});
// Analyze via CLIProxy
const description = await analyzeViaCliProxy(base64Data, mediaType, model, timeoutMs);
// Analyze via CLIProxy with retry logic
const { description, model: usedModel } = await analyzeWithRetry(base64Data, mediaType, timeoutMs);
debugLog('Analysis complete', {
responseLength: `${description.length} chars`,
model: usedModel,
});
// Output success
outputSuccess(filePath, description, model, stats.size);
outputSuccess(filePath, description, usedModel, stats.size);
} catch (err) {
if (process.env.CCS_DEBUG) {
console.error('[CCS Hook] Error:', err.message);