@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/cli/index.js +40 -3
- package/dist/config/index.d.ts +2 -2
- package/dist/config/schema.d.ts +270 -0
- package/dist/features/background-agent/manager.d.ts +6 -2
- package/dist/features/background-agent/types.d.ts +6 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/runtime-fallback/index.d.ts +27 -0
- package/dist/hooks/runtime-fallback/index.test.d.ts +1 -0
- package/dist/index.js +838 -46
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/provider-error-classifier.d.ts +23 -0
- package/dist/shared/provider-error-classifier.test.d.ts +1 -0
- package/dist/shared/retry-strategy.d.ts +39 -0
- package/dist/shared/retry-strategy.test.d.ts +1 -0
- package/dist/shared/runtime-fallback.d.ts +60 -0
- package/dist/shared/runtime-fallback.test.d.ts +1 -0
- package/dist/tools/delegate-task/tools.d.ts +3 -1
- package/package.json +1 -1
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
|
-
|
|
43788
|
-
|
|
43789
|
-
|
|
43790
|
-
|
|
43791
|
-
|
|
43792
|
-
|
|
43793
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43910
|
-
|
|
43911
|
-
|
|
43912
|
-
|
|
43913
|
-
|
|
43914
|
-
|
|
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
|
-
|
|
44294
|
-
|
|
44295
|
-
|
|
44296
|
-
|
|
44297
|
-
|
|
44298
|
-
|
|
44299
|
-
|
|
44300
|
-
|
|
44301
|
-
|
|
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
|
-
|
|
44304
|
-
|
|
44305
|
-
this.
|
|
44306
|
-
|
|
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,
|
|
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);
|