fix: prioritize direct providers for goal patch recovery

This commit is contained in:
thanhnv
2026-07-18 10:32:06 +07:00
parent 049f0c9b04
commit e17a373da6
5 changed files with 84 additions and 14 deletions
@@ -129,32 +129,46 @@ export class GoalsService {
const connections = this.connections(actor);
const local = connections.find((connection) => connection.connected && connection.kind === 'local');
const cloudConnections = connections.filter((connection) => connection.connected && connection.kind === 'cloud' && (connection.defaultModel || connection.models[0]));
const cloudConnections = connections
.filter((connection) => connection.connected && connection.kind === 'cloud' && (connection.defaultModel || connection.models[0]))
.sort((left, right) => ({ openai: 0, anthropic: 1 }[left.id] ?? 9) - ({ openai: 0, anthropic: 1 }[right.id] ?? 9));
const cloud = cloudConnections[0];
const gateway = connections.find((connection) => connection.connected && connection.kind === 'gateway');
const account = await this.accountReviewer();
const localModel = local?.defaultModel || local?.models[0] || 'ornith:9b';
const cloudModel = cloud?.defaultModel || cloud?.models[0] || gateway?.defaultModel || gateway?.models[0] || '';
const localRuntime = local ? this.localRuntime(this.runtime(local.id, localModel, actor)) : {
CASAN_CHAT_SELECTED_MODEL: `ollama:${localModel}`,
CASAN_OLLAMA_HOST: process.env.CASAN_OLLAMA_HOST || 'host.docker.internal:11434',
OLLAMA_HOST: process.env.OLLAMA_HOST || 'host.docker.internal:11434',
};
const selectedReviewer = cloud ?? gateway;
const cloudRuntime = selectedReviewer ? this.runtime(selectedReviewer.id, cloudModel, actor) : {};
// Runtime credentials for a saved connection must use that connection's
// discovered model, even when an environment-provided direct key has won
// priority for this run.
const cloudRuntime = cloud ? this.runtime(cloud.id, cloud.defaultModel || cloud.models[0] || '', actor) : {};
const gatewayRuntime = gateway
? this.runtime(gateway.id, gateway.defaultModel || gateway.models[0] || '', actor)
: {};
const gatewayCredentials = Object.fromEntries(
Object.entries(gatewayRuntime).filter(([key]) => key.startsWith('CASAN_OPENAI_COMPATIBLE_')),
);
const cloudCandidates = cloudConnections.map((connection) => {
const savedCloudCandidates = cloudConnections.map((connection) => {
const model = connection.defaultModel || connection.models[0];
const runtime = this.runtime(connection.id, model, actor);
return { model: String(runtime.CASAN_CHAT_SELECTED_MODEL || model), runtime };
});
const cloudCredentials = Object.assign({}, ...cloudCandidates.map(({ runtime }) => runtime));
const gatewayModels = gateway?.models.slice(0, 5).map((model) => `openai-compatible:${model}`) ?? [];
const envCloudCandidates = [
process.env.OPENAI_API_KEY ? { model: `openai:${process.env.CASAN_GOAL_OPENAI_MODEL || 'gpt-4o-mini'}`, runtime: {} } : null,
process.env.ANTHROPIC_API_KEY ? { model: `anthropic:${process.env.CASAN_GOAL_ANTHROPIC_MODEL || 'claude-3-5-sonnet-latest'}`, runtime: {} } : null,
].filter((candidate): candidate is { model: string; runtime: Record<string, string> } => candidate !== null);
const cloudCandidates = [...envCloudCandidates, ...savedCloudCandidates].filter((candidate, index, rows) => rows.findIndex((row) => row.model === candidate.model) === index);
const cloudCredentials = Object.assign({}, ...savedCloudCandidates.map(({ runtime }) => runtime));
const gatewayModels = gateway
? [gateway.defaultModel, ...gateway.models].filter((model, index, rows) => Boolean(model) && rows.indexOf(model) === index).slice(0, 5).map((model) => `openai-compatible:${model}`)
: [];
const preferredCloudModel = cloudCandidates[0]?.model || '';
const preferredCloudProvider = preferredCloudModel.startsWith('openai:') ? 'openai' : preferredCloudModel.startsWith('anthropic:') ? 'anthropic' : (cloud?.id || 'unavailable');
const cloudModel = preferredCloudModel || gateway?.defaultModel || gateway?.models[0] || '';
const id = randomUUID();
const timestamp = new Date().toISOString();
const job: GoalJob = {
@@ -170,11 +184,11 @@ export class GoalsService {
updated_at: timestamp,
local_provider: local?.id || 'local-policy',
local_model: String(localRuntime.CASAN_CHAT_SELECTED_MODEL || `ollama:${localModel}`),
cloud_provider: account ? `${account}-account` : (selectedReviewer?.id || 'unavailable'),
cloud_model: account ? `${account}-account-default` : String(cloudRuntime.CASAN_CHAT_SELECTED_MODEL || ''),
cloud_provider: account ? `${account}-account` : (preferredCloudProvider !== 'unavailable' ? preferredCloudProvider : (selectedReviewer?.id || 'unavailable')),
cloud_model: account ? `${account}-account-default` : preferredCloudModel || String(cloudRuntime.CASAN_CHAT_SELECTED_MODEL || ''),
stages: [
{ id: 'local-worker', status: 'queued', detail: 'Waiting for local worker', provider: local?.id || 'local-policy', model: localModel },
{ id: 'cloud-reviewer', status: 'queued', detail: account || selectedReviewer ? 'Waiting for independent reviewer' : 'Cloud unavailable; local reviewer will be used', provider: account ? `${account}-account` : (selectedReviewer?.id || 'local-policy'), model: account ? `${account}-account-default` : cloudModel || localModel },
{ id: 'cloud-reviewer', status: 'queued', detail: account || preferredCloudModel || selectedReviewer ? 'Waiting for independent reviewer' : 'Cloud unavailable; local reviewer will be used', provider: account ? `${account}-account` : (preferredCloudProvider !== 'unavailable' ? preferredCloudProvider : (selectedReviewer?.id || 'local-policy')), model: account ? `${account}-account-default` : cloudModel || localModel },
],
};
const jobFile = this.jobPath(actor.tenant, id);
@@ -199,6 +213,8 @@ export class GoalsService {
CASAN_GOAL_CLOUD_FALLBACK_MODEL: String(cloudRuntime.CASAN_CHAT_SELECTED_MODEL || ''),
CASAN_GOAL_CLOUD_MODELS: cloudCandidates.map(({ model }) => model).join(','),
CASAN_GOAL_OMNIROUTE_MODELS: gatewayModels.join(','),
// H2 recovery tries direct cloud credentials before gateway and local.
CASAN_GOAL_PATCH_REPAIR_MODELS: [...cloudCandidates.map(({ model }) => model), ...gatewayModels, String(localRuntime.CASAN_CHAT_SELECTED_MODEL || `ollama:${localModel}`)].filter((model, index, rows) => rows.indexOf(model) === index).join(','),
CASAN_GOAL_LOCAL_REVIEWER_MODEL: String(localRuntime.CASAN_CHAT_SELECTED_MODEL || `ollama:${localModel}`),
CASAN_GOAL_REVIEWER_MAX_ATTEMPTS: process.env.CASAN_GOAL_REVIEWER_MAX_ATTEMPTS || '8',
CASAN_GOAL_REVIEWER_DEADLINE_SEC: process.env.CASAN_GOAL_REVIEWER_DEADLINE_SEC || '600',