mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 04:17:11 +00:00
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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user