@lgcyaxi/oh-my-claude 1.0.0 → 1.0.1

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.
@@ -15,6 +15,13 @@ export const ProviderConfigSchema = z.object({
15
15
  note: z.string().optional(),
16
16
  });
17
17
 
18
+ // Agent fallback configuration schema
19
+ export const AgentFallbackConfigSchema = z.object({
20
+ provider: z.string(),
21
+ model: z.string(),
22
+ executionMode: z.enum(["task", "mcp"]).optional(),
23
+ });
24
+
18
25
  // Agent configuration schema
19
26
  export const AgentConfigSchema = z.object({
20
27
  provider: z.string(),
@@ -27,6 +34,7 @@ export const AgentConfigSchema = z.object({
27
34
  budget_tokens: z.number().optional(),
28
35
  })
29
36
  .optional(),
37
+ fallback: AgentFallbackConfigSchema.optional(),
30
38
  });
31
39
 
32
40
  // Category configuration schema
@@ -77,6 +85,7 @@ export const OhMyClaudeConfigSchema = z.object({
77
85
  }),
78
86
 
79
87
  agents: z.record(z.string(), AgentConfigSchema).default({
88
+ // Claude subscription agents (no fallback needed)
80
89
  Sisyphus: { provider: "claude", model: "claude-opus-4-5" },
81
90
  "claude-reviewer": {
82
91
  provider: "claude",
@@ -88,22 +97,36 @@ export const OhMyClaudeConfigSchema = z.object({
88
97
  model: "claude-haiku-4-5",
89
98
  temperature: 0.3,
90
99
  },
100
+ // External API agents (with Claude fallbacks)
91
101
  oracle: {
92
102
  provider: "deepseek",
93
103
  model: "deepseek-reasoner",
94
104
  temperature: 0.1,
105
+ fallback: { provider: "claude", model: "claude-opus-4-5", executionMode: "task" },
106
+ },
107
+ librarian: {
108
+ provider: "zhipu",
109
+ model: "glm-4.7",
110
+ temperature: 0.3,
111
+ fallback: { provider: "claude", model: "claude-sonnet-4-5", executionMode: "task" },
112
+ },
113
+ explore: {
114
+ provider: "deepseek",
115
+ model: "deepseek-chat",
116
+ temperature: 0.1,
117
+ fallback: { provider: "claude", model: "claude-haiku-4-5", executionMode: "task" },
95
118
  },
96
- librarian: { provider: "zhipu", model: "glm-4.7", temperature: 0.3 },
97
- explore: { provider: "deepseek", model: "deepseek-chat", temperature: 0.1 },
98
119
  "frontend-ui-ux": {
99
120
  provider: "zhipu",
100
121
  model: "glm-4v-flash",
101
122
  temperature: 0.7,
123
+ fallback: { provider: "claude", model: "claude-sonnet-4-5", executionMode: "task" },
102
124
  },
103
125
  "document-writer": {
104
126
  provider: "minimax",
105
127
  model: "MiniMax-M2.1",
106
128
  temperature: 0.5,
129
+ fallback: { provider: "claude", model: "claude-sonnet-4-5", executionMode: "task" },
107
130
  },
108
131
  }),
109
132
 
@@ -35,7 +35,7 @@ export function generateAgentMarkdown(agent: AgentDefinition): string {
35
35
  lines.push(`> ${agent.description}`);
36
36
  lines.push("");
37
37
 
38
- // Execution mode note
38
+ // Execution mode note with fallback info
39
39
  if (agent.executionMode === "task") {
40
40
  lines.push(
41
41
  `<!-- Execution: Claude Code Task tool (sync) - Uses Claude subscription -->`
@@ -44,12 +44,28 @@ export function generateAgentMarkdown(agent: AgentDefinition): string {
44
44
  lines.push(
45
45
  `<!-- Execution: oh-my-claude MCP server (async) - Uses ${agent.defaultProvider} API -->`
46
46
  );
47
+ // Add fallback info for MCP agents
48
+ if (agent.fallback) {
49
+ lines.push(
50
+ `<!-- Fallback: ${agent.fallback.provider}/${agent.fallback.model} via Task tool (when ${agent.defaultProvider.toUpperCase()}_API_KEY is not set) -->`
51
+ );
52
+ }
47
53
  }
48
54
  lines.push("");
49
55
 
50
56
  // The actual prompt
51
57
  lines.push(agent.prompt);
52
58
 
59
+ // Add fallback usage note for MCP agents
60
+ if (agent.executionMode === "mcp" && agent.fallback) {
61
+ lines.push("");
62
+ lines.push("## Fallback Mode");
63
+ lines.push("");
64
+ lines.push(`If \`${agent.defaultProvider.toUpperCase()}_API_KEY\` is not configured, this agent will automatically fall back to using **${agent.fallback.model}** via Claude's Task tool.`);
65
+ lines.push("");
66
+ lines.push("The fallback provides similar capabilities using your Claude subscription, though the primary provider may offer specialized features.");
67
+ }
68
+
53
69
  return lines.join("\n");
54
70
  }
55
71
 
@@ -50,6 +50,9 @@ Use this for async operations that should run in parallel without blocking.
50
50
  - visual-engineering: UI/UX work
51
51
  - writing: Documentation and text
52
52
 
53
+ **Fallback Behavior**:
54
+ If the provider's API key is not configured, the task will return with status "fallback_required" and include fallback info. When this happens, use Claude Code's Task tool with the specified fallback model instead.
55
+
53
56
  Note: Agents using Claude subscription (sisyphus, claude-reviewer, claude-scout) should use Claude Code's Task tool instead.`,
54
57
  inputSchema: {
55
58
  type: "object",
@@ -117,8 +120,8 @@ Note: Agents using Claude subscription (sisyphus, claude-reviewer, claude-scout)
117
120
  properties: {
118
121
  status: {
119
122
  type: "string",
120
- enum: ["pending", "running", "completed", "failed", "cancelled"],
121
- description: "Filter by status",
123
+ enum: ["pending", "running", "completed", "failed", "cancelled", "fallback_required"],
124
+ description: "Filter by status. 'fallback_required' means the provider API key is not configured and Claude Task tool should be used instead.",
122
125
  },
123
126
  limit: {
124
127
  type: "number",
@@ -320,6 +323,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
320
323
  completed: new Date(t.completedAt).toISOString(),
321
324
  }),
322
325
  ...(t.error && { error: t.error }),
326
+ ...(t.fallback && { fallback: t.fallback }),
323
327
  })),
324
328
  }),
