mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-20 22:23:36 +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_CURRENT_PROVIDER - Current CLIProxy provider (e.g., agy, gemini, codex)
|
||||||
* CCS_IMAGE_ANALYSIS_TIMEOUT=60 - Timeout in seconds (default: 60)
|
* CCS_IMAGE_ANALYSIS_TIMEOUT=60 - Timeout in seconds (default: 60)
|
||||||
* CCS_PROFILE_TYPE - Profile type (account/default skip)
|
* CCS_PROFILE_TYPE - Profile type (account/default skip)
|
||||||
|
* ANTHROPIC_MODEL - Fallback model if provider not in mapping
|
||||||
* CCS_DEBUG=1 - Enable debug output
|
* CCS_DEBUG=1 - Enable debug output
|
||||||
*
|
*
|
||||||
* Exit codes:
|
* Exit codes:
|
||||||
@@ -107,14 +108,14 @@ function getDebugContext(filePath, stats) {
|
|||||||
const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
|
const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
|
||||||
const model = providerModels[currentProvider] || DEFAULT_MODEL;
|
const model = providerModels[currentProvider] || DEFAULT_MODEL;
|
||||||
const timeout = parseInt(process.env.CCS_IMAGE_ANALYSIS_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);
|
const timeout = parseInt(process.env.CCS_IMAGE_ANALYSIS_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);
|
||||||
const isDefaultModel = !providerModels[currentProvider];
|
const modelsToTry = getModelsToTry();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
file: path.basename(filePath),
|
file: path.basename(filePath),
|
||||||
size: stats ? `${(stats.size / 1024).toFixed(1)} KB` : 'unknown',
|
size: stats ? `${(stats.size / 1024).toFixed(1)} KB` : 'unknown',
|
||||||
provider: currentProvider,
|
provider: currentProvider,
|
||||||
model: model,
|
model: model,
|
||||||
config: isDefaultModel ? 'default' : 'user-configured',
|
modelsToTry: modelsToTry.length > 1 ? modelsToTry.join(' -> ') : model,
|
||||||
timeout: `${timeout}s`,
|
timeout: `${timeout}s`,
|
||||||
endpoint: `http://${CLIPROXY_HOST}:${CLIPROXY_PORT}${CLIPROXY_PATH}`,
|
endpoint: `http://${CLIPROXY_HOST}:${CLIPROXY_PORT}${CLIPROXY_PATH}`,
|
||||||
};
|
};
|
||||||
@@ -148,6 +149,7 @@ function parseProviderModels(envValue) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get model for current provider from provider_models mapping
|
* Get model for current provider from provider_models mapping
|
||||||
|
* Returns primary model only (for display/logging)
|
||||||
*/
|
*/
|
||||||
function getModelForProvider() {
|
function getModelForProvider() {
|
||||||
const currentProvider = process.env.CCS_CURRENT_PROVIDER || '';
|
const currentProvider = process.env.CCS_CURRENT_PROVIDER || '';
|
||||||
@@ -155,6 +157,85 @@ function getModelForProvider() {
|
|||||||
return providerModels[currentProvider] || DEFAULT_MODEL;
|
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
|
* Check if file is an analyzable image or PDF
|
||||||
*/
|
*/
|
||||||
@@ -707,15 +788,16 @@ async function processHook() {
|
|||||||
base64Length: `${(base64Data.length / 1024).toFixed(1)}KB`,
|
base64Length: `${(base64Data.length / 1024).toFixed(1)}KB`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Analyze via CLIProxy
|
// Analyze via CLIProxy with retry logic
|
||||||
const description = await analyzeViaCliProxy(base64Data, mediaType, model, timeoutMs);
|
const { description, model: usedModel } = await analyzeWithRetry(base64Data, mediaType, timeoutMs);
|
||||||
|
|
||||||
debugLog('Analysis complete', {
|
debugLog('Analysis complete', {
|
||||||
responseLength: `${description.length} chars`,
|
responseLength: `${description.length} chars`,
|
||||||
|
model: usedModel,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Output success
|
// Output success
|
||||||
outputSuccess(filePath, description, model, stats.size);
|
outputSuccess(filePath, description, usedModel, stats.size);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (process.env.CCS_DEBUG) {
|
if (process.env.CCS_DEBUG) {
|
||||||
console.error('[CCS Hook] Error:', err.message);
|
console.error('[CCS Hook] Error:', err.message);
|
||||||
|
|||||||
Reference in New Issue
Block a user