opencode-immune 1.0.72 → 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 -35
  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.72";
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,24 +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
- var HIGH_CAPABILITY_FALLBACK_MODELS = [
3923
- { providerID: "codexsale", modelID: "gpt-5.5" },
3924
- { providerID: "codexsale", modelID: "gpt-5.4" }
3925
- ];
3926
3915
  var AUTO_CYCLE_LOCK_TTL_MS = 30 * 60 * 1e3;
3927
- var FALLBACK_MODEL_CANDIDATES = [
3928
- CHILD_SESSION_FALLBACK_MODEL,
3929
- ...HIGH_CAPABILITY_FALLBACK_MODELS,
3930
- RATE_LIMIT_FALLBACK_MODEL
3931
- ];
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
+ };
3932
3924
  var FALLBACK_AGENT_SUFFIX = "-provider-fallback";
3933
3925
  function isManagedUltraworkSession(state, sessionID) {
3934
3926
  return !!sessionID && state.managedUltraworkSessions.has(sessionID);
@@ -4212,15 +4204,57 @@ function isSameModel(a, b) {
4212
4204
  if (!a || !b) return false;
4213
4205
  return a.providerID === b.providerID && a.modelID === b.modelID;
4214
4206
  }
4215
- 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) {
4216
4246
  const currentProviderID = getModelProviderID(currentModel);
4217
- const otherProviderModel = FALLBACK_MODEL_CANDIDATES.find(
4218
- (candidate) => !isSameModel(candidate, currentModel) && getModelProviderID(candidate) !== currentProviderID
4247
+ const fallbackCandidates = state.fallbackModelCandidates.filter(
4248
+ (candidate) => !isSameModel(candidate, currentModel)
4219
4249
  );
4250
+ const otherProviderModel = sortFallbackModelsByCapability(
4251
+ fallbackCandidates.filter(
4252
+ (candidate) => getModelProviderID(candidate) !== currentProviderID
4253
+ ),
4254
+ currentModel
4255
+ )[0];
4220
4256
  if (otherProviderModel) return otherProviderModel;
4221
- return FALLBACK_MODEL_CANDIDATES.find(
4222
- (candidate) => !isSameModel(candidate, currentModel)
4223
- ) ?? RATE_LIMIT_FALLBACK_MODEL;
4257
+ return sortFallbackModelsByCapability(fallbackCandidates, currentModel)[0];
4224
4258
  }
4225
4259
  function getFailedModelFromError(error) {
4226
4260
  if (!error || typeof error !== "object") return void 0;
@@ -4233,13 +4267,6 @@ function getFailedModelFromError(error) {
4233
4267
  modelID: providerModelMatch[2].toLowerCase()
4234
4268
  };
4235
4269
  }
4236
- const unsupportedModelMatch = message.match(/["']([^"']+)["']\s+model\s+is\s+not\s+supported/i);
4237
- if (unsupportedModelMatch?.[1] && /\bcodex\b/i.test(message)) {
4238
- return {
4239
- providerID: "codexsale",
4240
- modelID: unsupportedModelMatch[1].toLowerCase()
4241
- };
4242
- }
4243
4270
  return void 0;
4244
4271
  }
