opencode-immune 1.0.71 → 1.0.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugin/server.js +103 -31
- package/package.json +1 -1
package/dist/plugin/server.js
CHANGED
|
@@ -3777,7 +3777,7 @@ import { fileURLToPath } from "url";
|
|
|
3777
3777
|
import { createHash } from "crypto";
|
|
3778
3778
|
import { tmpdir } from "os";
|
|
3779
3779
|
import { execFile } from "child_process";
|
|
3780
|
-
var PLUGIN_VERSION = "1.0.
|
|
3780
|
+
var PLUGIN_VERSION = "1.0.74";
|
|
3781
3781
|
var PLUGIN_PACKAGE_NAME = "opencode-immune";
|
|
3782
3782
|
var PLUGIN_DIRNAME = dirname(fileURLToPath(import.meta.url));
|
|
3783
3783
|
function getServerAuthHeaders() {
|
|
@@ -3855,6 +3855,7 @@ function createState(input) {
|
|
|
3855
3855
|
ultraworkPermissionSessions: /* @__PURE__ */ new Set(),
|
|
3856
3856
|
fallbackAgentByAgent: /* @__PURE__ */ new Map(),
|
|
3857
3857
|
baseAgentByFallbackAgent: /* @__PURE__ */ new Map(),
|
|
3858
|
+
fallbackModelCandidates: [],
|
|
3858
3859
|
ultraworkMarkerPath: join(
|
|
3859
3860
|
input.directory,
|
|
3860
3861
|
".opencode",
|
|
@@ -3911,20 +3912,15 @@ var MANAGED_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
|
3911
3912
|
var PROVIDER_RETRY_WATCHDOG_MS = 3e4;
|
|
3912
3913
|
var RETRY_PROMPT_DELIVERY_ATTEMPTS = 3;
|
|
3913
3914
|
var CHILD_FALLBACK_REQUEST_TTL_MS = 10 * 60 * 1e3;
|
|
3914
|
-
var RATE_LIMIT_FALLBACK_MODEL = {
|
|
3915
|
-
providerID: "codexsale",
|
|
3916
|
-
modelID: "gpt-5.4-mini"
|
|
3917
|
-
};
|
|
3918
|
-
var CHILD_SESSION_FALLBACK_MODEL = {
|
|
3919
|
-
providerID: "claudehub",
|
|
3920
|
-
modelID: "claude-opus-4-7"
|
|
3921
|
-
};
|
|
3922
3915
|
var AUTO_CYCLE_LOCK_TTL_MS = 30 * 60 * 1e3;
|
|
3923
|
-
var
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3916
|
+
var MODEL_NAME_CAPABILITY_SCORE = {
|
|
3917
|
+
"claude-opus-4-7": 100,
|
|
3918
|
+
"gpt-5.5": 100,
|
|
3919
|
+
"gpt-5.4": 90,
|
|
3920
|
+
"claude-sonnet-4-6": 90,
|
|
3921
|
+
"gpt-5.4-mini": 50,
|
|
3922
|
+
"claude-haiku-4-5": 50
|
|
3923
|
+
};
|
|
3928
3924
|
var FALLBACK_AGENT_SUFFIX = "-provider-fallback";
|
|
3929
3925
|
function isManagedUltraworkSession(state, sessionID) {
|
|
3930
3926
|
return !!sessionID && state.managedUltraworkSessions.has(sessionID);
|
|
@@ -4208,15 +4204,57 @@ function isSameModel(a, b) {
|
|
|
4208
4204
|
if (!a || !b) return false;
|
|
4209
4205
|
return a.providerID === b.providerID && a.modelID === b.modelID;
|
|
4210
4206
|
}
|
|
4211
|
-
function
|
|
4207
|
+
function getModelCapabilityScore(model) {
|
|
4208
|
+
if (!model) return 90;
|
|
4209
|
+
const modelID = model.modelID.toLowerCase();
|
|
4210
|
+
const explicitScore = MODEL_NAME_CAPABILITY_SCORE[modelID];
|
|
4211
|
+
if (explicitScore !== void 0) return explicitScore;
|
|
4212
|
+
if (/\b(mini|nano|small|lite|haiku|flash)\b/i.test(modelID)) return 50;
|
|
4213
|
+
if (/\b(opus|gpt-5\.5|gpt-5\.4|o3|o4|gpt-4\.1|gpt-4o)\b/i.test(modelID)) return 90;
|
|
4214
|
+
return 70;
|
|
4215
|
+
}
|
|
4216
|
+
function getMinimumFallbackCapabilityScore(currentModel) {
|
|
4217
|
+
const currentScore = getModelCapabilityScore(currentModel);
|
|
4218
|
+
if (currentScore >= 90) return 90;
|
|
4219
|
+
if (currentScore >= 70) return 70;
|
|
4220
|
+
return 0;
|
|
4221
|
+
}
|
|
4222
|
+
function sortFallbackModelsByCapability(candidates, currentModel) {
|
|
4223
|
+
const currentScore = getModelCapabilityScore(currentModel);
|
|
4224
|
+
const minimumScore = getMinimumFallbackCapabilityScore(currentModel);
|
|
4225
|
+
return [...candidates].sort((a, b) => {
|
|
4226
|
+
const aScore = getModelCapabilityScore(a);
|
|
4227
|
+
const bScore = getModelCapabilityScore(b);
|
|
4228
|
+
const aAllowed = aScore >= minimumScore ? 0 : 1;
|
|
4229
|
+
const bAllowed = bScore >= minimumScore ? 0 : 1;
|
|
4230
|
+
if (aAllowed !== bAllowed) return aAllowed - bAllowed;
|
|
4231
|
+
const aDirectionPenalty = aScore >= currentScore ? 0 : 1;
|
|
4232
|
+
const bDirectionPenalty = bScore >= currentScore ? 0 : 1;
|
|
4233
|
+
if (aDirectionPenalty !== bDirectionPenalty) {
|
|
4234
|
+
return aDirectionPenalty - bDirectionPenalty;
|
|
4235
|
+
}
|
|
4236
|
+
const aSameModelName = currentModel && a.modelID === currentModel.modelID ? 0 : 1;
|
|
4237
|
+
const bSameModelName = currentModel && b.modelID === currentModel.modelID ? 0 : 1;
|
|
4238
|
+
if (aSameModelName !== bSameModelName) return aSameModelName - bSameModelName;
|
|
4239
|
+
const aDistance = Math.abs(aScore - currentScore);
|
|
4240
|
+
const bDistance = Math.abs(bScore - currentScore);
|
|
4241
|
+
if (aDistance !== bDistance) return aDistance - bDistance;
|
|
4242
|
+
return bScore - aScore;
|
|
4243
|
+
});
|
|
4244
|
+
}
|
|
4245
|
+
function selectFallbackModel(state, currentModel) {
|
|
4212
4246
|
const currentProviderID = getModelProviderID(currentModel);
|
|
4213
|
-
const
|
|
4214
|
-
(candidate) => !isSameModel(candidate, currentModel)
|
|
4247
|
+
const fallbackCandidates = state.fallbackModelCandidates.filter(
|
|
4248
|
+
(candidate) => !isSameModel(candidate, currentModel)
|
|
4215
4249
|
);
|
|
4250
|
+
const otherProviderModel = sortFallbackModelsByCapability(
|
|
4251
|
+
fallbackCandidates.filter(
|
|
4252
|
+
(candidate) => getModelProviderID(candidate) !== currentProviderID
|
|
4253
|
+
),
|
|
4254
|
+
currentModel
|
|
4255
|
+
)[0];
|
|
4216
4256
|
if (otherProviderModel) return otherProviderModel;
|
|
4217
|
-
return
|
|
4218
|
-
(candidate) => !isSameModel(candidate, currentModel)
|
|
4219
|
-
) ?? RATE_LIMIT_FALLBACK_MODEL;
|
|
4257
|
+
return sortFallbackModelsByCapability(fallbackCandidates, currentModel)[0];
|
|
4220
4258
|
}
|
|
4221
4259
|
function getFailedModelFromError(error) {
|
|
4222
4260
|
if (!error || typeof error !== "object") return void 0;
|
|
@@ -4229,13 +4267,6 @@ function getFailedModelFromError(error) {
|
|
|
4229
4267
|
modelID: providerModelMatch[2].toLowerCase()
|
|
4230
4268
|
};
|
|
4231
4269
|
}
|
|
4232
|
-
const unsupportedModelMatch = message.match(/["']([^"']+)["']\s+model\s+is\s+not\s+supported/i);
|
|
4233
|
-
if (unsupportedModelMatch?.[1] && /\bcodex\b/i.test(message)) {
|
|
4234
|
-
return {
|
|
4235
|
-
providerID: "codexsale",
|
|
4236
|
-
modelID: unsupportedModelMatch[1].toLowerCase()
|
|
4237
|
-
};
|
|
4238
|
-
}
|
|
4239
4270
|
return void 0;
|
|
4240
4271
|
}
|
|
4241
4272
|
async function updateManagedSessionModel(state, sessionID, model) {
|
|
@@ -4252,7 +4283,7 @@ function getSessionFallbackModel(state, sessionID, preferredModel) {
|
|
|
4252
4283
|
if (preferredModel && !isSameModel(preferredModel, currentModel)) {
|
|
4253
4284
|
return preferredModel;
|
|
4254
4285
|
}
|
|
4255
|
-
return selectFallbackModel(currentModel);
|
|
4286
|
+
return selectFallbackModel(state, currentModel);
|
|
4256
4287
|
}
|
|
4257
4288
|
function getFallbackAgentForAgent(state, agent) {
|
|
4258
4289
|
return state.fallbackAgentByAgent.get(agent) ?? agent;
|
|
@@ -4389,8 +4420,10 @@ async function recoverUntrackedRootSessionForActiveTask(state, sessionID, reason
|
|
|
4389
4420
|
}
|
|
4390
4421
|
function recordChildFallbackRequest(state, managedSession, childSessionID, error) {
|
|
4391
4422
|
const fallbackModel = selectFallbackModel(
|
|
4423
|
+
state,
|
|
4392
4424
|
getFailedModelFromError(error) ?? managedSession.currentModel
|
|
4393
4425
|
);
|
|
4426
|
+
if (!fallbackModel) return;
|
|
4394
4427
|
const routerSessionID = getRouterSessionIDForChild(managedSession);
|
|
4395
4428
|
const request = {
|
|
4396
4429
|
childSessionID,
|
|
@@ -4421,6 +4454,7 @@ function scheduleProviderRetryWatchdog(state, sessionID, model) {
|
|
|
4421
4454
|
if (!isManagedRootUltraworkSession(state, sessionID)) return;
|
|
4422
4455
|
if (state.sessionRetryTimers.has(sessionID)) return;
|
|
4423
4456
|
const fallbackModel = getSessionFallbackModel(state, sessionID, model);
|
|
4457
|
+
if (!fallbackModel) return;
|
|
4424
4458
|
await setSessionFallbackModel(state, sessionID, fallbackModel);
|
|
4425
4459
|
await writeDiagnosticLog(state, "provider-retry-watchdog:fired", {
|
|
4426
4460
|
sessionID,
|
|
@@ -4652,6 +4686,36 @@ function parseModelRef(model) {
|
|
|
4652
4686
|
if (!providerID || !modelID) return void 0;
|
|
4653
4687
|
return { providerID, modelID };
|
|
4654
4688
|
}
|
|
4689
|
+
function addFallbackModelCandidate(candidates, model) {
|
|
4690
|
+
const parsed = parseModelRef(model);
|
|
4691
|
+
if (!parsed) return;
|
|
4692
|
+
candidates.set(modelRefToString(parsed).toLowerCase(), parsed);
|
|
4693
|
+
}
|
|
4694
|
+
function collectFallbackModelCandidates(config) {
|
|
4695
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
4696
|
+
addFallbackModelCandidate(candidates, config.model);
|
|
4697
|
+
addFallbackModelCandidate(candidates, config.small_model);
|
|
4698
|
+
for (const agentConfig of Object.values(config.agent ?? {})) {
|
|
4699
|
+
if (!isRecord(agentConfig)) continue;
|
|
4700
|
+
const model = agentConfig.model;
|
|
4701
|
+
if (typeof model === "string") addFallbackModelCandidate(candidates, model);
|
|
4702
|
+
}
|
|
4703
|
+
const enabledProviders = new Set(config.enabled_providers ?? []);
|
|
4704
|
+
const disabledProviders = new Set(config.disabled_providers ?? []);
|
|
4705
|
+
const isProviderEnabled = (providerID) => {
|
|
4706
|
+
if (enabledProviders.size > 0 && !enabledProviders.has(providerID)) return false;
|
|
4707
|
+
return !disabledProviders.has(providerID);
|
|
4708
|
+
};
|
|
4709
|
+
for (const [providerID, providerConfig] of Object.entries(config.provider ?? {})) {
|
|
4710
|
+
if (!isProviderEnabled(providerID) || !isRecord(providerConfig)) continue;
|
|
4711
|
+
const models = providerConfig.models;
|
|
4712
|
+
if (!isRecord(models)) continue;
|
|
4713
|
+
for (const modelID of Object.keys(models)) {
|
|
4714
|
+
addFallbackModelCandidate(candidates, `${providerID}/${modelID}`);
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
return sortFallbackModelsByCapability([...candidates.values()]);
|
|
4718
|
+
}
|
|
4655
4719
|
function canCreateFallbackForAgent(agentID, agentConfig) {
|
|
4656
4720
|
if (agentID.endsWith(FALLBACK_AGENT_SUFFIX)) return false;
|
|
4657
4721
|
if (!isRecord(agentConfig)) return false;
|
|
@@ -4682,11 +4746,12 @@ function allowFallbackAgentInTaskPermissions(config, baseAgentID, fallbackAgentI
|
|
|
4682
4746
|
function createConfigHandler(state) {
|
|
4683
4747
|
return async (config) => {
|
|
4684
4748
|
if (!config.agent) return;
|
|
4749
|
+
state.fallbackModelCandidates = collectFallbackModelCandidates(config);
|
|
4685
4750
|
for (const [agentID, agentConfig] of Object.entries(config.agent)) {
|
|
4686
4751
|
if (!canCreateFallbackForAgent(agentID, agentConfig)) continue;
|
|
4687
4752
|
const baseModel = parseModelRef(agentConfig.model);
|
|
4688
|
-
const fallbackModel = selectFallbackModel(baseModel);
|
|
4689
|
-
if (!baseModel || isSameModel(baseModel, fallbackModel)) continue;
|
|
4753
|
+
const fallbackModel = selectFallbackModel(state, baseModel);
|
|
4754
|
+
if (!baseModel || !fallbackModel || isSameModel(baseModel, fallbackModel)) continue;
|
|
4690
4755
|
const fallbackAgentID = `${agentID}${FALLBACK_AGENT_SUFFIX}`;
|
|
4691
4756
|
if (config.agent[fallbackAgentID]) continue;
|
|
4692
4757
|
config.agent[fallbackAgentID] = {
|
|
@@ -5499,9 +5564,12 @@ function createEventHandler(state) {
|
|
|
5499
5564
|
state.sessionErrorRetryCount.set(fallbackSessionID, count + 1);
|
|
5500
5565
|
if (shouldUseFallbackForManagedError(error, managedSession, count)) {
|
|
5501
5566
|
const fallbackModel = selectFallbackModel(
|
|
5567
|
+
state,
|
|
5502
5568
|
getFailedModelFromError(error) ?? managedSession.currentModel
|
|
5503
5569
|
);
|
|
5504
|
-
|
|
5570
|
+
if (fallbackModel) {
|
|
5571
|
+
await setSessionFallbackModel(state, fallbackSessionID, fallbackModel);
|
|
5572
|
+
}
|
|
5505
5573
|
}
|
|
5506
5574
|
pluginLog.warn(
|
|
5507
5575
|
`[opencode-immune] session.error without sessionID matched retryable error. Retrying sole managed root session ${fallbackSessionID}.`
|
|
@@ -5557,8 +5625,10 @@ function createEventHandler(state) {
|
|
|
5557
5625
|
return;
|
|
5558
5626
|
} else if (isRoot && shouldUseFallbackForManagedError(error, managedSession, count)) {
|
|
5559
5627
|
const selectedFallbackModel = selectFallbackModel(
|
|
5628
|
+
state,
|
|
5560
5629
|
getFailedModelFromError(error) ?? managedSession.currentModel
|
|
5561
5630
|
);
|
|
5631
|
+
if (!selectedFallbackModel) return;
|
|
5562
5632
|
await setSessionFallbackModel(state, sessionID, selectedFallbackModel);
|
|
5563
5633
|
const errorType = getRetryableErrorType(error);
|
|
5564
5634
|
pluginLog.info(
|
|
@@ -5705,6 +5775,7 @@ function createTextCompleteHandler(state) {
|
|
|
5705
5775
|
cancelProviderRetryWatchdog(state, sessionID, "assistant text completed");
|
|
5706
5776
|
if (isProviderRetryBanner(text) && isManagedRootSession) {
|
|
5707
5777
|
const fallbackModel = getSessionFallbackModel(state, sessionID);
|
|
5778
|
+
if (!fallbackModel) return;
|
|
5708
5779
|
await setSessionFallbackModel(state, sessionID, fallbackModel);
|
|
5709
5780
|
scheduleManagedSessionRetry(state, sessionID, {
|
|
5710
5781
|
delayMs: 1e3,
|
|
@@ -5831,6 +5902,7 @@ function createMultiCycleHandler(state) {
|
|
|
5831
5902
|
if (sessionID && RATE_LIMIT_MESSAGE_PATTERN.test(messageContent)) {
|
|
5832
5903
|
if (managedSession && !managedSession.fallbackModel) {
|
|
5833
5904
|
const fallbackModel = getSessionFallbackModel(state, sessionID);
|
|
5905
|
+
if (!fallbackModel) return;
|
|
5834
5906
|
await setSessionFallbackModel(state, sessionID, fallbackModel);
|
|
5835
5907
|
pluginLog.info(
|
|
5836
5908
|
`[opencode-immune] Rate limit message detected in chat output for session ${sessionID}. Fallback model pinned to ${fallbackModel.providerID}/${fallbackModel.modelID}.`
|