mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-03 13:12:09 +00:00
fix(image-analysis): honor configured profile_backends at launch
resolveImageAnalysisRuntimeStatus defaulted to DEFAULT_IMAGE_ANALYSIS_CONFIG when callers omitted the config argument. That constant ships empty profile_backends and a gemini fallback_backend, so the launch paths that call it without a config (settings profile dispatch and headless delegation) never saw user-configured mappings. A profile mapped to another backend still resolved to gemini, failed the Gemini auth check, and silently dropped to native Read. Profiles whose model has no vision support could not read images at all, even with a reachable CLIProxy and an authenticated backend. getImageAnalysisHookEnv already reads the saved config, so the launch env and the runtime status disagreed on the same launch: CCS_IMAGE_ANALYSIS_BACKEND_ID carried the mapped backend while the status object reported native-read. Default to getImageAnalysisConfig() so both read the same source. Callers that pass an explicit config keep their existing behavior. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
a0990e0ef1
commit
fe3447f487
3 files changed
+130
-8
No files matched your search
@@ -16,10 +16,8 @@ import { getProxyTarget, type ProxyTarget } from '../../cliproxy/proxy/proxy-tar
|
||||
import { getProviderDisplayName, isCLIProxyProvider } from '../../cliproxy/provider-capabilities';
|
||||
import { isCliproxyRunning } from '../../cliproxy/services/stats-fetcher';
|
||||
import type { CLIProxyProvider } from '../../cliproxy/types';
|
||||
import {
|
||||
DEFAULT_IMAGE_ANALYSIS_CONFIG,
|
||||
type ImageAnalysisConfig,
|
||||
} from '../../config/unified-config-types';
|
||||
import { getImageAnalysisConfig } from '../../config/config-loader-facade';
|
||||
import { type ImageAnalysisConfig } from '../../config/unified-config-types';
|
||||
import {
|
||||
resolveImageAnalysisStatus,
|
||||
type ImageAnalysisResolutionContext,
|
||||
@@ -191,9 +189,14 @@ export async function hydrateImageAnalysisRuntimeStatus(
|
||||
|
||||
export async function resolveImageAnalysisRuntimeStatus(
|
||||
context: ImageAnalysisResolutionContext,
|
||||
config: ImageAnalysisConfig = DEFAULT_IMAGE_ANALYSIS_CONFIG,
|
||||
config?: ImageAnalysisConfig,
|
||||
deps: Partial<ImageAnalysisRuntimeStatusDeps> = {}
|
||||
): Promise<ImageAnalysisStatus> {
|
||||
const baseStatus = resolveImageAnalysisStatus(context, config);
|
||||
// Fall back to the user's saved image_analysis config, not the built-in
|
||||
// constant. Launch paths call this without an explicit config, and the
|
||||
// constant carries empty profile_backends plus a gemini fallback_backend,
|
||||
// so user mappings were dropped and every profile resolved to gemini.
|
||||
const resolvedConfig = config ?? getImageAnalysisConfig();
|
||||
const baseStatus = resolveImageAnalysisStatus(context, resolvedConfig);
|
||||
return hydrateImageAnalysisRuntimeStatus(baseStatus, deps);
|
||||
}
|
||||
@@ -89,8 +89,15 @@ describe('image-analysis-runtime-status circular dependency regression', () => {
|
||||
'../../cliproxy/services/stats-fetcher': {
|
||||
isCliproxyRunning: async () => true,
|
||||
},
|
||||
'../../config/unified-config-types': {
|
||||
DEFAULT_IMAGE_ANALYSIS_CONFIG: {},
|
||||
'../../config/unified-config-types': {},
|
||||
'../../config/config-loader-facade': {
|
||||
getImageAnalysisConfig: () => ({
|
||||
enabled: true,
|
||||
timeout: 60,
|
||||
provider_models: { ghcp: 'claude-haiku-4.5' },
|
||||
fallback_backend: 'ghcp',
|
||||
profile_backends: {},
|
||||
}),
|
||||
},
|
||||
'./image-analysis-backend-resolver': {
|
||||
resolveImageAnalysisStatus: () => createStatus(),
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
|
||||
/**
|
||||
* Regression: resolveImageAnalysisRuntimeStatus used to default to the built-in
|
||||
* DEFAULT_IMAGE_ANALYSIS_CONFIG constant, which carries empty profile_backends
|
||||
* and a gemini fallback_backend. Launch paths call it without an explicit
|
||||
* config, so user-configured profile_backends were dropped and every settings
|
||||
* profile resolved to gemini, then bailed to native Read on missing Gemini auth.
|
||||
*/
|
||||
describe('resolveImageAnalysisRuntimeStatus config default', () => {
|
||||
let tmpHome = '';
|
||||
let previousCcsHome: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpHome = mkdtempSync(join(tmpdir(), 'ccs-image-analysis-config-default-'));
|
||||
const ccsDir = join(tmpHome, '.ccs');
|
||||
mkdirSync(ccsDir, { recursive: true });
|
||||
|
||||
const settingsPath = join(ccsDir, 'deepseek.settings.json');
|
||||
writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: 'https://api.deepseek.com/anthropic',
|
||||
ANTHROPIC_MODEL: 'deepseek-v4-pro',
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + '\n'
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(ccsDir, 'config.yaml'),
|
||||
[
|
||||
'version: 14',
|
||||
'profiles:',
|
||||
' deepseek:',
|
||||
' type: api',
|
||||
` settings: ${settingsPath}`,
|
||||
'image_analysis:',
|
||||
' enabled: true',
|
||||
' timeout: 60',
|
||||
' provider_models:',
|
||||
' claude: claude-haiku-4-5-20251001',
|
||||
' fallback_backend: claude',
|
||||
' profile_backends:',
|
||||
' deepseek: claude',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
previousCcsHome = process.env.CCS_HOME;
|
||||
process.env.CCS_HOME = tmpHome;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (previousCcsHome === undefined) {
|
||||
delete process.env.CCS_HOME;
|
||||
} else {
|
||||
process.env.CCS_HOME = previousCcsHome;
|
||||
}
|
||||
|
||||
if (tmpHome) {
|
||||
rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('honors user profile_backends when no explicit config is passed', async () => {
|
||||
const { resolveImageAnalysisRuntimeStatus } = await import(
|
||||
'../../../../src/utils/hooks/image-analysis-runtime-status'
|
||||
);
|
||||
|
||||
const status = await resolveImageAnalysisRuntimeStatus(
|
||||
{
|
||||
profileName: 'deepseek',
|
||||
profileType: 'settings',
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
checkRemoteProxy: async () => ({ reachable: true }),
|
||||
fetchRemoteAuthStatus: async () => [{ provider: 'claude', authenticated: true }],
|
||||
getProxyTarget: () => ({
|
||||
host: '100.64.0.1',
|
||||
port: 8317,
|
||||
protocol: 'http',
|
||||
isRemote: true,
|
||||
}),
|
||||
initializeAccounts: () => {},
|
||||
getAuthStatus: () => ({
|
||||
provider: 'claude',
|
||||
authenticated: true,
|
||||
tokenDir: join(tmpHome, 'auth'),
|
||||
tokenFiles: [],
|
||||
accounts: [],
|
||||
defaultAccount: undefined,
|
||||
}),
|
||||
isCliproxyRunning: async () => true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(status.backendId).toBe('claude');
|
||||
expect(status.resolutionSource).toBe('profile-backend');
|
||||
expect(status.model).toBe('claude-haiku-4-5-20251001');
|
||||
expect(status.runtimePath).toBe('/api/provider/claude');
|
||||
expect(status.effectiveRuntimeMode).toBe('cliproxy-image-analysis');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user