4245
4272
  async function updateManagedSessionModel(state, sessionID, model) {
@@ -4256,7 +4283,7 @@ function getSessionFallbackModel(state, sessionID, preferredModel) {
4256
4283
  if (preferredModel && !isSameModel(preferredModel, currentModel)) {
4257
4284
  return preferredModel;
4258
4285
  }
4259
- return selectFallbackModel(currentModel);
4286
+ return selectFallbackModel(state, currentModel);
4260
4287
  }
4261
4288
  function getFallbackAgentForAgent(state, agent) {
4262
4289
  return state.fallbackAgentByAgent.get(agent) ?? agent;
@@ -4393,8 +4420,10 @@ async function recoverUntrackedRootSessionForActiveTask(state, sessionID, reason
4393
4420
  }
4394
4421
  function recordChildFallbackRequest(state, managedSession, childSessionID, error) {
4395
4422
  const fallbackModel = selectFallbackModel(
4423
+ state,
4396
4424
  getFailedModelFromError(error) ?? managedSession.currentModel
4397
4425
  );
4426
+ if (!fallbackModel) return;
4398
4427
  const routerSessionID = getRouterSessionIDForChild(managedSession);
4399
4428
  const request = {
4400
4429
  childSessionID,
@@ -4425,6 +4454,7 @@ function scheduleProviderRetryWatchdog(state, sessionID, model) {
4425
4454
  if (!isManagedRootUltraworkSession(state, sessionID)) return;
4426
4455
  if (state.sessionRetryTimers.has(sessionID)) return;
4427
4456
  const fallbackModel = getSessionFallbackModel(state, sessionID, model);
4457
+ if (!fallbackModel) return;
4428
4458
  await setSessionFallbackModel(state, sessionID, fallbackModel);
4429
4459
  await writeDiagnosticLog(state, "provider-retry-watchdog:fired", {
4430
4460
  sessionID,
@@ -4656,6 +4686,36 @@ function parseModelRef(model) {
4656
4686
  if (!providerID || !modelID) return void 0;
4657
4687
  return { providerID, modelID };
4658
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
+ }
4659
4719
  function canCreateFallbackForAgent(agentID, agentConfig) {
4660
4720
  if (agentID.endsWith(FALLBACK_AGENT_SUFFIX)) return false;
4661
4721
  if (!isRecord(agentConfig)) return false;
@@ -4686,11 +4746,12 @@ function allowFallbackAgentInTaskPermissions(config, baseAgentID, fallbackAgentI
4686
4746
  function createConfigHandler(state) {
4687
4747
  return async (config) => {
4688
4748
  if (!config.agent) return;
4749
+ state.fallbackModelCandidates = collectFallbackModelCandidates(config);
4689
4750
  for (const [agentID, agentConfig] of Object.entries(config.agent)) {
4690
4751
  if (!canCreateFallbackForAgent(agentID, agentConfig)) continue;
4691
4752
  const baseModel = parseModelRef(agentConfig.model);
4692
- const fallbackModel = selectFallbackModel(baseModel);
4693
- if (!baseModel || isSameModel(baseModel, fallbackModel)) continue;
4753
+ const fallbackModel = selectFallbackModel(state, baseModel);
4754
+ if (!baseModel || !fallbackModel || isSameModel(baseModel, fallbackModel)) continue;
4694
4755
  const fallbackAgentID = `${agentID}${FALLBACK_AGENT_SUFFIX}`;
4695
4756
  if (config.agent[fallbackAgentID]) continue;
4696
4757
  config.agent[fallbackAgentID] = {
@@ -5503,9 +5564,12 @@ function createEventHandler(state) {
5503
5564
  state.sessionErrorRetryCount.set(fallbackSessionID, count + 1);
5504
5565
  if (shouldUseFallbackForManagedError(error, managedSession, count)) {
5505
5566
  const fallbackModel = selectFallbackModel(
5567
+ state,
5506
5568
  getFailedModelFromError(error) ?? managedSession.currentModel
5507
5569
  );
5508
- await setSessionFallbackModel(state, fallbackSessionID, fallbackModel);
5570
+ if (fallbackModel) {
5571
+ await setSessionFallbackModel(state, fallbackSessionID, fallbackModel);
5572
+ }
5509
5573
  }
5510
5574
  pluginLog.warn(
5511
5575
  `[opencode-immune] session.error without sessionID matched retryable error. Retrying sole managed root session ${fallbackSessionID}.`
@@ -5561,8 +5625,10 @@ function createEventHandler(state) {
5561
5625
  return;
5562
5626
  } else if (isRoot && shouldUseFallbackForManagedError(error, managedSession, count)) {
5563
5627
  const selectedFallbackModel = selectFallbackModel(
5628
+ state,
5564
5629
  getFailedModelFromError(error) ?? managedSession.currentModel
5565
5630
  );
5631
+ if (!selectedFallbackModel) return;
5566
5632
  await setSessionFallbackModel(state, sessionID, selectedFallbackModel);
5567
5633
  const errorType = getRetryableErrorType(error);
5568
5634
  pluginLog.info(
@@ -5709,6 +5775,7 @@ function createTextCompleteHandler(state) {
5709
5775
  cancelProviderRetryWatchdog(state, sessionID, "assistant text completed");
5710
5776
  if (isProviderRetryBanner(text) && isManagedRootSession) {
5711
5777
  const fallbackModel = getSessionFallbackModel(state, sessionID);
5778
+ if (!fallbackModel) return;
5712
5779
  await setSessionFallbackModel(state, sessionID, fallbackModel);
5713
5780
  scheduleManagedSessionRetry(state, sessionID, {
5714
5781
  delayMs: 1e3,
@@ -5835,6 +5902,7 @@ function createMultiCycleHandler(state) {
5835
5902
  if (sessionID && RATE_LIMIT_MESSAGE_PATTERN.test(messageContent)) {
5836
5903
  if (managedSession && !managedSession.fallbackModel) {
5837
5904
  const fallbackModel = getSessionFallbackModel(state, sessionID);
5905
+ if (!fallbackModel) return;
5838
5906
  await setSessionFallbackModel(state, sessionID, fallbackModel);
5839
5907
  pluginLog.info(
5840
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.72",
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": {