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.
Files changed (2) hide show
  1. package/dist/plugin/server.js +103 -31
  2. package/package.json +1 -1
@@ -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.71";
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 FALLBACK_MODEL_CANDIDATES = [
3924
- CHILD_SESSION_FALLBACK_MODEL,
3925
- RATE_LIMIT_FALLBACK_MODEL,
3926
- { providerID: "codexsale", modelID: "gpt-5.5" }
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 selectFallbackModel(currentModel) {
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 otherProviderModel = FALLBACK_MODEL_CANDIDATES.find(
4214
- (candidate) => !isSameModel(candidate, currentModel) && getModelProviderID(candidate) !== currentProviderID
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 FALLBACK_MODEL_CANDIDATES.find(
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
- await setSessionFallbackModel(state, fallbackSessionID, fallbackModel);
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}.`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.71",
3
+ "version": "1.0.74",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
6
6
  "exports": {