@skj1724/oh-my-opencode 3.19.6 → 3.19.7

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 CHANGED
@@ -6117,6 +6117,12 @@ var init_model_resolver = __esm(() => {
6117
6117
  init_model_availability();
6118
6118
  });
6119
6119
 
6120
+ // src/shared/runtime-fallback.ts
6121
+ var init_runtime_fallback = __esm(() => {
6122
+ init_model_availability();
6123
+ init_model_requirements();
6124
+ });
6125
+
6120
6126
  // src/shared/perf-timer.ts
6121
6127
  class PerfTimer {
6122
6128
  marks = new Map;
@@ -6226,6 +6232,7 @@ var init_shared = __esm(() => {
6226
6232
  init_model_requirements();
6227
6233
  init_model_resolver();
6228
6234
  init_model_availability();
6235
+ init_runtime_fallback();
6229
6236
  init_perf_tracer();
6230
6237
  init_fileio_monitor();
6231
6238
  init_windows_reserved_names();
@@ -8460,7 +8467,7 @@ var import_picocolors2 = __toESM(require_picocolors(), 1);
8460
8467
  // package.json
8461
8468
  var package_default = {
8462
8469
  name: "@skj1724/oh-my-opencode",
8463
- version: "3.19.6",
8470
+ version: "3.19.7",
8464
8471
  description: "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
8465
8472
  main: "dist/index.js",
8466
8473
  types: "dist/index.d.ts",
@@ -24997,6 +25004,7 @@ var HookNameSchema = exports_external.enum([
24997
25004
  "auto-slash-command",
24998
25005
  "edit-error-recovery",
24999
25006
  "delegate-task-retry",
25007
+ "runtime-fallback",
25000
25008
  "prometheus-md-only",
25001
25009
  "perf-profiler",
25002
25010
  "start-work",
@@ -25183,6 +25191,15 @@ var GitMasterConfigSchema = exports_external.object({
25183
25191
  commit_footer: exports_external.boolean().default(true),
25184
25192
  include_co_authored_by: exports_external.boolean().default(true)
25185
25193
  });
25194
+ var RuntimeFallbackConfigSchema = exports_external.object({
25195
+ enabled: exports_external.boolean().default(true),
25196
+ max_attempts: exports_external.number().min(0).default(3),
25197
+ initial_delay_ms: exports_external.number().min(0).default(2000),
25198
+ backoff_factor: exports_external.number().min(1).default(2),
25199
+ max_delay_ms: exports_external.number().min(0).default(30000),
25200
+ respect_retry_after: exports_external.boolean().default(true),
25201
+ jitter: exports_external.boolean().default(true)
25202
+ });
25186
25203
  var OhMyOpenCodeConfigSchema = exports_external.object({
25187
25204
  $schema: exports_external.string().optional(),
25188
25205
  disabled_mcps: exports_external.array(AnyMcpNameSchema).optional(),
@@ -25201,7 +25218,8 @@ var OhMyOpenCodeConfigSchema = exports_external.object({
25201
25218
  ralph_loop: RalphLoopConfigSchema.optional(),
25202
25219
  background_task: BackgroundTaskConfigSchema.optional(),
25203
25220
  notification: NotificationConfigSchema.optional(),
25204
- git_master: GitMasterConfigSchema.optional()
25221
+ git_master: GitMasterConfigSchema.optional(),
25222
+ runtime_fallback: RuntimeFallbackConfigSchema.optional()
25205
25223
  });
25206
25224
  // src/cli/doctor/checks/config.ts
25207
25225
  var USER_CONFIG_DIR2 = getOpenCodeConfigDir({ binary: "opencode" });
@@ -70,6 +70,7 @@ export declare const HookNameSchema: z.ZodEnum<{
70
70
  "auto-slash-command": "auto-slash-command";
71
71
  "edit-error-recovery": "edit-error-recovery";
72
72
  "delegate-task-retry": "delegate-task-retry";
73
+ "runtime-fallback": "runtime-fallback";
73
74
  "prometheus-md-only": "prometheus-md-only";
74
75
  "perf-profiler": "perf-profiler";
75
76
  "start-work": "start-work";
@@ -1016,6 +1017,15 @@ export declare const GitMasterConfigSchema: z.ZodObject<{
1016
1017
  commit_footer: z.ZodDefault<z.ZodBoolean>;
1017
1018
  include_co_authored_by: z.ZodDefault<z.ZodBoolean>;
1018
1019
  }, z.core.$strip>;
1020
+ export declare const RuntimeFallbackConfigSchema: z.ZodObject<{
1021
+ enabled: z.ZodDefault<z.ZodBoolean>;
1022
+ max_attempts: z.ZodDefault<z.ZodNumber>;
1023
+ initial_delay_ms: z.ZodDefault<z.ZodNumber>;
1024
+ backoff_factor: z.ZodDefault<z.ZodNumber>;
1025
+ max_delay_ms: z.ZodDefault<z.ZodNumber>;
1026
+ respect_retry_after: z.ZodDefault<z.ZodBoolean>;
1027
+ jitter: z.ZodDefault<z.ZodBoolean>;
1028
+ }, z.core.$strip>;
1019
1029
  export declare const OhMyOpenCodeConfigSchema: z.ZodObject<{
1020
1030
  $schema: z.ZodOptional<z.ZodString>;
1021
1031
  disabled_mcps: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -1064,6 +1074,7 @@ export declare const OhMyOpenCodeConfigSchema: z.ZodObject<{
1064
1074
  "auto-slash-command": "auto-slash-command";
1065
1075
  "edit-error-recovery": "edit-error-recovery";
1066
1076
  "delegate-task-retry": "delegate-task-retry";
1077
+ "runtime-fallback": "runtime-fallback";
1067
1078
  "prometheus-md-only": "prometheus-md-only";
1068
1079
  "perf-profiler": "perf-profiler";
1069
1080
  "start-work": "start-work";
@@ -1855,6 +1866,15 @@ export declare const OhMyOpenCodeConfigSchema: z.ZodObject<{
1855
1866
  commit_footer: z.ZodDefault<z.ZodBoolean>;
1856
1867
  include_co_authored_by: z.ZodDefault<z.ZodBoolean>;
1857
1868
  }, z.core.$strip>>;
1869
+ runtime_fallback: z.ZodOptional<z.ZodObject<{
1870
+ enabled: z.ZodDefault<z.ZodBoolean>;
1871
+ max_attempts: z.ZodDefault<z.ZodNumber>;
1872
+ initial_delay_ms: z.ZodDefault<z.ZodNumber>;
1873
+ backoff_factor: z.ZodDefault<z.ZodNumber>;
1874
+ max_delay_ms: z.ZodDefault<z.ZodNumber>;
1875
+ respect_retry_after: z.ZodDefault<z.ZodBoolean>;
1876
+ jitter: z.ZodDefault<z.ZodBoolean>;
1877
+ }, z.core.$strip>>;
1858
1878
  }, z.core.$strip>;
1859
1879
  export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>;
1860
1880
  export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>;
@@ -1877,4 +1897,5 @@ export type CategoryConfig = z.infer<typeof CategoryConfigSchema>;
1877
1897
  export type CategoriesConfig = z.infer<typeof CategoriesConfigSchema>;
1878
1898
  export type BuiltinCategoryName = z.infer<typeof BuiltinCategoryNameSchema>;
1879
1899
  export type GitMasterConfig = z.infer<typeof GitMasterConfigSchema>;
1900
+ export type RuntimeFallbackConfig = z.infer<typeof RuntimeFallbackConfigSchema>;
1880
1901
  export { AnyMcpNameSchema, type AnyMcpName, McpNameSchema, type McpName } from "../mcp/types";
@@ -1,3 +1,5 @@
1
+ import type { FallbackAttempt } from "../../shared/runtime-fallback";
2
+ export type { FallbackAttempt };
1
3
  export type BackgroundTaskStatus = "pending" | "running" | "completed" | "error" | "cancelled";
2
4
  export interface PhaseTiming {
3
5
  /** queuedAt -> startedAt in ms */
@@ -65,6 +67,8 @@ export interface BackgroundTask {
65
67
  maxRuntimeMs?: number;
66
68
  /** Max duration of a single step in ms (0 / undefined = unlimited) */
67
69
  stepTimeoutMs?: number;
70
+ /** Fallback attempts history */
71
+ attempts?: FallbackAttempt[];
68
72
  }
69
73
  export interface LaunchInput {
70
74
  description: string;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Runtime Fallback Hook
3
+ *
4
+ * 处理 session.error 事件中的 provider 错误(quota、rate_limit),
5
+ * 在 retry 耗尽后自动切换到 fallback 模型。
6
+ *
7
+ * 不处理:context_overflow(由 context-window-recovery 处理)、auth、bad_request。
8
+ * 避让 sessionRecovery(可恢复错误优先由 sessionRecovery 处理)。
9
+ */
10
+ import type { PluginInput } from "@opencode-ai/plugin";
11
+ export interface RuntimeFallbackOptions {
12
+ sessionRecovery?: {
13
+ isRecoverableError: (error: unknown) => boolean;
14
+ };
15
+ }
16
+ export declare function createRuntimeFallbackHook(ctx: PluginInput, options?: RuntimeFallbackOptions): {
17
+ handler: ({ event, }: {
18
+ event: {
19
+ type: string;
20
+ properties?: unknown;
21
+ };
22
+ }) => Promise<boolean>;
23
+ };
@@ -0,0 +1 @@
1
+ export {};
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,81 @@ 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
+ throw new Error(`No fallback chain found for agent="${agent ?? ""}" category="${category ?? ""}"`);
5144
+ }
5145
+ function resolveNextFallbackModel(input) {
5146
+ const {
5147
+ agent,
5148
+ category,
5149
+ currentModel,
5150
+ attempts,
5151
+ availableModels,
5152
+ lastErrorClassification
5153
+ } = input;
5154
+ const chain = getChain(agent, category);
5155
+ const candidates = expandChain(chain);
5156
+ const skipKeys = new Set;
5157
+ skipKeys.add(modelKey(currentModel));
5158
+ for (const a of attempts) {
5159
+ skipKeys.add(modelKey(a.model));
5160
+ }
5161
+ const resultAttempts = [...attempts];
5162
+ const currentKey = modelKey(currentModel);
5163
+ const isInAttempts = attempts.some((a) => modelKey(a.model) === currentKey);
5164
+ if (!isInAttempts) {
5165
+ resultAttempts.push({ model: currentModel });
5166
+ }
5167
+ const hasAvailabilityFilter = availableModels != null && availableModels.size > 0;
5168
+ for (const candidate of candidates) {
5169
+ const key = modelKey(candidate);
5170
+ if (skipKeys.has(key))
5171
+ continue;
5172
+ if (hasAvailabilityFilter) {
5173
+ const match = fuzzyMatchModel(key, availableModels, [candidate.providerID]);
5174
+ if (!match)
5175
+ continue;
5176
+ }
5177
+ return {
5178
+ kind: "next",
5179
+ model: candidate,
5180
+ attempts: resultAttempts
5181
+ };
5182
+ }
5183
+ return {
5184
+ kind: "exhausted",
5185
+ attempts: resultAttempts,
5186
+ lastErrorClassification
5187
+ };
5188
+ }
5189
+ var init_runtime_fallback = __esm(() => {
5190
+ init_model_availability();
5191
+ init_model_requirements();
5192
+ });
5193
+
5067
5194
  // src/shared/perf-timer.ts
5068
5195
  class PerfTimer {
5069
5196
  marks = new Map;
@@ -5348,6 +5475,7 @@ var init_shared = __esm(() => {
5348
5475
  init_model_requirements();
5349
5476
  init_model_resolver();
5350
5477
  init_model_availability();
5478
+ init_runtime_fallback();
5351
5479
  init_perf_tracer();
5352
5480
  init_fileio_monitor();
5353
5481
  init_windows_reserved_names();
@@ -43257,6 +43385,256 @@ function initTaskToastManager(client2, concurrencyManager) {
43257
43385
  }
43258
43386
  // src/tools/delegate-task/tools.ts
43259
43387
  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
43260
43638
  var SISYPHUS_JUNIOR_AGENT = "sisyphus-junior";
43261
43639
  function parseModelString(model) {
43262
43640
  const parts = model.split("/");
@@ -43868,7 +44246,12 @@ Session ID: ${sessionID}`;
43868
44246
  path: { id: sessionID }
43869
44247
  });
43870
44248
  if (messagesResult.error) {
43871
- return `Error fetching result: ${messagesResult.error}
44249
+ const classification = classifyProviderError(messagesResult.error);
44250
+ const diagnosis = classification.category !== "unknown" ? `
44251
+
44252
+ \uD83D\uDD0D **\u9519\u8BEF\u5206\u7C7B**: ${classification.reason}
44253
+ ${classification.shouldFallback ? "\uD83D\uDCA1 \u6B64\u9519\u8BEF\u53EF\u901A\u8FC7 runtime fallback \u81EA\u52A8\u5904\u7406\u3002" : classification.retryable ? "\u23F3 \u6B64\u9519\u8BEF\u53EF\u91CD\u8BD5\u3002" : ""}` : "";
44254
+ return `Error fetching result: ${messagesResult.error}${diagnosis}
43872
44255
 
43873
44256
  Session ID: ${sessionID}`;
43874
44257
  }
@@ -43906,13 +44289,14 @@ ${textContent || "(\u65E0\u6587\u672C\u8F93\u51FA)"}
43906
44289
  if (syncSessionID) {
43907
44290
  subagentSessions.delete(syncSessionID);
43908
44291
  }
43909
- return formatDetailedError(error45, {
43910
- operation: "\u6267\u884C\u4EFB\u52A1",
43911
- args,
43912
- sessionID: syncSessionID,
43913
- agent: agentToUse,
43914
- category: args.category
43915
- });
44292
+ const classification = classifyProviderError(error45);
44293
+ const diagnosis = classification.category !== "unknown" ? `
44294
+
44295
+ \uD83D\uDD0D **\u9519\u8BEF\u5206\u7C7B**: ${classification.reason}
44296
+ ${classification.shouldFallback ? "\uD83D\uDCA1 \u6B64\u9519\u8BEF\u53EF\u901A\u8FC7 runtime fallback \u81EA\u52A8\u5904\u7406\u3002" : classification.retryable ? "\u23F3 \u6B64\u9519\u8BEF\u53EF\u91CD\u8BD5\u3002" : ""}` : "";
44297
+ return `\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error45 instanceof Error ? error45.message : String(error45)}${diagnosis}
44298
+
44299
+ Session ID: ${syncSessionID ?? "unknown"}`;
43916
44300
  }
43917
44301
  }
43918
44302
  });
@@ -44041,6 +44425,9 @@ class ConcurrencyManager {
44041
44425
  }
44042
44426
  }
44043
44427
 
44428
+ // src/features/background-agent/manager.ts
44429
+ init_runtime_fallback();
44430
+
44044
44431
  // src/features/background-agent/perf-aggregator.ts
44045
44432
  function percentile(sorted, p) {
44046
44433
  if (sorted.length === 0)
@@ -44293,13 +44680,58 @@ class BackgroundManager {
44293
44680
  log("[background-agent] promptAsync error:", error45);
44294
44681
  const existingTask = this.findBySession(sessionID);
44295
44682
  if (existingTask) {
44296
- existingTask.status = "error";
44297
- const errorMessage = error45 instanceof Error ? error45.message : String(error45);
44298
- if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
44299
- existingTask.error = `Agent "${input.agent}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.`;
44683
+ const classification = classifyProviderError(error45);
44684
+ if (classification.retryable || classification.shouldFallback) {
44685
+ const attempts = existingTask.attempts ?? [];
44686
+ const currentModel = input.model ?? { providerID: "", modelID: "" };
44687
+ const fallbackResult = resolveNextFallbackModel({
44688
+ agent: input.agent,
44689
+ currentModel,
44690
+ attempts,
44691
+ lastErrorClassification: classification
44692
+ });
44693
+ if (fallbackResult.kind === "next") {
44694
+ existingTask.attempts = [...attempts, {
44695
+ model: fallbackResult.model,
44696
+ error: classification
44697
+ }];
44698
+ log("[background-agent] Fallback to model:", fallbackResult.model);
44699
+ this.client.session.prompt({
44700
+ path: { id: sessionID },
44701
+ body: {
44702
+ agent: input.agent,
44703
+ model: fallbackResult.model,
44704
+ parts: [{ type: "text", text: input.prompt }]
44705
+ }
44706
+ }).catch((retryError) => {
44707
+ log("[background-agent] Fallback prompt error:", retryError);
44708
+ const task2 = this.findBySession(sessionID);
44709
+ if (task2) {
44710
+ task2.status = "error";
44711
+ task2.error = `Fallback failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`;
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}`;
44300
44726
  } else {
44301
- existingTask.error = errorMessage;
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;
44732
+ }
44302
44733
  }
44734
+ existingTask.status = "error";
44303
44735
  existingTask.completedAt = new Date;
44304
44736
  if (existingTask.concurrencyKey) {
44305
44737
  this.concurrencyManager.release(existingTask.concurrencyKey);
@@ -63126,6 +63558,7 @@ var HookNameSchema = exports_external2.enum([
63126
63558
  "auto-slash-command",
63127
63559
  "edit-error-recovery",
63128
63560
  "delegate-task-retry",
63561
+ "runtime-fallback",
63129
63562
  "prometheus-md-only",
63130
63563
  "perf-profiler",
63131
63564
  "start-work",
@@ -63312,6 +63745,15 @@ var GitMasterConfigSchema = exports_external2.object({
63312
63745
  commit_footer: exports_external2.boolean().default(true),
63313
63746
  include_co_authored_by: exports_external2.boolean().default(true)
63314
63747
  });
63748
+ var RuntimeFallbackConfigSchema = exports_external2.object({
63749
+ enabled: exports_external2.boolean().default(true),
63750
+ max_attempts: exports_external2.number().min(0).default(3),
63751
+ initial_delay_ms: exports_external2.number().min(0).default(2000),
63752
+ backoff_factor: exports_external2.number().min(1).default(2),
63753
+ max_delay_ms: exports_external2.number().min(0).default(30000),
63754
+ respect_retry_after: exports_external2.boolean().default(true),
63755
+ jitter: exports_external2.boolean().default(true)
63756
+ });
63315
63757
  var OhMyOpenCodeConfigSchema = exports_external2.object({
63316
63758
  $schema: exports_external2.string().optional(),
63317
63759
  disabled_mcps: exports_external2.array(AnyMcpNameSchema).optional(),
@@ -63330,7 +63772,8 @@ var OhMyOpenCodeConfigSchema = exports_external2.object({
63330
63772
  ralph_loop: RalphLoopConfigSchema.optional(),
63331
63773
  background_task: BackgroundTaskConfigSchema.optional(),
63332
63774
  notification: NotificationConfigSchema.optional(),
63333
- git_master: GitMasterConfigSchema.optional()
63775
+ git_master: GitMasterConfigSchema.optional(),
63776
+ runtime_fallback: RuntimeFallbackConfigSchema.optional()
63334
63777
  });
63335
63778
  // src/plugin-config.ts
63336
63779
  init_shared();
@@ -28,6 +28,7 @@ export * from "./agent-tool-restrictions";
28
28
  export * from "./model-requirements";
29
29
  export * from "./model-resolver";
30
30
  export * from "./model-availability";
31
+ export * from "./runtime-fallback";
31
32
  export * from "./perf-timer";
32
33
  export * from "./perf-tracer";
33
34
  export * from "./case-insensitive";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Provider Error Classifier
3
+ *
4
+ * 统一的 provider 错误分类逻辑,用于判断错误类型和是否可重试/fallback。
5
+ * 支持 OpenAI、Anthropic、Gemini、xAI、Zhipu 等主流 provider。
6
+ */
7
+ export type ErrorCategory = "rate_limit" | "quota" | "overloaded" | "context_overflow" | "auth" | "bad_request" | "unknown";
8
+ export interface ProviderErrorClassification {
9
+ category: ErrorCategory;
10
+ retryable: boolean;
11
+ shouldFallback: boolean;
12
+ statusCode?: number;
13
+ providerGuess?: string;
14
+ retryAfterMs?: number;
15
+ reason: string;
16
+ }
17
+ /**
18
+ * 分类 provider 错误
19
+ *
20
+ * @param error - 未知类型的错误对象
21
+ * @returns 包含错误分类、可重试性、是否应该 fallback 等信息
22
+ */
23
+ export declare function classifyProviderError(error: unknown): ProviderErrorClassification;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Retry/backoff 策略服务
3
+ *
4
+ * 提供纯函数式的重试决策计算,支持指数退避、Retry-After 和 Jitter。
5
+ */
6
+ export interface RetryConfig {
7
+ /** 最大重试次数 */
8
+ max_attempts: number;
9
+ /** 初始延迟(毫秒) */
10
+ initial_delay_ms: number;
11
+ /** 退避因子 */
12
+ backoff_factor: number;
13
+ /** 最大延迟(毫秒) */
14
+ max_delay_ms: number;
15
+ /** 是否启用 jitter */
16
+ jitter: boolean;
17
+ /** 是否尊重 Retry-After 头 */
18
+ respect_retry_after: boolean;
19
+ }
20
+ export interface RetryDecision {
21
+ /** 是否可重试 */
22
+ retryable: boolean;
23
+ /** 延迟时间(毫秒) */
24
+ delay_ms: number;
25
+ /** 当前尝试次数 */
26
+ attempt: number;
27
+ /** 决策原因 */
28
+ reason: string;
29
+ }
30
+ export declare const DEFAULT_RETRY_CONFIG: RetryConfig;
31
+ /**
32
+ * 计算重试延迟
33
+ *
34
+ * @param attempt - 当前尝试次数(从 0 开始)
35
+ * @param config - 重试配置
36
+ * @param retryAfterMs - 可选的 Retry-After 值(毫秒)
37
+ * @returns 重试决策
38
+ */
39
+ export declare function calculateRetryDelay(attempt: number, config: RetryConfig, retryAfterMs?: number): RetryDecision;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Runtime Fallback Decision Service
3
+ *
4
+ * 纯函数:根据 agent/category 的 fallback chain,结合当前失败状态和可用模型,
5
+ * 决定下一个要尝试的模型。
6
+ */
7
+ import type { ProviderErrorClassification } from "./provider-error-classifier";
8
+ export interface FallbackModel {
9
+ providerID: string;
10
+ modelID: string;
11
+ variant?: string;
12
+ }
13
+ export interface FallbackAttempt {
14
+ model: FallbackModel;
15
+ error?: ProviderErrorClassification;
16
+ }
17
+ export interface FallbackNextResult {
18
+ kind: "next";
19
+ model: FallbackModel;
20
+ attempts: FallbackAttempt[];
21
+ }
22
+ export interface FallbackExhaustedResult {
23
+ kind: "exhausted";
24
+ attempts: FallbackAttempt[];
25
+ lastErrorClassification?: ProviderErrorClassification;
26
+ }
27
+ export type FallbackResult = FallbackNextResult | FallbackExhaustedResult;
28
+ export interface RuntimeFallbackInput {
29
+ agent?: string;
30
+ category?: string;
31
+ currentModel: FallbackModel;
32
+ attempts: FallbackAttempt[];
33
+ availableModels?: Set<string>;
34
+ lastErrorClassification?: ProviderErrorClassification;
35
+ }
36
+ /**
37
+ * 解析下一个 fallback 模型
38
+ *
39
+ * 逻辑:
40
+ * 1. 从 AGENT_MODEL_REQUIREMENTS 或 CATEGORY_MODEL_REQUIREMENTS 获取 fallbackChain
41
+ * 2. 将 chain 展开为候选列表(每个 provider × model 组合,保持顺序)
42
+ * 3. 跳过 currentModel 和 attempts 中的 model
43
+ * 4. 如果 availableModels 非空,使用 fuzzyMatchModel 检查可用性
44
+ * 5. 返回第一个有效候选,或 exhausted
45
+ */
46
+ export declare function resolveNextFallbackModel(input: RuntimeFallbackInput): FallbackResult;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skj1724/oh-my-opencode",
3
- "version": "3.19.6",
3
+ "version": "3.19.7",
4
4
  "description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",