@skj1724/oh-my-opencode 3.19.7 → 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 +21 -2
- package/dist/config/index.d.ts +2 -2
- package/dist/config/schema.d.ts +249 -0
- package/dist/features/background-agent/manager.d.ts +6 -2
- package/dist/features/background-agent/types.d.ts +2 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/runtime-fallback/index.d.ts +4 -0
- package/dist/index.js +694 -345
- package/dist/shared/provider-error-classifier.d.ts +1 -1
- package/dist/shared/runtime-fallback.d.ts +15 -1
- package/dist/tools/delegate-task/tools.d.ts +3 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5140,7 +5140,7 @@ function getChain(agent, category) {
|
|
|
5140
5140
|
if (category && CATEGORY_MODEL_REQUIREMENTS[category]) {
|
|
5141
5141
|
return CATEGORY_MODEL_REQUIREMENTS[category].fallbackChain;
|
|
5142
5142
|
}
|
|
5143
|
-
|
|
5143
|
+
return;
|
|
5144
5144
|
}
|
|
5145
5145
|
function resolveNextFallbackModel(input) {
|
|
5146
5146
|
const {
|
|
@@ -5149,40 +5149,58 @@ function resolveNextFallbackModel(input) {
|
|
|
5149
5149
|
currentModel,
|
|
5150
5150
|
attempts,
|
|
5151
5151
|
availableModels,
|
|
5152
|
-
lastErrorClassification
|
|
5152
|
+
lastErrorClassification,
|
|
5153
|
+
configuredFallbackModels,
|
|
5154
|
+
maxAttempts
|
|
5153
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
|
+
}
|
|
5154
5164
|
const chain = getChain(agent, category);
|
|
5155
|
-
const candidates = expandChain(chain);
|
|
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
|
+
}
|
|
5156
5172
|
const skipKeys = new Set;
|
|
5157
5173
|
skipKeys.add(modelKey(currentModel));
|
|
5158
5174
|
for (const a of attempts) {
|
|
5159
5175
|
skipKeys.add(modelKey(a.model));
|
|
5160
5176
|
}
|
|
5161
|
-
const
|
|
5162
|
-
const currentKey = modelKey(currentModel);
|
|
5163
|
-
const isInAttempts = attempts.some((a) => modelKey(a.model) === currentKey);
|
|
5164
|
-
if (!isInAttempts) {
|
|
5165
|
-
resultAttempts.push({ model: currentModel });
|
|
5166
|
-
}
|
|
5177
|
+
const skipped = [];
|
|
5167
5178
|
const hasAvailabilityFilter = availableModels != null && availableModels.size > 0;
|
|
5168
5179
|
for (const candidate of candidates) {
|
|
5169
5180
|
const key = modelKey(candidate);
|
|
5170
|
-
if (skipKeys.has(key))
|
|
5181
|
+
if (skipKeys.has(key)) {
|
|
5182
|
+
skipped.push({ model: candidate, reason: "already attempted or current model" });
|
|
5171
5183
|
continue;
|
|
5184
|
+
}
|
|
5172
5185
|
if (hasAvailabilityFilter) {
|
|
5173
5186
|
const match = fuzzyMatchModel(key, availableModels, [candidate.providerID]);
|
|
5174
|
-
if (!match)
|
|
5187
|
+
if (!match) {
|
|
5188
|
+
skipped.push({ model: candidate, reason: "model unavailable" });
|
|
5175
5189
|
continue;
|
|
5190
|
+
}
|
|
5176
5191
|
}
|
|
5177
5192
|
return {
|
|
5178
5193
|
kind: "next",
|
|
5179
5194
|
model: candidate,
|
|
5180
|
-
attempts
|
|
5195
|
+
attempts,
|
|
5196
|
+
skipped
|
|
5181
5197
|
};
|
|
5182
5198
|
}
|
|
5183
5199
|
return {
|
|
5184
5200
|
kind: "exhausted",
|
|
5185
|
-
attempts
|
|
5201
|
+
attempts,
|
|
5202
|
+
reason: "No fallback candidates available",
|
|
5203
|
+
skipped,
|
|
5186
5204
|
lastErrorClassification
|
|
5187
5205
|
};
|
|
5188
5206
|
}
|
|
@@ -24629,6 +24647,450 @@ function createPerfProfilerHook(options) {
|
|
|
24629
24647
|
"chat.message": async () => {}
|
|
24630
24648
|
};
|
|
24631
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
|
+
}
|
|
24632
25094
|
// src/features/context-injector/collector.ts
|
|
24633
25095
|
var PRIORITY_ORDER = {
|
|
24634
25096
|
critical: 0,
|
|
@@ -43385,256 +43847,7 @@ function initTaskToastManager(client2, concurrencyManager) {
|
|
|
43385
43847
|
}
|
|
43386
43848
|
// src/tools/delegate-task/tools.ts
|
|
43387
43849
|
init_shared();
|
|
43388
|
-
|
|
43389
|
-
// src/shared/provider-error-classifier.ts
|
|
43390
|
-
function extractErrorInfo(error45) {
|
|
43391
|
-
if (typeof error45 === "string") {
|
|
43392
|
-
return { message: error45 };
|
|
43393
|
-
}
|
|
43394
|
-
if (error45 instanceof Error) {
|
|
43395
|
-
const anyErr = error45;
|
|
43396
|
-
return {
|
|
43397
|
-
statusCode: anyErr.status ?? anyErr.statusCode ?? anyErr.httpStatus,
|
|
43398
|
-
code: anyErr.code ?? anyErr.error?.code,
|
|
43399
|
-
type: anyErr.type ?? anyErr.error?.type,
|
|
43400
|
-
message: error45.message,
|
|
43401
|
-
status: anyErr.status ?? anyErr.error?.status,
|
|
43402
|
-
headers: anyErr.headers
|
|
43403
|
-
};
|
|
43404
|
-
}
|
|
43405
|
-
if (typeof error45 === "object" && error45 !== null) {
|
|
43406
|
-
const obj = error45;
|
|
43407
|
-
const inner = obj.error ?? {};
|
|
43408
|
-
return {
|
|
43409
|
-
statusCode: obj.status ?? obj.statusCode ?? inner.status,
|
|
43410
|
-
code: inner.code ?? obj.code,
|
|
43411
|
-
type: inner.type ?? obj.type,
|
|
43412
|
-
message: inner.message ?? obj.message ?? String(error45),
|
|
43413
|
-
status: inner.status ?? obj.status,
|
|
43414
|
-
headers: obj.headers
|
|
43415
|
-
};
|
|
43416
|
-
}
|
|
43417
|
-
return { message: String(error45) };
|
|
43418
|
-
}
|
|
43419
|
-
function parseRetryAfterMs(headers) {
|
|
43420
|
-
if (!headers)
|
|
43421
|
-
return;
|
|
43422
|
-
const retryAfter = headers["retry-after"] ?? headers["Retry-After"];
|
|
43423
|
-
if (retryAfter) {
|
|
43424
|
-
const seconds = Number(retryAfter);
|
|
43425
|
-
if (!isNaN(seconds) && seconds > 0) {
|
|
43426
|
-
return seconds * 1000;
|
|
43427
|
-
}
|
|
43428
|
-
}
|
|
43429
|
-
const reset = headers["x-ratelimit-reset"] ?? headers["X-Ratelimit-Reset"];
|
|
43430
|
-
if (reset) {
|
|
43431
|
-
const resetTimestamp = Number(reset);
|
|
43432
|
-
if (!isNaN(resetTimestamp)) {
|
|
43433
|
-
const resetMs = resetTimestamp > 1000000000000 ? resetTimestamp : resetTimestamp * 1000;
|
|
43434
|
-
const delayMs = resetMs - Date.now();
|
|
43435
|
-
return delayMs > 0 ? delayMs : 0;
|
|
43436
|
-
}
|
|
43437
|
-
}
|
|
43438
|
-
return;
|
|
43439
|
-
}
|
|
43440
|
-
function isContextOverflow(message, code) {
|
|
43441
|
-
const lowerMessage = message.toLowerCase();
|
|
43442
|
-
return lowerMessage.includes("context_length_exceeded") || lowerMessage.includes("prompt is too long") || lowerMessage.includes("maximum context length") || code === "context_length_exceeded";
|
|
43443
|
-
}
|
|
43444
|
-
function isZhipuQuotaCode(code) {
|
|
43445
|
-
if (typeof code !== "number")
|
|
43446
|
-
return false;
|
|
43447
|
-
return [1113, 1304, 1308, 1309].includes(code);
|
|
43448
|
-
}
|
|
43449
|
-
function isZhipuRateLimitCode(code) {
|
|
43450
|
-
if (typeof code !== "number")
|
|
43451
|
-
return false;
|
|
43452
|
-
return [1302, 1303].includes(code);
|
|
43453
|
-
}
|
|
43454
|
-
function isZhipuOverloadedCode(code) {
|
|
43455
|
-
if (typeof code !== "number")
|
|
43456
|
-
return false;
|
|
43457
|
-
return code === 1312;
|
|
43458
|
-
}
|
|
43459
|
-
function isGeminiQuotaDetails(details) {
|
|
43460
|
-
if (!Array.isArray(details))
|
|
43461
|
-
return false;
|
|
43462
|
-
return details.some((d) => d?.["@type"]?.includes("QuotaFailure") || d?.["@type"]?.includes("google.rpc.QuotaFailure"));
|
|
43463
|
-
}
|
|
43464
|
-
function isGeminiPerMinuteLimit(message, details) {
|
|
43465
|
-
const lowerMessage = message.toLowerCase();
|
|
43466
|
-
if (lowerMessage.includes("per_minute") || lowerMessage.includes("per minute")) {
|
|
43467
|
-
return true;
|
|
43468
|
-
}
|
|
43469
|
-
if (Array.isArray(details)) {
|
|
43470
|
-
return details.some((d) => d?.["@type"]?.includes("RetryInfo"));
|
|
43471
|
-
}
|
|
43472
|
-
return false;
|
|
43473
|
-
}
|
|
43474
|
-
function classifyProviderError(error45) {
|
|
43475
|
-
const info = extractErrorInfo(error45);
|
|
43476
|
-
const { statusCode, code, type: type2, message, status, headers } = info;
|
|
43477
|
-
if (isContextOverflow(message, code)) {
|
|
43478
|
-
return {
|
|
43479
|
-
category: "context_overflow",
|
|
43480
|
-
retryable: false,
|
|
43481
|
-
shouldFallback: false,
|
|
43482
|
-
statusCode,
|
|
43483
|
-
reason: "Context length exceeded, prompt too long for model"
|
|
43484
|
-
};
|
|
43485
|
-
}
|
|
43486
|
-
if (statusCode === 401 || statusCode === 403) {
|
|
43487
|
-
return {
|
|
43488
|
-
category: "auth",
|
|
43489
|
-
retryable: false,
|
|
43490
|
-
shouldFallback: false,
|
|
43491
|
-
statusCode,
|
|
43492
|
-
reason: statusCode === 401 ? "Invalid API key or authentication" : "Permission denied"
|
|
43493
|
-
};
|
|
43494
|
-
}
|
|
43495
|
-
if (statusCode === 400) {
|
|
43496
|
-
return {
|
|
43497
|
-
category: "bad_request",
|
|
43498
|
-
retryable: false,
|
|
43499
|
-
shouldFallback: false,
|
|
43500
|
-
statusCode,
|
|
43501
|
-
reason: "Invalid request parameters"
|
|
43502
|
-
};
|
|
43503
|
-
}
|
|
43504
|
-
if (statusCode === 429 && (code === "insufficient_quota" || type2 === "insufficient_quota")) {
|
|
43505
|
-
return {
|
|
43506
|
-
category: "quota",
|
|
43507
|
-
retryable: false,
|
|
43508
|
-
shouldFallback: true,
|
|
43509
|
-
statusCode,
|
|
43510
|
-
providerGuess: "openai",
|
|
43511
|
-
reason: "OpenAI quota exceeded, billing issue"
|
|
43512
|
-
};
|
|
43513
|
-
}
|
|
43514
|
-
if (statusCode === 402 && type2 === "billing_error") {
|
|
43515
|
-
return {
|
|
43516
|
-
category: "quota",
|
|
43517
|
-
retryable: false,
|
|
43518
|
-
shouldFallback: true,
|
|
43519
|
-
statusCode,
|
|
43520
|
-
providerGuess: "anthropic",
|
|
43521
|
-
reason: "Anthropic billing error, payment required"
|
|
43522
|
-
};
|
|
43523
|
-
}
|
|
43524
|
-
if (statusCode === 429 && status === "RESOURCE_EXHAUSTED" && isGeminiQuotaDetails(info.headers ? undefined : error45?.error?.details)) {
|
|
43525
|
-
return {
|
|
43526
|
-
category: "quota",
|
|
43527
|
-
retryable: false,
|
|
43528
|
-
shouldFallback: true,
|
|
43529
|
-
statusCode,
|
|
43530
|
-
providerGuess: "gemini",
|
|
43531
|
-
reason: "Gemini daily quota exceeded"
|
|
43532
|
-
};
|
|
43533
|
-
}
|
|
43534
|
-
if (statusCode === 429 && isZhipuQuotaCode(code)) {
|
|
43535
|
-
const quotaReasons = {
|
|
43536
|
-
1113: "\u8D26\u6237\u6B20\u8D39",
|
|
43537
|
-
1304: "\u8C03\u7528\u6B21\u6570\u8D85\u8FC7\u9650\u989D",
|
|
43538
|
-
1308: "\u4F7F\u7528\u91CF\u8D85\u8FC7\u4E0A\u9650",
|
|
43539
|
-
1309: "\u5957\u9910\u5DF2\u5230\u671F"
|
|
43540
|
-
};
|
|
43541
|
-
return {
|
|
43542
|
-
category: "quota",
|
|
43543
|
-
retryable: false,
|
|
43544
|
-
shouldFallback: true,
|
|
43545
|
-
statusCode,
|
|
43546
|
-
providerGuess: "zhipu",
|
|
43547
|
-
reason: `Zhipu/GLM: ${quotaReasons[code] ?? "quota exceeded"}`
|
|
43548
|
-
};
|
|
43549
|
-
}
|
|
43550
|
-
if (statusCode === 529 && type2 === "overloaded_error") {
|
|
43551
|
-
return {
|
|
43552
|
-
category: "overloaded",
|
|
43553
|
-
retryable: true,
|
|
43554
|
-
shouldFallback: false,
|
|
43555
|
-
statusCode,
|
|
43556
|
-
providerGuess: "anthropic",
|
|
43557
|
-
reason: "Anthropic API overloaded"
|
|
43558
|
-
};
|
|
43559
|
-
}
|
|
43560
|
-
if (statusCode === 429 && isZhipuOverloadedCode(code)) {
|
|
43561
|
-
return {
|
|
43562
|
-
category: "overloaded",
|
|
43563
|
-
retryable: true,
|
|
43564
|
-
shouldFallback: false,
|
|
43565
|
-
statusCode,
|
|
43566
|
-
providerGuess: "zhipu",
|
|
43567
|
-
reason: "Zhipu/GLM: \u5F53\u524D\u8D1F\u8F7D\u8FC7\u9AD8"
|
|
43568
|
-
};
|
|
43569
|
-
}
|
|
43570
|
-
if (statusCode === 429 && type2 === "rate_limit_error") {
|
|
43571
|
-
return {
|
|
43572
|
-
category: "rate_limit",
|
|
43573
|
-
retryable: true,
|
|
43574
|
-
shouldFallback: false,
|
|
43575
|
-
statusCode,
|
|
43576
|
-
providerGuess: "anthropic",
|
|
43577
|
-
retryAfterMs: parseRetryAfterMs(headers),
|
|
43578
|
-
reason: "Anthropic rate limit exceeded"
|
|
43579
|
-
};
|
|
43580
|
-
}
|
|
43581
|
-
if (statusCode === 429 && code === "rate_limit_exceeded") {
|
|
43582
|
-
return {
|
|
43583
|
-
category: "rate_limit",
|
|
43584
|
-
retryable: true,
|
|
43585
|
-
shouldFallback: false,
|
|
43586
|
-
statusCode,
|
|
43587
|
-
providerGuess: "openai",
|
|
43588
|
-
retryAfterMs: parseRetryAfterMs(headers),
|
|
43589
|
-
reason: "OpenAI rate limit exceeded"
|
|
43590
|
-
};
|
|
43591
|
-
}
|
|
43592
|
-
if (statusCode === 429 && status === "RESOURCE_EXHAUSTED" && isGeminiPerMinuteLimit(message, error45?.error?.details)) {
|
|
43593
|
-
return {
|
|
43594
|
-
category: "rate_limit",
|
|
43595
|
-
retryable: true,
|
|
43596
|
-
shouldFallback: false,
|
|
43597
|
-
statusCode,
|
|
43598
|
-
providerGuess: "gemini",
|
|
43599
|
-
retryAfterMs: parseRetryAfterMs(headers),
|
|
43600
|
-
reason: "Gemini per-minute rate limit exceeded"
|
|
43601
|
-
};
|
|
43602
|
-
}
|
|
43603
|
-
if (statusCode === 429 && isZhipuRateLimitCode(code)) {
|
|
43604
|
-
const rateLimitReasons = {
|
|
43605
|
-
1302: "\u5E76\u53D1\u8BF7\u6C42\u8D85\u8FC7\u9650\u5236",
|
|
43606
|
-
1303: "\u8BF7\u6C42\u9891\u7387\u8D85\u8FC7\u9650\u5236"
|
|
43607
|
-
};
|
|
43608
|
-
return {
|
|
43609
|
-
category: "rate_limit",
|
|
43610
|
-
retryable: true,
|
|
43611
|
-
shouldFallback: false,
|
|
43612
|
-
statusCode,
|
|
43613
|
-
providerGuess: "zhipu",
|
|
43614
|
-
retryAfterMs: parseRetryAfterMs(headers),
|
|
43615
|
-
reason: `Zhipu/GLM: ${rateLimitReasons[code] ?? "rate limit exceeded"}`
|
|
43616
|
-
};
|
|
43617
|
-
}
|
|
43618
|
-
if (statusCode === 429) {
|
|
43619
|
-
return {
|
|
43620
|
-
category: "rate_limit",
|
|
43621
|
-
retryable: true,
|
|
43622
|
-
shouldFallback: false,
|
|
43623
|
-
statusCode,
|
|
43624
|
-
retryAfterMs: parseRetryAfterMs(headers),
|
|
43625
|
-
reason: "Rate limit exceeded (generic 429)"
|
|
43626
|
-
};
|
|
43627
|
-
}
|
|
43628
|
-
return {
|
|
43629
|
-
category: "unknown",
|
|
43630
|
-
retryable: false,
|
|
43631
|
-
shouldFallback: false,
|
|
43632
|
-
statusCode,
|
|
43633
|
-
reason: `Unknown error: ${message.substring(0, 100)}`
|
|
43634
|
-
};
|
|
43635
|
-
}
|
|
43636
|
-
|
|
43637
|
-
// src/tools/delegate-task/tools.ts
|
|
43850
|
+
init_runtime_fallback();
|
|
43638
43851
|
var SISYPHUS_JUNIOR_AGENT = "sisyphus-junior";
|
|
43639
43852
|
function parseModelString(model) {
|
|
43640
43853
|
const parts = model.split("/");
|
|
@@ -43643,6 +43856,19 @@ function parseModelString(model) {
|
|
|
43643
43856
|
}
|
|
43644
43857
|
return;
|
|
43645
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
|
+
}
|
|
43646
43872
|
function getMessageDir9(sessionID) {
|
|
43647
43873
|
if (!existsSync48(MESSAGE_STORAGE))
|
|
43648
43874
|
return null;
|
|
@@ -43732,8 +43958,15 @@ ${categoryPromptAppend}`;
|
|
|
43732
43958
|
return skillContent || categoryPromptAppend;
|
|
43733
43959
|
}
|
|
43734
43960
|
function createDelegateTask(options) {
|
|
43735
|
-
const { manager, client: client2, directory, userCategories, gitMasterConfig, sisyphusJuniorModel } = options;
|
|
43961
|
+
const { manager, client: client2, directory, userCategories, gitMasterConfig, sisyphusJuniorModel, runtimeFallbackConfig, agentFallbackModels } = options;
|
|
43736
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
|
+
};
|
|
43737
43970
|
const categoryNames = Object.keys(allCategories);
|
|
43738
43971
|
const categoryExamples = categoryNames.map((k) => `'${k}'`).join(", ");
|
|
43739
43972
|
const categoryList = categoryNames.map((name) => {
|
|
@@ -44062,6 +44295,7 @@ ${textContent || "(\u65E0\u6587\u672C\u8F93\u51FA)"}
|
|
|
44062
44295
|
parentMessageID: ctx.messageID,
|
|
44063
44296
|
parentModel,
|
|
44064
44297
|
parentAgent,
|
|
44298
|
+
category: args.category,
|
|
44065
44299
|
model: categoryModel,
|
|
44066
44300
|
skills: args.load_skills.length > 0 ? args.load_skills : undefined,
|
|
44067
44301
|
skillContent: systemContent
|
|
@@ -44162,26 +44396,85 @@ Status: ${task.status}
|
|
|
44162
44396
|
}
|
|
44163
44397
|
});
|
|
44164
44398
|
} catch (promptError) {
|
|
44165
|
-
|
|
44166
|
-
|
|
44167
|
-
|
|
44168
|
-
|
|
44169
|
-
|
|
44170
|
-
|
|
44171
|
-
|
|
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",
|
|
44172
44472
|
args,
|
|
44173
44473
|
sessionID,
|
|
44174
44474
|
agent: agentToUse,
|
|
44175
44475
|
category: args.category
|
|
44176
44476
|
});
|
|
44177
44477
|
}
|
|
44178
|
-
return formatDetailedError(promptError, {
|
|
44179
|
-
operation: "\u53D1\u9001 prompt",
|
|
44180
|
-
args,
|
|
44181
|
-
sessionID,
|
|
44182
|
-
agent: agentToUse,
|
|
44183
|
-
category: args.category
|
|
44184
|
-
});
|
|
44185
44478
|
}
|
|
44186
44479
|
const POLL_INTERVAL_MS = 500;
|
|
44187
44480
|
const MAX_POLL_TIME_MS = 10 * 60 * 1000;
|
|
@@ -44250,7 +44543,7 @@ Session ID: ${sessionID}`;
|
|
|
44250
44543
|
const diagnosis = classification.category !== "unknown" ? `
|
|
44251
44544
|
|
|
44252
44545
|
\uD83D\uDD0D **\u9519\u8BEF\u5206\u7C7B**: ${classification.reason}
|
|
44253
|
-
${classification.shouldFallback ? "\uD83D\uDCA1 \u6B64\u9519\u8BEF\
|
|
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" : ""}` : "";
|
|
44254
44547
|
return `Error fetching result: ${messagesResult.error}${diagnosis}
|
|
44255
44548
|
|
|
44256
44549
|
Session ID: ${sessionID}`;
|
|
@@ -44293,7 +44586,7 @@ ${textContent || "(\u65E0\u6587\u672C\u8F93\u51FA)"}
|
|
|
44293
44586
|
const diagnosis = classification.category !== "unknown" ? `
|
|
44294
44587
|
|
|
44295
44588
|
\uD83D\uDD0D **\u9519\u8BEF\u5206\u7C7B**: ${classification.reason}
|
|
44296
|
-
${classification.shouldFallback ? "\uD83D\uDCA1 \u6B64\u9519\u8BEF\
|
|
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" : ""}` : "";
|
|
44297
44590
|
return `\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error45 instanceof Error ? error45.message : String(error45)}${diagnosis}
|
|
44298
44591
|
|
|
44299
44592
|
Session ID: ${syncSessionID ?? "unknown"}`;
|
|
@@ -44548,6 +44841,7 @@ class BackgroundManager {
|
|
|
44548
44841
|
parentMessageID: input.parentMessageID,
|
|
44549
44842
|
parentModel: input.parentModel,
|
|
44550
44843
|
parentAgent: input.parentAgent,
|
|
44844
|
+
category: input.category,
|
|
44551
44845
|
model: input.model,
|
|
44552
44846
|
maxSteps: this.config?.maxSteps,
|
|
44553
44847
|
maxRuntimeMs: this.config?.maxRuntimeMs,
|
|
@@ -44676,72 +44970,79 @@ class BackgroundManager {
|
|
|
44676
44970
|
},
|
|
44677
44971
|
parts: [{ type: "text", text: input.prompt }]
|
|
44678
44972
|
}
|
|
44679
|
-
}).catch((error45) => {
|
|
44680
|
-
|
|
44681
|
-
|
|
44682
|
-
|
|
44683
|
-
|
|
44684
|
-
|
|
44685
|
-
|
|
44686
|
-
|
|
44687
|
-
|
|
44688
|
-
|
|
44689
|
-
|
|
44690
|
-
|
|
44691
|
-
|
|
44692
|
-
|
|
44693
|
-
|
|
44694
|
-
|
|
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;
|
|
45002
|
+
}
|
|
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,
|
|
44695
45009
|
model: fallbackResult.model,
|
|
44696
|
-
|
|
44697
|
-
}
|
|
44698
|
-
|
|
44699
|
-
|
|
44700
|
-
|
|
44701
|
-
|
|
44702
|
-
|
|
44703
|
-
|
|
44704
|
-
|
|
44705
|
-
|
|
44706
|
-
|
|
44707
|
-
|
|
44708
|
-
|
|
44709
|
-
|
|
44710
|
-
|
|
44711
|
-
|
|
44712
|
-
task2.completedAt = new Date;
|
|
44713
|
-
if (task2.concurrencyKey) {
|
|
44714
|
-
this.concurrencyManager.release(task2.concurrencyKey);
|
|
44715
|
-
task2.concurrencyKey = undefined;
|
|
44716
|
-
}
|
|
44717
|
-
this.markForNotification(task2);
|
|
44718
|
-
this.notifyParentSession(task2).catch((err) => {
|
|
44719
|
-
log("[background-agent] Failed to notify on fallback error:", err);
|
|
44720
|
-
});
|
|
44721
|
-
}
|
|
44722
|
-
});
|
|
44723
|
-
return;
|
|
44724
|
-
}
|
|
44725
|
-
existingTask.error = `All fallback models exhausted. Last error: ${classification.reason}`;
|
|
44726
|
-
} else {
|
|
44727
|
-
const errorMessage = error45 instanceof Error ? error45.message : String(error45);
|
|
44728
|
-
if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
|
|
44729
|
-
existingTask.error = `Agent "${input.agent}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.`;
|
|
44730
|
-
} else {
|
|
44731
|
-
existingTask.error = errorMessage;
|
|
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;
|
|
44732
45026
|
}
|
|
44733
45027
|
}
|
|
44734
|
-
existingTask.status = "error";
|
|
44735
|
-
existingTask.completedAt = new Date;
|
|
44736
|
-
if (existingTask.concurrencyKey) {
|
|
44737
|
-
this.concurrencyManager.release(existingTask.concurrencyKey);
|
|
44738
|
-
existingTask.concurrencyKey = undefined;
|
|
44739
|
-
}
|
|
44740
|
-
this.markForNotification(existingTask);
|
|
44741
|
-
this.notifyParentSession(existingTask).catch((err) => {
|
|
44742
|
-
log("[background-agent] Failed to notify on error:", err);
|
|
44743
|
-
});
|
|
44744
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);
|
|
44745
45046
|
});
|
|
44746
45047
|
}
|
|
44747
45048
|
getTask(id) {
|
|
@@ -63568,6 +63869,23 @@ var BuiltinCommandNameSchema = exports_external2.enum([
|
|
|
63568
63869
|
"init-deep",
|
|
63569
63870
|
"start-work"
|
|
63570
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);
|
|
63571
63889
|
var AgentOverrideConfigSchema = exports_external2.object({
|
|
63572
63890
|
model: exports_external2.string().optional(),
|
|
63573
63891
|
variant: exports_external2.string().optional(),
|
|
@@ -63582,7 +63900,8 @@ var AgentOverrideConfigSchema = exports_external2.object({
|
|
|
63582
63900
|
description: exports_external2.string().optional(),
|
|
63583
63901
|
mode: exports_external2.enum(["subagent", "primary", "all"]).optional(),
|
|
63584
63902
|
color: exports_external2.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
|
|
63585
|
-
permission: AgentPermissionSchema.optional()
|
|
63903
|
+
permission: AgentPermissionSchema.optional(),
|
|
63904
|
+
fallback_models: FallbackModelsSchema.optional()
|
|
63586
63905
|
});
|
|
63587
63906
|
var AgentOverridesSchema = exports_external2.object({
|
|
63588
63907
|
build: AgentOverrideConfigSchema.optional(),
|
|
@@ -63618,6 +63937,7 @@ var CategoryConfigSchema = exports_external2.object({
|
|
|
63618
63937
|
description: exports_external2.string().optional(),
|
|
63619
63938
|
model: exports_external2.string().optional(),
|
|
63620
63939
|
variant: exports_external2.string().optional(),
|
|
63940
|
+
fallback_models: FallbackModelsSchema.optional(),
|
|
63621
63941
|
temperature: exports_external2.number().min(0).max(2).optional(),
|
|
63622
63942
|
top_p: exports_external2.number().min(0).max(1).optional(),
|
|
63623
63943
|
maxTokens: exports_external2.number().optional(),
|
|
@@ -68829,9 +69149,31 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
68829
69149
|
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
|
68830
69150
|
const firstMessageVariantGate = createFirstMessageVariantGate();
|
|
68831
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
|
+
});
|
|
68832
69163
|
const modelCacheState = createModelCacheState();
|
|
68833
69164
|
const contextWindowMonitor = isHookEnabled("context-window-monitor") ? createContextWindowMonitorHook(ctx) : null;
|
|
68834
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;
|
|
68835
69177
|
let sessionNotification = null;
|
|
68836
69178
|
if (isHookEnabled("session-notification")) {
|
|
68837
69179
|
const forceEnable = pluginConfig.notification?.force_enable ?? false;
|
|
@@ -68884,7 +69226,10 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
68884
69226
|
const prometheusMdOnly = isHookEnabled("prometheus-md-only") ? createPrometheusMdOnlyHook(ctx) : null;
|
|
68885
69227
|
const questionLabelTruncator = createQuestionLabelTruncatorHook();
|
|
68886
69228
|
const taskResumeInfo = createTaskResumeInfoHook();
|
|
68887
|
-
const backgroundManager = new BackgroundManager(ctx,
|
|
69229
|
+
const backgroundManager = new BackgroundManager(ctx, {
|
|
69230
|
+
...pluginConfig.background_task,
|
|
69231
|
+
runtimeFallback: pluginConfig.runtime_fallback
|
|
69232
|
+
});
|
|
68888
69233
|
const atlasHook = isHookEnabled("atlas") ? createAtlasHook(ctx, { directory: ctx.directory, backgroundManager }) : null;
|
|
68889
69234
|
const perfTracer = pluginConfig.experimental?.profiling?.enabled ? new PerfTracer({
|
|
68890
69235
|
enabled: true,
|
|
@@ -68931,7 +69276,9 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
68931
69276
|
directory: ctx.directory,
|
|
68932
69277
|
userCategories: pluginConfig.categories,
|
|
68933
69278
|
gitMasterConfig: pluginConfig.git_master,
|
|
68934
|
-
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]))
|
|
68935
69282
|
});
|
|
68936
69283
|
const disabledSkills = new Set(pluginConfig.disabled_skills ?? []);
|
|
68937
69284
|
const systemMcpNames = getSystemMcpServerNames();
|
|
@@ -69100,6 +69447,8 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
69100
69447
|
hookCount++;
|
|
69101
69448
|
await wrapWithTiming(perfTracer, "event", "anthropicContextWindowLimitRecovery", () => anthropicContextWindowLimitRecovery?.event(input), evtSessionID);
|
|
69102
69449
|
hookCount++;
|
|
69450
|
+
await wrapWithTiming(perfTracer, "event", "runtimeFallback", () => runtimeFallback?.handler(input), evtSessionID);
|
|
69451
|
+
hookCount++;
|
|
69103
69452
|
await wrapWithTiming(perfTracer, "event", "agentUsageReminder", () => agentUsageReminder?.event(input), evtSessionID);
|
|
69104
69453
|
hookCount++;
|
|
69105
69454
|
await wrapWithTiming(perfTracer, "event", "interactiveBashSession", () => interactiveBashSession?.event(input), evtSessionID);
|