325
329
  },
@@ -7,11 +7,11 @@
7
7
  * - Handle concurrency limits per provider
8
8
  */
9
9
 
10
- import { routeByAgent, routeByCategory } from "../../providers/router";
10
+ import { routeByAgent, routeByCategory, FallbackRequiredError } from "../../providers/router";
11
11
  import { getAgent } from "../../agents";
12
12
  import type { ChatMessage } from "../../providers/types";
13
13
 
14
- export type TaskStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
14
+ export type TaskStatus = "pending" | "running" | "completed" | "failed" | "cancelled" | "fallback_required";
15
15
 
16
16
  export interface Task {
17
17
  id: string;
@@ -21,6 +21,13 @@ export interface Task {
21
21
  status: TaskStatus;
22
22
  result?: string;
23
23
  error?: string;
24
+ /** Fallback info when primary provider is not configured */
25
+ fallback?: {
26
+ provider: string;
27
+ model: string;
28
+ executionMode?: string;
29
+ reason: string;
30
+ };
24
31
  createdAt: number;
25
32
  startedAt?: number;
26
33
  completedAt?: number;
@@ -123,8 +130,20 @@ async function runTask(task: Task, systemPrompt?: string): Promise<void> {
123
130
  task.completedAt = Date.now();
124
131
  tasks.set(task.id, task);
125
132
  } catch (error) {
126
- task.status = "failed";
127
- task.error = error instanceof Error ? error.message : String(error);
133
+ // Handle fallback required error specially
134
+ if (error instanceof FallbackRequiredError) {
135
+ task.status = "fallback_required";
136
+ task.error = error.message;
137
+ task.fallback = {
138
+ provider: error.fallback.provider,
139
+ model: error.fallback.model,
140
+ executionMode: error.fallback.executionMode,
141
+ reason: error.reason,
142
+ };
143
+ } else {
144
+ task.status = "failed";
145
+ task.error = error instanceof Error ? error.message : String(error);
146
+ }
128
147
  task.completedAt = Date.now();
129
148
  tasks.set(task.id, task);
130
149
  }
@@ -144,6 +163,12 @@ export function pollTask(taskId: string): {
144
163
  status: TaskStatus;
145
164
  result?: string;
146
165
  error?: string;
166
+ fallback?: {
167
+ provider: string;
168
+ model: string;
169
+ executionMode?: string;
170
+ reason: string;
171
+ };
147
172
  } {
148
173
  const task = tasks.get(taskId);
149
174
 
@@ -155,6 +180,7 @@ export function pollTask(taskId: string): {
155
180
  status: task.status,
156
181
  result: task.result,
157
182
  error: task.error,
183
+ fallback: task.fallback,
158
184
  };
159
185
  }
160
186
 
@@ -16,6 +16,8 @@ import {
16
16
  resolveProviderForAgent,
17
17
  resolveProviderForCategory,
18
18
  getProviderDetails,
19
+ shouldUseFallback,
20
+ isProviderConfigured,
19
21
  type OhMyClaudeConfig,
20
22
  } from "../config";
21
23
 
@@ -80,6 +82,21 @@ function getProviderClient(
80
82
  return client;
81
83
  }
82
84
 
85
+ /**
86
+ * Custom error class for fallback scenarios
87
+ */
88
+ export class FallbackRequiredError extends Error {
89
+ constructor(
90
+ message: string,
91
+ public readonly agentName: string,
92
+ public readonly fallback: { provider: string; model: string; executionMode?: string },
93
+ public readonly reason: string
94
+ ) {
95
+ super(message);
96
+ this.name = "FallbackRequiredError";
97
+ }
98
+ }
99
+
83
100
  /**
84
101
  * Route a request to the appropriate provider based on agent name
85
102
  */
@@ -106,6 +123,17 @@ export async function routeByAgent(
106
123
  );
107
124
  }
108
125
 
126
+ // Check if fallback should be used (primary provider not configured)
127
+ const fallbackCheck = shouldUseFallback(config, agentName);
128
+ if (fallbackCheck.useFallback && fallbackCheck.fallback) {
129
+ throw new FallbackRequiredError(
130
+ `Agent "${agentName}" requires fallback: ${fallbackCheck.reason}. Use Task tool with ${fallbackCheck.fallback.model} instead.`,
131
+ agentName,
132
+ fallbackCheck.fallback,
133
+ fallbackCheck.reason ?? "Provider not configured"
134
+ );
135
+ }
136
+
109
137
  const client = getProviderClient(agentConfig.provider, config);
110
138
 
111
139
  const request: ChatCompletionRequest = {