@skj1724/oh-my-opencode 3.19.6 → 3.19.8

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/index.js CHANGED
@@ -4859,7 +4859,7 @@ var init_agent_tool_restrictions = __esm(() => {
4859
4859
  });
4860
4860
 
4861
4861
  // src/shared/model-requirements.ts
4862
- var AGENT_MODEL_REQUIREMENTS;
4862
+ var AGENT_MODEL_REQUIREMENTS, CATEGORY_MODEL_REQUIREMENTS;
4863
4863
  var init_model_requirements = __esm(() => {
4864
4864
  AGENT_MODEL_REQUIREMENTS = {
4865
4865
  sisyphus: {
@@ -4928,6 +4928,58 @@ var init_model_requirements = __esm(() => {
4928
4928
  ]
4929
4929
  }
4930
4930
  };
4931
+ CATEGORY_MODEL_REQUIREMENTS = {
4932
+ "visual-engineering": {
4933
+ fallbackChain: [
4934
+ { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
4935
+ { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" },
4936
+ { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" }
4937
+ ]
4938
+ },
4939
+ ultrabrain: {
4940
+ fallbackChain: [
4941
+ { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2-codex", variant: "xhigh" },
4942
+ { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" },
4943
+ { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" }
4944
+ ]
4945
+ },
4946
+ artistry: {
4947
+ fallbackChain: [
4948
+ { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "max" },
4949
+ { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" },
4950
+ { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" }
4951
+ ]
4952
+ },
4953
+ quick: {
4954
+ fallbackChain: [
4955
+ { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-haiku-4-5" },
4956
+ { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
4957
+ { providers: ["opencode"], model: "gpt-5-nano" }
4958
+ ]
4959
+ },
4960
+ "unspecified-low": {
4961
+ fallbackChain: [
4962
+ { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
4963
+ { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2-codex", variant: "medium" },
4964
+ { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" }
4965
+ ]
4966
+ },
4967
+ "unspecified-high": {
4968
+ fallbackChain: [
4969
+ { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" },
4970
+ { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
4971
+ { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" }
4972
+ ]
4973
+ },
4974
+ writing: {
4975
+ fallbackChain: [
4976
+ { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
4977
+ { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
4978
+ { providers: ["zai-coding-plan"], model: "glm-4.7" },
4979
+ { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" }
4980
+ ]
4981
+ }
4982
+ };
4931
4983
  });
4932
4984
 
4933
4985
  // src/shared/model-availability.ts
@@ -5064,6 +5116,99 @@ var init_model_resolver = __esm(() => {
5064
5116
  init_model_availability();
5065
5117
  });
5066
5118
 
5119
+ // src/shared/runtime-fallback.ts
5120
+ function expandChain(chain) {
5121
+ const candidates = [];
5122
+ for (const entry of chain) {
5123
+ for (const provider of entry.providers) {
5124
+ candidates.push({
5125
+ providerID: provider,
5126
+ modelID: entry.model,
5127
+ variant: entry.variant
5128
+ });
5129
+ }
5130
+ }
5131
+ return candidates;
5132
+ }
5133
+ function modelKey(m) {
5134
+ return `${m.providerID}/${m.modelID}`;
5135
+ }
5136
+ function getChain(agent, category) {
5137
+ if (agent && AGENT_MODEL_REQUIREMENTS[agent]) {
5138
+ return AGENT_MODEL_REQUIREMENTS[agent].fallbackChain;
5139
+ }
5140
+ if (category && CATEGORY_MODEL_REQUIREMENTS[category]) {
5141
+ return CATEGORY_MODEL_REQUIREMENTS[category].fallbackChain;
5142
+ }
5143
+ return;
5144
+ }
5145
+ function resolveNextFallbackModel(input) {
5146
+ const {
5147
+ agent,
5148
+ category,
5149
+ currentModel,
5150
+ attempts,
5151
+ availableModels,
5152
+ lastErrorClassification,
5153
+ configuredFallbackModels,
5154
+ maxAttempts
5155
+ } = input;
5156
+ if (maxAttempts !== undefined && attempts.length >= maxAttempts) {
5157
+ return {
5158
+ kind: "exhausted",
5159
+ attempts,
5160
+ reason: "max fallback attempts reached",
5161
+ lastErrorClassification
5162
+ };
5163
+ }
5164
+ const chain = getChain(agent, category);
5165
+ const candidates = configuredFallbackModels && configuredFallbackModels.length > 0 ? configuredFallbackModels : chain ? expandChain(chain) : undefined;
5166
+ if (!candidates) {
5167
+ return {
5168
+ kind: "unconfigured",
5169
+ reason: `No fallback chain found for agent="${agent ?? ""}" category="${category ?? ""}"`
5170
+ };
5171
+ }
5172
+ const skipKeys = new Set;
5173
+ skipKeys.add(modelKey(currentModel));
5174
+ for (const a of attempts) {
5175
+ skipKeys.add(modelKey(a.model));
5176
+ }
5177
+ const skipped = [];
5178
+ const hasAvailabilityFilter = availableModels != null && availableModels.size > 0;
5179
+ for (const candidate of candidates) {
5180
+ const key = modelKey(candidate);
5181
+ if (skipKeys.has(key)) {
5182
+ skipped.push({ model: candidate, reason: "already attempted or current model" });
5183
+ continue;
5184
+ }
5185
+ if (hasAvailabilityFilter) {
5186
+ const match = fuzzyMatchModel(key, availableModels, [candidate.providerID]);
5187
+ if (!match) {
5188
+ skipped.push({ model: candidate, reason: "model unavailable" });
5189
+ continue;
5190
+ }
5191
+ }
5192
+ return {
5193
+ kind: "next",
5194
+ model: candidate,
5195
+ attempts,
5196
+ skipped
5197
+ };
5198
+ }
5199
+ return {
5200
+ kind: "exhausted",
5201
+ attempts,
5202
+ reason: "No fallback candidates available",
5203
+ skipped,
5204
+ lastErrorClassification
5205
+ };
5206
+ }
5207
+ var init_runtime_fallback = __esm(() => {
5208
+ init_model_availability();
5209
+ init_model_requirements();
5210
+ });
5211
+
5067
5212
  // src/shared/perf-timer.ts
5068
5213
  class PerfTimer {
5069
5214
  marks = new Map;
@@ -5348,6 +5493,7 @@ var init_shared = __esm(() => {
5348
5493
  init_model_requirements();
5349
5494
  init_model_resolver();
5350
5495
  init_model_availability();
5496
+ init_runtime_fallback();
5351
5497
  init_perf_tracer();
5352
5498
  init_fileio_monitor();
5353
5499
  init_windows_reserved_names();
@@ -24501,6 +24647,450 @@ function createPerfProfilerHook(options) {
24501
24647
  "chat.message": async () => {}
24502
24648
  };
24503
24649
  }
24650
+ // src/shared/provider-error-classifier.ts
24651
+ function extractErrorInfo(error) {
24652
+ if (typeof error === "string") {
24653
+ return { message: error };
24654
+ }
24655
+ if (error instanceof Error) {
24656
+ const anyErr = error;
24657
+ return {
24658
+ statusCode: anyErr.status ?? anyErr.statusCode ?? anyErr.httpStatus,
24659
+ code: anyErr.code ?? anyErr.error?.code,
24660
+ type: anyErr.type ?? anyErr.error?.type,
24661
+ message: error.message,
24662
+ status: anyErr.status ?? anyErr.error?.status,
24663
+ headers: anyErr.headers
24664
+ };
24665
+ }
24666
+ if (typeof error === "object" && error !== null) {
24667
+ const obj = error;
24668
+ const inner = obj.error ?? {};
24669
+ return {
24670
+ statusCode: obj.status ?? obj.statusCode ?? inner.status,
24671
+ code: inner.code ?? obj.code,
24672
+ type: inner.type ?? obj.type,
24673
+ message: inner.message ?? obj.message ?? String(error),
24674
+ status: inner.status ?? obj.status,
24675
+ headers: obj.headers
24676
+ };
24677
+ }
24678
+ return { message: String(error) };
24679
+ }
24680
+ function parseRetryAfterMs(headers) {
24681
+ if (!headers)
24682
+ return;
24683
+ const retryAfter = headers["retry-after"] ?? headers["Retry-After"];
24684
+ if (retryAfter) {
24685
+ const seconds = Number(retryAfter);
24686
+ if (!isNaN(seconds) && seconds > 0) {
24687
+ return seconds * 1000;
24688
+ }
24689
+ }
24690
+ const reset = headers["x-ratelimit-reset"] ?? headers["X-Ratelimit-Reset"];
24691
+ if (reset) {
24692
+ const resetTimestamp = Number(reset);
24693
+ if (!isNaN(resetTimestamp)) {
24694
+ const resetMs = resetTimestamp > 1000000000000 ? resetTimestamp : resetTimestamp * 1000;
24695
+ const delayMs = resetMs - Date.now();
24696
+ return delayMs > 0 ? delayMs : 0;
24697
+ }
24698
+ }
24699
+ return;
24700
+ }
24701
+ function isContextOverflow(message, code) {
24702
+ const lowerMessage = message.toLowerCase();
24703
+ return lowerMessage.includes("context_length_exceeded") || lowerMessage.includes("prompt is too long") || lowerMessage.includes("maximum context length") || code === "context_length_exceeded";
24704
+ }
24705
+ function isZhipuQuotaCode(code) {
24706
+ if (typeof code !== "number")
24707
+ return false;
24708
+ return [1113, 1304, 1308, 1309].includes(code);
24709
+ }
24710
+ function isZhipuRateLimitCode(code) {
24711
+ if (typeof code !== "number")
24712
+ return false;
24713
+ return [1302, 1303].includes(code);
24714
+ }
24715
+ function isZhipuOverloadedCode(code) {
24716
+ if (typeof code !== "number")
24717
+ return false;
24718
+ return code === 1312;
24719
+ }
24720
+ function isGeminiQuotaDetails(details) {
24721
+ if (!Array.isArray(details))
24722
+ return false;
24723
+ return details.some((d) => d?.["@type"]?.includes("QuotaFailure") || d?.["@type"]?.includes("google.rpc.QuotaFailure"));
24724
+ }
24725
+ function isGeminiPerMinuteLimit(message, details) {
24726
+ const lowerMessage = message.toLowerCase();
24727
+ if (lowerMessage.includes("per_minute") || lowerMessage.includes("per minute")) {
24728
+ return true;
24729
+ }
24730
+ if (Array.isArray(details)) {
24731
+ return details.some((d) => d?.["@type"]?.includes("RetryInfo"));
24732
+ }
24733
+ return false;
24734
+ }
24735
+ function classifyProviderError(error) {
24736
+ const info = extractErrorInfo(error);
24737
+ const { statusCode, code, type: type2, message, status, headers } = info;
24738
+ if (isContextOverflow(message, code)) {
24739
+ return {
24740
+ category: "context_overflow",
24741
+ retryable: false,
24742
+ shouldFallback: false,
24743
+ statusCode,
24744
+ reason: "Context length exceeded, prompt too long for model"
24745
+ };
24746
+ }
24747
+ if (statusCode === 401 || statusCode === 403) {
24748
+ return {
24749
+ category: "auth",
24750
+ retryable: false,
24751
+ shouldFallback: false,
24752
+ statusCode,
24753
+ reason: statusCode === 401 ? "Invalid API key or authentication" : "Permission denied"
24754
+ };
24755
+ }
24756
+ const lowerMessage = message.toLowerCase();
24757
+ const isModelUnavailableMessage = lowerMessage.includes("model not found") || lowerMessage.includes("model unavailable") || lowerMessage.includes("unsupported model") || lowerMessage.includes("invalid model") || lowerMessage.includes("unknown model");
24758
+ const isProviderUnavailableMessage = lowerMessage.includes("provider not found") || lowerMessage.includes("unknown provider") || lowerMessage.includes("invalid provider");
24759
+ if (isProviderUnavailableMessage && (statusCode === 400 || statusCode === 404 || statusCode === 422)) {
24760
+ return {
24761
+ category: "provider_unavailable",
24762
+ retryable: false,
24763
+ shouldFallback: true,
24764
+ statusCode,
24765
+ reason: `Provider unavailable: ${message.substring(0, 100)}`
24766
+ };
24767
+ }
24768
+ if (isModelUnavailableMessage && (statusCode === 400 || statusCode === 404 || statusCode === 422)) {
24769
+ return {
24770
+ category: "model_unavailable",
24771
+ retryable: false,
24772
+ shouldFallback: true,
24773
+ statusCode,
24774
+ reason: `Model unavailable: ${message.substring(0, 100)}`
24775
+ };
24776
+ }
24777
+ if (statusCode === 404) {
24778
+ return {
24779
+ category: "model_unavailable",
24780
+ retryable: false,
24781
+ shouldFallback: true,
24782
+ statusCode,
24783
+ reason: `Model not found (404): ${message.substring(0, 100)}`
24784
+ };
24785
+ }
24786
+ if (statusCode === 400) {
24787
+ return {
24788
+ category: "bad_request",
24789
+ retryable: false,
24790
+ shouldFallback: false,
24791
+ statusCode,
24792
+ reason: "Invalid request parameters"
24793
+ };
24794
+ }
24795
+ if (statusCode === 429 && (code === "insufficient_quota" || type2 === "insufficient_quota")) {
24796
+ return {
24797
+ category: "quota",
24798
+ retryable: false,
24799
+ shouldFallback: true,
24800
+ statusCode,
24801
+ providerGuess: "openai",
24802
+ reason: "OpenAI quota exceeded, billing issue"
24803
+ };
24804
+ }
24805
+ if (statusCode === 402 && type2 === "billing_error") {
24806
+ return {
24807
+ category: "quota",
24808
+ retryable: false,
24809
+ shouldFallback: true,
24810
+ statusCode,
24811
+ providerGuess: "anthropic",
24812
+ reason: "Anthropic billing error, payment required"
24813
+ };
24814
+ }
24815
+ if (statusCode === 429 && status === "RESOURCE_EXHAUSTED" && isGeminiQuotaDetails(info.headers ? undefined : error?.error?.details)) {
24816
+ return {
24817
+ category: "quota",
24818
+ retryable: false,
24819
+ shouldFallback: true,
24820
+ statusCode,
24821
+ providerGuess: "gemini",
24822
+ reason: "Gemini daily quota exceeded"
24823
+ };
24824
+ }
24825
+ if (statusCode === 429 && isZhipuQuotaCode(code)) {
24826
+ const quotaReasons = {
24827
+ 1113: "\u8D26\u6237\u6B20\u8D39",
24828
+ 1304: "\u8C03\u7528\u6B21\u6570\u8D85\u8FC7\u9650\u989D",
24829
+ 1308: "\u4F7F\u7528\u91CF\u8D85\u8FC7\u4E0A\u9650",
24830
+ 1309: "\u5957\u9910\u5DF2\u5230\u671F"
24831
+ };
24832
+ return {
24833
+ category: "quota",
24834
+ retryable: false,
24835
+ shouldFallback: true,
24836
+ statusCode,
24837
+ providerGuess: "zhipu",
24838
+ reason: `Zhipu/GLM: ${quotaReasons[code] ?? "quota exceeded"}`
24839
+ };
24840
+ }
24841
+ if (statusCode === 529 && type2 === "overloaded_error") {
24842
+ return {
24843
+ category: "overloaded",
24844
+ retryable: true,
24845
+ shouldFallback: false,
24846
+ statusCode,
24847
+ providerGuess: "anthropic",
24848
+ reason: "Anthropic API overloaded"
24849
+ };
24850
+ }
24851
+ if (statusCode === 429 && isZhipuOverloadedCode(code)) {
24852
+ return {
24853
+ category: "overloaded",
24854
+ retryable: true,
24855
+ shouldFallback: false,
24856
+ statusCode,
24857
+ providerGuess: "zhipu",
24858
+ reason: "Zhipu/GLM: \u5F53\u524D\u8D1F\u8F7D\u8FC7\u9AD8"
24859
+ };
24860
+ }
24861
+ if (statusCode === 429 && type2 === "rate_limit_error") {
24862
+ return {
24863
+ category: "rate_limit",
24864
+ retryable: true,
24865
+ shouldFallback: false,
24866
+ statusCode,
24867
+ providerGuess: "anthropic",
24868
+ retryAfterMs: parseRetryAfterMs(headers),
24869
+ reason: "Anthropic rate limit exceeded"
24870
+ };
24871
+ }
24872
+ if (statusCode === 429 && code === "rate_limit_exceeded") {
24873
+ return {
24874
+ category: "rate_limit",
24875
+ retryable: true,
24876
+ shouldFallback: false,
24877
+ statusCode,
24878
+ providerGuess: "openai",
24879
+ retryAfterMs: parseRetryAfterMs(headers),
24880
+ reason: "OpenAI rate limit exceeded"
24881
+ };
24882
+ }
24883
+ if (statusCode === 429 && status === "RESOURCE_EXHAUSTED" && isGeminiPerMinuteLimit(message, error?.error?.details)) {
24884
+ return {
24885
+ category: "rate_limit",
24886
+ retryable: true,
24887
+ shouldFallback: false,
24888
+ statusCode,
24889
+ providerGuess: "gemini",
24890
+ retryAfterMs: parseRetryAfterMs(headers),
24891
+ reason: "Gemini per-minute rate limit exceeded"
24892
+ };
24893
+ }
24894
+ if (statusCode === 429 && isZhipuRateLimitCode(code)) {
24895
+ const rateLimitReasons = {
24896
+ 1302: "\u5E76\u53D1\u8BF7\u6C42\u8D85\u8FC7\u9650\u5236",
24897
+ 1303: "\u8BF7\u6C42\u9891\u7387\u8D85\u8FC7\u9650\u5236"
24898
+ };
24899
+ return {
24900
+ category: "rate_limit",
24901
+ retryable: true,
24902
+ shouldFallback: false,
24903
+ statusCode,
24904
+ providerGuess: "zhipu",
24905
+ retryAfterMs: parseRetryAfterMs(headers),
24906
+ reason: `Zhipu/GLM: ${rateLimitReasons[code] ?? "rate limit exceeded"}`
24907
+ };
24908
+ }
24909
+ if (statusCode === 429) {
24910
+ return {
24911
+ category: "rate_limit",
24912
+ retryable: true,
24913
+ shouldFallback: false,
24914
+ statusCode,
24915
+ retryAfterMs: parseRetryAfterMs(headers),
24916
+ reason: "Rate limit exceeded (generic 429)"
24917
+ };
24918
+ }
24919
+ return {
24920
+ category: "unknown",
24921
+ retryable: false,
24922
+ shouldFallback: false,
24923
+ statusCode,
24924
+ reason: `Unknown error: ${message.substring(0, 100)}`
24925
+ };
24926
+ }
24927
+
24928
+ // src/shared/retry-strategy.ts
24929
+ var DEFAULT_RETRY_CONFIG = {
24930
+ max_attempts: 3,
24931
+ initial_delay_ms: 1000,
24932
+ backoff_factor: 2,
24933
+ max_delay_ms: 30000,
24934
+ jitter: true,
24935
+ respect_retry_after: true
24936
+ };
24937
+ function calculateRetryDelay(attempt, config, retryAfterMs) {
24938
+ if (attempt >= config.max_attempts) {
24939
+ return {
24940
+ retryable: false,
24941
+ delay_ms: 0,
24942
+ attempt,
24943
+ reason: `max attempts (${config.max_attempts}) reached`
24944
+ };
24945
+ }
24946
+ const exponentialDelay = config.initial_delay_ms * Math.pow(config.backoff_factor, attempt);
24947
+ let baseDelay;
24948
+ let reason;
24949
+ if (config.respect_retry_after && retryAfterMs !== undefined && retryAfterMs > 0) {
24950
+ baseDelay = retryAfterMs;
24951
+ reason = "Retry-After";
24952
+ } else {
24953
+ baseDelay = exponentialDelay;
24954
+ reason = "exponential backoff";
24955
+ }
24956
+ baseDelay = Math.min(baseDelay, config.max_delay_ms);
24957
+ let finalDelay;
24958
+ if (config.jitter) {
24959
+ const jitterRange = baseDelay * 0.5;
24960
+ finalDelay = baseDelay + (Math.random() * 2 - 1) * jitterRange;
24961
+ finalDelay = Math.max(0, finalDelay);
24962
+ } else {
24963
+ finalDelay = baseDelay;
24964
+ }
24965
+ return {
24966
+ retryable: true,
24967
+ delay_ms: Math.round(finalDelay),
24968
+ attempt,
24969
+ reason
24970
+ };
24971
+ }
24972
+
24973
+ // src/hooks/runtime-fallback/index.ts
24974
+ init_logger();
24975
+ init_runtime_fallback();
24976
+ function createRuntimeFallbackHook(ctx, options) {
24977
+ const retryStates = new Map;
24978
+ const fallbackAttempts = new Map;
24979
+ const config = options?.config ?? {
24980
+ enabled: true,
24981
+ max_attempts: 3,
24982
+ initial_delay_ms: DEFAULT_RETRY_CONFIG.initial_delay_ms,
24983
+ backoff_factor: DEFAULT_RETRY_CONFIG.backoff_factor,
24984
+ max_delay_ms: DEFAULT_RETRY_CONFIG.max_delay_ms,
24985
+ respect_retry_after: DEFAULT_RETRY_CONFIG.respect_retry_after,
24986
+ jitter: DEFAULT_RETRY_CONFIG.jitter
24987
+ };
24988
+ const handler = async ({
24989
+ event
24990
+ }) => {
24991
+ if (!config.enabled)
24992
+ return false;
24993
+ if (event.type === "session.deleted") {
24994
+ const props2 = event.properties;
24995
+ const info = props2?.info;
24996
+ const sessionID2 = info?.id ?? props2?.sessionID;
24997
+ if (sessionID2) {
24998
+ retryStates.delete(sessionID2);
24999
+ fallbackAttempts.delete(sessionID2);
25000
+ }
25001
+ return false;
25002
+ }
25003
+ if (event.type !== "session.error")
25004
+ return false;
25005
+ const props = event.properties;
25006
+ const sessionID = props?.sessionID;
25007
+ const error = props?.error;
25008
+ if (!sessionID || error === undefined || error === null)
25009
+ return false;
25010
+ if (options?.sessionRecovery?.isRecoverableError(error)) {
25011
+ return false;
25012
+ }
25013
+ const classification = classifyProviderError(error);
25014
+ if (classification.category === "context_overflow") {
25015
+ return false;
25016
+ }
25017
+ if (classification.category === "auth" || classification.category === "bad_request") {
25018
+ return false;
25019
+ }
25020
+ if (classification.category === "rate_limit") {
25021
+ const state2 = retryStates.get(sessionID) ?? { attempt: 0, lastAttemptTime: Date.now() };
25022
+ const decision = calculateRetryDelay(state2.attempt, config, classification.retryAfterMs);
25023
+ if (decision.retryable) {
25024
+ retryStates.set(sessionID, {
25025
+ attempt: state2.attempt + 1,
25026
+ lastAttemptTime: Date.now()
25027
+ });
25028
+ await new Promise((resolve8) => setTimeout(resolve8, decision.delay_ms));
25029
+ return false;
25030
+ }
25031
+ retryStates.delete(sessionID);
25032
+ }
25033
+ if (classification.category !== "quota" && classification.category !== "rate_limit") {
25034
+ return false;
25035
+ }
25036
+ let currentModel = { providerID: "", modelID: "" };
25037
+ let agent = props?.agent;
25038
+ const category = props?.category;
25039
+ try {
25040
+ const messagesResp = await ctx.client.session.messages?.({ path: { id: sessionID } });
25041
+ const messages = messagesResp?.data ?? [];
25042
+ for (let i2 = messages.length - 1;i2 >= 0; i2--) {
25043
+ const info = messages[i2].info;
25044
+ if (!agent && info?.agent)
25045
+ agent = info.agent;
25046
+ const msgModel = info?.model;
25047
+ if (msgModel?.providerID && msgModel?.modelID) {
25048
+ currentModel = { providerID: msgModel.providerID, modelID: msgModel.modelID };
25049
+ break;
25050
+ }
25051
+ if (info?.providerID && info?.modelID) {
25052
+ currentModel = { providerID: info.providerID, modelID: info.modelID };
25053
+ break;
25054
+ }
25055
+ }
25056
+ } catch (messageReadError) {
25057
+ log("[runtime-fallback] failed to read session messages", { sessionID, error: String(messageReadError) });
25058
+ }
25059
+ agent ??= "sisyphus";
25060
+ const attempts = fallbackAttempts.get(sessionID) ?? [];
25061
+ const fallbackResult = resolveNextFallbackModel({
25062
+ agent,
25063
+ category,
25064
+ currentModel,
25065
+ attempts,
25066
+ configuredFallbackModels: options?.getConfiguredFallbackModels?.(agent, category),
25067
+ maxAttempts: config.max_attempts,
25068
+ lastErrorClassification: classification
25069
+ });
25070
+ if (fallbackResult.kind !== "next") {
25071
+ return false;
25072
+ }
25073
+ try {
25074
+ await ctx.client.session.prompt({
25075
+ path: { id: sessionID },
25076
+ body: {
25077
+ model: fallbackResult.model,
25078
+ parts: [{ type: "text", text: "continue" }]
25079
+ },
25080
+ query: { directory: ctx.directory }
25081
+ });
25082
+ fallbackAttempts.delete(sessionID);
25083
+ return true;
25084
+ } catch (fallbackError) {
25085
+ fallbackAttempts.set(sessionID, [
25086
+ ...attempts,
25087
+ { model: fallbackResult.model, error: classifyProviderError(fallbackError) }
25088
+ ]);
25089
+ return false;
25090
+ }
25091
+ };
25092
+ return { handler };
25093
+ }
24504
25094
  // src/features/context-injector/collector.ts
24505
25095
  var PRIORITY_ORDER = {
24506
25096
  critical: 0,
@@ -43257,6 +43847,7 @@ function initTaskToastManager(client2, concurrencyManager) {
43257
43847
  }
43258
43848
  // src/tools/delegate-task/tools.ts
43259
43849
  init_shared();
43850
+ init_runtime_fallback();
43260
43851
  var SISYPHUS_JUNIOR_AGENT = "sisyphus-junior";
43261
43852
  function parseModelString(model) {
43262
43853
  const parts = model.split("/");
@@ -43265,6 +43856,19 @@ function parseModelString(model) {
43265
43856
  }
43266
43857
  return;
43267
43858
  }
43859
+ function parseFallbackModelEntries(entries) {
43860
+ return entries?.map((entry) => {
43861
+ if ("model" in entry) {
43862
+ const parsed = parseModelString(entry.model);
43863
+ return {
43864
+ providerID: parsed?.providerID ?? "",
43865
+ modelID: parsed?.modelID ?? entry.model,
43866
+ variant: entry.variant
43867
+ };
43868
+ }
43869
+ return entry;
43870
+ });
43871
+ }
43268
43872
  function getMessageDir9(sessionID) {
43269
43873
  if (!existsSync48(MESSAGE_STORAGE))
43270
43874
  return null;
@@ -43354,8 +43958,15 @@ ${categoryPromptAppend}`;
43354
43958
  return skillContent || categoryPromptAppend;
43355
43959
  }
43356
43960
  function createDelegateTask(options) {
43357
- const { manager, client: client2, directory, userCategories, gitMasterConfig, sisyphusJuniorModel } = options;
43961
+ const { manager, client: client2, directory, userCategories, gitMasterConfig, sisyphusJuniorModel, runtimeFallbackConfig, agentFallbackModels } = options;
43358
43962
  const allCategories = { ...DEFAULT_CATEGORIES, ...userCategories };
43963
+ const getConfiguredFallbackModels = (agent, category) => {
43964
+ const agentModels = agent ? agentFallbackModels?.[agent] : undefined;
43965
+ if (agentModels)
43966
+ return parseFallbackModelEntries(agentModels);
43967
+ const categoryModels = category ? userCategories?.[category]?.fallback_models : undefined;
43968
+ return parseFallbackModelEntries(categoryModels);
43969
+ };
43359
43970
  const categoryNames = Object.keys(allCategories);
43360
43971
  const categoryExamples = categoryNames.map((k) => `'${k}'`).join(", ");
43361
43972
  const categoryList = categoryNames.map((name) => {
@@ -43684,6 +44295,7 @@ ${textContent || "(\u65E0\u6587\u672C\u8F93\u51FA)"}
43684
44295
  parentMessageID: ctx.messageID,
43685
44296
  parentModel,
43686
44297
  parentAgent,
44298
+ category: args.category,
43687
44299
  model: categoryModel,
43688
44300
  skills: args.load_skills.length > 0 ? args.load_skills : undefined,
43689
44301
  skillContent: systemContent
@@ -43784,26 +44396,85 @@ Status: ${task.status}
43784
44396
  }
43785
44397
  });
43786
44398
  } catch (promptError) {
43787
- if (toastManager && taskId !== undefined) {
43788
- toastManager.removeTask(taskId);
43789
- }
43790
- const errorMessage = promptError instanceof Error ? promptError.message : String(promptError);
43791
- if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
43792
- return formatDetailedError(new Error(`Agent "${agentToUse}" \u672A\u627E\u5230\u3002\u8BF7\u786E\u8BA4\u8BE5 agent \u5DF2\u5728 opencode.json \u4E2D\u6CE8\u518C\u6216\u7531\u63D2\u4EF6\u63D0\u4F9B\u3002`), {
43793
- operation: "\u53D1\u9001 prompt \u7ED9 agent",
44399
+ const classification = classifyProviderError(promptError);
44400
+ const canFallback = runtimeFallbackConfig?.enabled !== false && (classification.retryable || classification.shouldFallback);
44401
+ if (canFallback) {
44402
+ const attempts = [];
44403
+ let fallbackSucceeded = false;
44404
+ let currentModel = categoryModel ?? { providerID: "", modelID: "" };
44405
+ let currentClassification = classification;
44406
+ while (true) {
44407
+ const fallbackResult = resolveNextFallbackModel({
44408
+ agent: agentToUse,
44409
+ category: args.category,
44410
+ currentModel,
44411
+ attempts,
44412
+ configuredFallbackModels: getConfiguredFallbackModels(agentToUse, args.category),
44413
+ maxAttempts: runtimeFallbackConfig?.max_attempts,
44414
+ lastErrorClassification: currentClassification
44415
+ });
44416
+ if (fallbackResult.kind !== "next")
44417
+ break;
44418
+ try {
44419
+ await client2.session.prompt({
44420
+ path: { id: sessionID },
44421
+ body: {
44422
+ agent: agentToUse,
44423
+ system: systemContent,
44424
+ tools: {
44425
+ task: false,
44426
+ delegate_task: false,
44427
+ call_omo_agent: true
44428
+ },
44429
+ parts: [{ type: "text", text: args.prompt }],
44430
+ model: fallbackResult.model
44431
+ }
44432
+ });
44433
+ fallbackSucceeded = true;
44434
+ break;
44435
+ } catch (fallbackError) {
44436
+ currentClassification = classifyProviderError(fallbackError);
44437
+ attempts.push({ model: fallbackResult.model, error: currentClassification });
44438
+ currentModel = fallbackResult.model;
44439
+ if (!currentClassification.retryable && !currentClassification.shouldFallback) {
44440
+ throw fallbackError;
44441
+ }
44442
+ }
44443
+ }
44444
+ if (!fallbackSucceeded) {
44445
+ if (toastManager && taskId !== undefined) {
44446
+ toastManager.removeTask(taskId);
44447
+ }
44448
+ return formatDetailedError(promptError, {
44449
+ operation: "\u53D1\u9001 prompt",
44450
+ args,
44451
+ sessionID,
44452
+ agent: agentToUse,
44453
+ category: args.category
44454
+ });
44455
+ }
44456
+ } else {
44457
+ if (toastManager && taskId !== undefined) {
44458
+ toastManager.removeTask(taskId);
44459
+ }
44460
+ const errorMessage = promptError instanceof Error ? promptError.message : String(promptError);
44461
+ if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
44462
+ return formatDetailedError(new Error(`Agent "${agentToUse}" \u672A\u627E\u5230\u3002\u8BF7\u786E\u8BA4\u8BE5 agent \u5DF2\u5728 opencode.json \u4E2D\u6CE8\u518C\u6216\u7531\u63D2\u4EF6\u63D0\u4F9B\u3002`), {
44463
+ operation: "\u53D1\u9001 prompt \u7ED9 agent",
44464
+ args,
44465
+ sessionID,
44466
+ agent: agentToUse,
44467
+ category: args.category
44468
+ });
44469
+ }
44470
+ return formatDetailedError(promptError, {
44471
+ operation: "\u53D1\u9001 prompt",
43794
44472
  args,
43795
44473
  sessionID,
43796
44474
  agent: agentToUse,
43797
44475
  category: args.category
43798
44476
  });
43799
44477
  }
43800
- return formatDetailedError(promptError, {
43801
- operation: "\u53D1\u9001 prompt",
43802
- args,
43803
- sessionID,
43804
- agent: agentToUse,
43805
- category: args.category
43806
- });
43807
44478
  }
43808
44479
  const POLL_INTERVAL_MS = 500;
43809
44480
  const MAX_POLL_TIME_MS = 10 * 60 * 1000;
@@ -43868,7 +44539,12 @@ Session ID: ${sessionID}`;
43868
44539
  path: { id: sessionID }
43869
44540
  });
43870
44541
  if (messagesResult.error) {
43871
- return `Error fetching result: ${messagesResult.error}
44542
+ const classification = classifyProviderError(messagesResult.error);
44543
+ const diagnosis = classification.category !== "unknown" ? `
44544
+
44545
+ \uD83D\uDD0D **\u9519\u8BEF\u5206\u7C7B**: ${classification.reason}
44546
+ ${classification.shouldFallback ? "\uD83D\uDCA1 \u6B64\u9519\u8BEF\u7B26\u5408 runtime fallback \u6761\u4EF6\u3002" : classification.retryable ? "\u23F3 \u6B64\u9519\u8BEF\u53EF\u91CD\u8BD5\u3002" : ""}` : "";
44547
+ return `Error fetching result: ${messagesResult.error}${diagnosis}
43872
44548
 
43873
44549
  Session ID: ${sessionID}`;
43874
44550
  }
@@ -43906,13 +44582,14 @@ ${textContent || "(\u65E0\u6587\u672C\u8F93\u51FA)"}
43906
44582
  if (syncSessionID) {
43907
44583
  subagentSessions.delete(syncSessionID);
43908
44584
  }
43909
- return formatDetailedError(error45, {
43910
- operation: "\u6267\u884C\u4EFB\u52A1",
43911
- args,
43912
- sessionID: syncSessionID,
43913
- agent: agentToUse,
43914
- category: args.category
43915
- });
44585
+ const classification = classifyProviderError(error45);
44586
+ const diagnosis = classification.category !== "unknown" ? `
44587
+
44588
+ \uD83D\uDD0D **\u9519\u8BEF\u5206\u7C7B**: ${classification.reason}
44589
+ ${classification.shouldFallback ? "\uD83D\uDCA1 \u6B64\u9519\u8BEF\u7B26\u5408 runtime fallback \u6761\u4EF6\u3002" : classification.retryable ? "\u23F3 \u6B64\u9519\u8BEF\u53EF\u91CD\u8BD5\u3002" : ""}` : "";
44590
+ return `\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error45 instanceof Error ? error45.message : String(error45)}${diagnosis}
44591
+
44592
+ Session ID: ${syncSessionID ?? "unknown"}`;
43916
44593
  }
43917
44594
  }
43918
44595
  });
@@ -44041,6 +44718,9 @@ class ConcurrencyManager {
44041
44718
  }
44042
44719
  }
44043
44720
 
44721
+ // src/features/background-agent/manager.ts
44722
+ init_runtime_fallback();
44723
+
44044
44724
  // src/features/background-agent/perf-aggregator.ts
44045
44725
  function percentile(sorted, p) {
44046
44726
  if (sorted.length === 0)
@@ -44161,6 +44841,7 @@ class BackgroundManager {
44161
44841
  parentMessageID: input.parentMessageID,
44162
44842
  parentModel: input.parentModel,
44163
44843
  parentAgent: input.parentAgent,
44844
+ category: input.category,
44164
44845
  model: input.model,
44165
44846
  maxSteps: this.config?.maxSteps,
44166
44847
  maxRuntimeMs: this.config?.maxRuntimeMs,
@@ -44289,27 +44970,79 @@ class BackgroundManager {
44289
44970
  },
44290
44971
  parts: [{ type: "text", text: input.prompt }]
44291
44972
  }
44292
- }).catch((error45) => {
44293
- log("[background-agent] promptAsync error:", error45);
44294
- const existingTask = this.findBySession(sessionID);
44295
- if (existingTask) {
44296
- existingTask.status = "error";
44297
- const errorMessage = error45 instanceof Error ? error45.message : String(error45);
44298
- if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
44299
- existingTask.error = `Agent "${input.agent}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.`;
44300
- } else {
44301
- existingTask.error = errorMessage;
44973
+ }).catch(async (error45) => {
44974
+ await this.handlePromptFailure(sessionID, input, error45);
44975
+ });
44976
+ }
44977
+ async handlePromptFailure(sessionID, input, error45) {
44978
+ log("[background-agent] promptAsync error:", error45);
44979
+ const existingTask = this.findBySession(sessionID);
44980
+ if (!existingTask)
44981
+ return;
44982
+ const runtimeFallback = this.config?.runtimeFallback;
44983
+ const classification = classifyProviderError(error45);
44984
+ const canFallback = runtimeFallback?.enabled !== false && (classification.retryable || classification.shouldFallback);
44985
+ if (canFallback) {
44986
+ let currentError = error45;
44987
+ let currentClassification = classification;
44988
+ let currentModel = input.model ?? { providerID: "", modelID: "" };
44989
+ while (true) {
44990
+ const attempts = existingTask.attempts ?? [];
44991
+ const fallbackResult = resolveNextFallbackModel({
44992
+ agent: input.agent,
44993
+ category: input.category,
44994
+ currentModel,
44995
+ attempts,
44996
+ maxAttempts: runtimeFallback?.max_attempts,
44997
+ lastErrorClassification: currentClassification
44998
+ });
44999
+ if (fallbackResult.kind !== "next") {
45000
+ existingTask.error = fallbackResult.kind === "exhausted" ? `All fallback models exhausted. Last error: ${currentClassification.reason}` : fallbackResult.reason;
45001
+ break;
44302
45002
  }
44303
- existingTask.completedAt = new Date;
44304
- if (existingTask.concurrencyKey) {
44305
- this.concurrencyManager.release(existingTask.concurrencyKey);
44306
- existingTask.concurrencyKey = undefined;
45003
+ log("[background-agent] Fallback to model:", fallbackResult.model);
45004
+ try {
45005
+ await this.client.session.prompt({
45006
+ path: { id: sessionID },
45007
+ body: {
45008
+ agent: input.agent,
45009
+ model: fallbackResult.model,
45010
+ parts: [{ type: "text", text: input.prompt }]
45011
+ }
45012
+ });
45013
+ existingTask.attempts = attempts;
45014
+ return;
45015
+ } catch (retryError) {
45016
+ currentError = retryError;
45017
+ currentClassification = classifyProviderError(currentError);
45018
+ existingTask.attempts = [...attempts, {
45019
+ model: fallbackResult.model,
45020
+ error: currentClassification
45021
+ }];
45022
+ currentModel = fallbackResult.model;
45023
+ if (!currentClassification.retryable && !currentClassification.shouldFallback) {
45024
+ existingTask.error = `Fallback failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`;
45025
+ break;
45026
+ }
44307
45027
  }
44308
- this.markForNotification(existingTask);
44309
- this.notifyParentSession(existingTask).catch((err) => {
44310
- log("[background-agent] Failed to notify on error:", err);
44311
- });
44312
45028
  }
45029
+ } else {
45030
+ const errorMessage = error45 instanceof Error ? error45.message : String(error45);
45031
+ if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
45032
+ existingTask.error = `Agent "${input.agent}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.`;
45033
+ } else {
45034
+ existingTask.error = errorMessage;
45035
+ }
45036
+ }
45037
+ existingTask.status = "error";
45038
+ existingTask.completedAt = new Date;
45039
+ if (existingTask.concurrencyKey) {
45040
+ this.concurrencyManager.release(existingTask.concurrencyKey);
45041
+ existingTask.concurrencyKey = undefined;
45042
+ }
45043
+ this.markForNotification(existingTask);
45044
+ this.notifyParentSession(existingTask).catch((err) => {
45045
+ log("[background-agent] Failed to notify on error:", err);
44313
45046
  });
44314
45047
  }
44315
45048
  getTask(id) {
@@ -63126,6 +63859,7 @@ var HookNameSchema = exports_external2.enum([
63126
63859
  "auto-slash-command",
63127
63860
  "edit-error-recovery",
63128
63861
  "delegate-task-retry",
63862
+ "runtime-fallback",
63129
63863
  "prometheus-md-only",
63130
63864
  "perf-profiler",
63131
63865
  "start-work",
@@ -63135,6 +63869,23 @@ var BuiltinCommandNameSchema = exports_external2.enum([
63135
63869
  "init-deep",
63136
63870
  "start-work"
63137
63871
  ]);
63872
+ var ProviderModelStringSchema = exports_external2.string().refine((value) => {
63873
+ const separatorIndex = value.indexOf("/");
63874
+ return separatorIndex > 0 && separatorIndex < value.length - 1;
63875
+ }, "Expected provider/model format");
63876
+ var NonEmptyStringSchema = exports_external2.string().min(1);
63877
+ var FallbackModelEntrySchema = exports_external2.union([
63878
+ exports_external2.object({
63879
+ model: ProviderModelStringSchema,
63880
+ variant: exports_external2.string().optional()
63881
+ }).strict(),
63882
+ exports_external2.object({
63883
+ providerID: NonEmptyStringSchema,
63884
+ modelID: NonEmptyStringSchema,
63885
+ variant: exports_external2.string().optional()
63886
+ }).strict()
63887
+ ]);
63888
+ var FallbackModelsSchema = exports_external2.array(FallbackModelEntrySchema);
63138
63889
  var AgentOverrideConfigSchema = exports_external2.object({
63139
63890
  model: exports_external2.string().optional(),
63140
63891
  variant: exports_external2.string().optional(),
@@ -63149,7 +63900,8 @@ var AgentOverrideConfigSchema = exports_external2.object({
63149
63900
  description: exports_external2.string().optional(),
63150
63901
  mode: exports_external2.enum(["subagent", "primary", "all"]).optional(),
63151
63902
  color: exports_external2.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
63152
- permission: AgentPermissionSchema.optional()
63903
+ permission: AgentPermissionSchema.optional(),
63904
+ fallback_models: FallbackModelsSchema.optional()
63153
63905
  });
63154
63906
  var AgentOverridesSchema = exports_external2.object({
63155
63907
  build: AgentOverrideConfigSchema.optional(),
@@ -63185,6 +63937,7 @@ var CategoryConfigSchema = exports_external2.object({
63185
63937
  description: exports_external2.string().optional(),
63186
63938
  model: exports_external2.string().optional(),
63187
63939
  variant: exports_external2.string().optional(),
63940
+ fallback_models: FallbackModelsSchema.optional(),
63188
63941
  temperature: exports_external2.number().min(0).max(2).optional(),
63189
63942
  top_p: exports_external2.number().min(0).max(1).optional(),
63190
63943
  maxTokens: exports_external2.number().optional(),
@@ -63312,6 +64065,15 @@ var GitMasterConfigSchema = exports_external2.object({
63312
64065
  commit_footer: exports_external2.boolean().default(true),
63313
64066
  include_co_authored_by: exports_external2.boolean().default(true)
63314
64067
  });
64068
+ var RuntimeFallbackConfigSchema = exports_external2.object({
64069
+ enabled: exports_external2.boolean().default(true),
64070
+ max_attempts: exports_external2.number().min(0).default(3),
64071
+ initial_delay_ms: exports_external2.number().min(0).default(2000),
64072
+ backoff_factor: exports_external2.number().min(1).default(2),
64073
+ max_delay_ms: exports_external2.number().min(0).default(30000),
64074
+ respect_retry_after: exports_external2.boolean().default(true),
64075
+ jitter: exports_external2.boolean().default(true)
64076
+ });
63315
64077
  var OhMyOpenCodeConfigSchema = exports_external2.object({
63316
64078
  $schema: exports_external2.string().optional(),
63317
64079
  disabled_mcps: exports_external2.array(AnyMcpNameSchema).optional(),
@@ -63330,7 +64092,8 @@ var OhMyOpenCodeConfigSchema = exports_external2.object({
63330
64092
  ralph_loop: RalphLoopConfigSchema.optional(),
63331
64093
  background_task: BackgroundTaskConfigSchema.optional(),
63332
64094
  notification: NotificationConfigSchema.optional(),
63333
- git_master: GitMasterConfigSchema.optional()
64095
+ git_master: GitMasterConfigSchema.optional(),
64096
+ runtime_fallback: RuntimeFallbackConfigSchema.optional()
63334
64097
  });
63335
64098
  // src/plugin-config.ts
63336
64099
  init_shared();
@@ -68386,9 +69149,31 @@ var OhMyOpenCodePlugin = async (ctx) => {
68386
69149
  const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
68387
69150
  const firstMessageVariantGate = createFirstMessageVariantGate();
68388
69151
  const isHookEnabled = (hookName) => !disabledHooks.has(hookName);
69152
+ const parseConfiguredFallbackModels = (entries) => entries?.map((entry) => {
69153
+ if ("model" in entry) {
69154
+ const separatorIndex = entry.model.indexOf("/");
69155
+ return {
69156
+ providerID: entry.model.slice(0, separatorIndex),
69157
+ modelID: entry.model.slice(separatorIndex + 1),
69158
+ variant: entry.variant
69159
+ };
69160
+ }
69161
+ return entry;
69162
+ });
68389
69163
  const modelCacheState = createModelCacheState();
68390
69164
  const contextWindowMonitor = isHookEnabled("context-window-monitor") ? createContextWindowMonitorHook(ctx) : null;
68391
69165
  const sessionRecovery = isHookEnabled("session-recovery") ? createSessionRecoveryHook(ctx, { experimental: pluginConfig.experimental }) : null;
69166
+ const runtimeFallback = isHookEnabled("runtime-fallback") && pluginConfig.runtime_fallback?.enabled !== false ? createRuntimeFallbackHook(ctx, {
69167
+ config: pluginConfig.runtime_fallback,
69168
+ sessionRecovery: sessionRecovery ?? undefined,
69169
+ getConfiguredFallbackModels: (agent, category) => {
69170
+ const agentModels = agent && pluginConfig.agents?.[agent]?.fallback_models;
69171
+ if (agentModels)
69172
+ return parseConfiguredFallbackModels(agentModels);
69173
+ const categoryModels = category ? pluginConfig.categories?.[category]?.fallback_models : undefined;
69174
+ return parseConfiguredFallbackModels(categoryModels);
69175
+ }
69176
+ }) : null;
68392
69177
  let sessionNotification = null;
68393
69178
  if (isHookEnabled("session-notification")) {
68394
69179
  const forceEnable = pluginConfig.notification?.force_enable ?? false;
@@ -68441,7 +69226,10 @@ var OhMyOpenCodePlugin = async (ctx) => {
68441
69226
  const prometheusMdOnly = isHookEnabled("prometheus-md-only") ? createPrometheusMdOnlyHook(ctx) : null;
68442
69227
  const questionLabelTruncator = createQuestionLabelTruncatorHook();
68443
69228
  const taskResumeInfo = createTaskResumeInfoHook();
68444
- const backgroundManager = new BackgroundManager(ctx, pluginConfig.background_task);
69229
+ const backgroundManager = new BackgroundManager(ctx, {
69230
+ ...pluginConfig.background_task,
69231
+ runtimeFallback: pluginConfig.runtime_fallback
69232
+ });
68445
69233
  const atlasHook = isHookEnabled("atlas") ? createAtlasHook(ctx, { directory: ctx.directory, backgroundManager }) : null;
68446
69234
  const perfTracer = pluginConfig.experimental?.profiling?.enabled ? new PerfTracer({
68447
69235
  enabled: true,
@@ -68488,7 +69276,9 @@ var OhMyOpenCodePlugin = async (ctx) => {
68488
69276
  directory: ctx.directory,
68489
69277
  userCategories: pluginConfig.categories,
68490
69278
  gitMasterConfig: pluginConfig.git_master,
68491
- sisyphusJuniorModel: pluginConfig.agents?.["sisyphus-junior"]?.model
69279
+ sisyphusJuniorModel: pluginConfig.agents?.["sisyphus-junior"]?.model,
69280
+ runtimeFallbackConfig: pluginConfig.runtime_fallback,
69281
+ agentFallbackModels: Object.fromEntries(Object.entries(pluginConfig.agents ?? {}).map(([agent, config4]) => [agent, config4?.fallback_models]))
68492
69282
  });
68493
69283
  const disabledSkills = new Set(pluginConfig.disabled_skills ?? []);
68494
69284
  const systemMcpNames = getSystemMcpServerNames();
@@ -68657,6 +69447,8 @@ var OhMyOpenCodePlugin = async (ctx) => {
68657
69447
  hookCount++;
68658
69448
  await wrapWithTiming(perfTracer, "event", "anthropicContextWindowLimitRecovery", () => anthropicContextWindowLimitRecovery?.event(input), evtSessionID);
68659
69449
  hookCount++;
69450
+ await wrapWithTiming(perfTracer, "event", "runtimeFallback", () => runtimeFallback?.handler(input), evtSessionID);
69451
+ hookCount++;
68660
69452
  await wrapWithTiming(perfTracer, "event", "agentUsageReminder", () => agentUsageReminder?.event(input), evtSessionID);
68661
69453
  hookCount++;
68662
69454
  await wrapWithTiming(perfTracer, "event", "interactiveBashSession", () => interactiveBashSession?.event(input), evtSessionID);