fix(orchestrator): make provider-aware Claude key startup requirements
This commit is contained in:
@@ -12,6 +12,9 @@ describe("AgentSpawnerService", () => {
|
||||
// Create mock ConfigService
|
||||
mockConfigService = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "orchestrator.aiProvider") {
|
||||
return "ollama";
|
||||
}
|
||||
if (key === "orchestrator.claude.apiKey") {
|
||||
return "test-api-key";
|
||||
}
|
||||
@@ -31,19 +34,80 @@ describe("AgentSpawnerService", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it("should initialize with Claude API key from config", () => {
|
||||
it("should initialize with default AI provider when API key is omitted", () => {
|
||||
const noClaudeConfigService = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "orchestrator.aiProvider") {
|
||||
return "ollama";
|
||||
}
|
||||
if (key === "orchestrator.spawner.maxConcurrentAgents") {
|
||||
return 20;
|
||||
}
|
||||
if (key === "orchestrator.spawner.sessionCleanupDelayMs") {
|
||||
return 30000;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
} as unknown as ConfigService;
|
||||
|
||||
const serviceNoKey = new AgentSpawnerService(noClaudeConfigService);
|
||||
expect(serviceNoKey).toBeDefined();
|
||||
});
|
||||
|
||||
it("should initialize with Claude provider when key is present", () => {
|
||||
expect(mockConfigService.get).toHaveBeenCalledWith("orchestrator.claude.apiKey");
|
||||
});
|
||||
|
||||
it("should throw error if Claude API key is missing", () => {
|
||||
it("should initialize with CLAUDE provider when API key is present", () => {
|
||||
const claudeConfigService = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "orchestrator.aiProvider") {
|
||||
return "claude";
|
||||
}
|
||||
if (key === "orchestrator.claude.apiKey") {
|
||||
return "test-api-key";
|
||||
}
|
||||
if (key === "orchestrator.spawner.maxConcurrentAgents") {
|
||||
return 20;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
} as unknown as ConfigService;
|
||||
|
||||
const claudeService = new AgentSpawnerService(claudeConfigService);
|
||||
expect(claudeService).toBeDefined();
|
||||
});
|
||||
|
||||
it("should throw error if Claude API key is missing when provider is claude", () => {
|
||||
const badConfigService = {
|
||||
get: vi.fn(() => undefined),
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "orchestrator.aiProvider") {
|
||||
return "claude";
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
} as unknown as ConfigService;
|
||||
|
||||
expect(() => new AgentSpawnerService(badConfigService)).toThrow(
|
||||
"CLAUDE_API_KEY is not configured"
|
||||
"CLAUDE_API_KEY is required when AI_PROVIDER is set to 'claude'"
|
||||
);
|
||||
});
|
||||
|
||||
it("should still initialize when CLAUDE_API_KEY is missing for non-Claude provider", () => {
|
||||
const nonClaudeConfigService = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "orchestrator.aiProvider") {
|
||||
return "ollama";
|
||||
}
|
||||
if (key === "orchestrator.spawner.maxConcurrentAgents") {
|
||||
return 20;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
} as unknown as ConfigService;
|
||||
|
||||
expect(() => new AgentSpawnerService(nonClaudeConfigService)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("spawnAgent", () => {
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
* This allows time for status queries before the session is removed
|
||||
*/
|
||||
const DEFAULT_SESSION_CLEANUP_DELAY_MS = 30000; // 30 seconds
|
||||
const SUPPORTED_AI_PROVIDERS = ["ollama", "claude", "openai"] as const;
|
||||
type SupportedAiProvider = (typeof SUPPORTED_AI_PROVIDERS)[number];
|
||||
|
||||
/**
|
||||
* Service responsible for spawning Claude agents using Anthropic SDK
|
||||
@@ -21,22 +23,38 @@ const DEFAULT_SESSION_CLEANUP_DELAY_MS = 30000; // 30 seconds
|
||||
@Injectable()
|
||||
export class AgentSpawnerService implements OnModuleDestroy {
|
||||
private readonly logger = new Logger(AgentSpawnerService.name);
|
||||
private readonly anthropic: Anthropic;
|
||||
private readonly anthropic: Anthropic | undefined;
|
||||
private readonly aiProvider: SupportedAiProvider;
|
||||
private readonly sessions = new Map<string, AgentSession>();
|
||||
private readonly maxConcurrentAgents: number;
|
||||
private readonly sessionCleanupDelayMs: number;
|
||||
private readonly cleanupTimers = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
const configuredProvider = this.configService.get<string>("orchestrator.aiProvider");
|
||||
this.aiProvider = this.normalizeAiProvider(configuredProvider);
|
||||
|
||||
this.logger.log(`AgentSpawnerService resolved AI provider: ${this.aiProvider}`);
|
||||
|
||||
const apiKey = this.configService.get<string>("orchestrator.claude.apiKey");
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error("CLAUDE_API_KEY is not configured");
|
||||
}
|
||||
if (this.aiProvider === "claude") {
|
||||
if (!apiKey) {
|
||||
throw new Error("CLAUDE_API_KEY is required when AI_PROVIDER is set to 'claude'");
|
||||
}
|
||||
|
||||
this.anthropic = new Anthropic({
|
||||
apiKey,
|
||||
});
|
||||
this.logger.log("CLAUDE_API_KEY is configured. Initializing Anthropic client.");
|
||||
this.anthropic = new Anthropic({ apiKey });
|
||||
} else {
|
||||
if (apiKey) {
|
||||
this.logger.debug(
|
||||
`CLAUDE_API_KEY is set but ignored because AI provider is '${this.aiProvider}'`
|
||||
);
|
||||
} else {
|
||||
this.logger.log(`CLAUDE_API_KEY not required for AI provider '${this.aiProvider}'.`);
|
||||
}
|
||||
this.anthropic = undefined;
|
||||
}
|
||||
|
||||
// Default to 20 if not configured
|
||||
this.maxConcurrentAgents =
|
||||
@@ -48,10 +66,27 @@ export class AgentSpawnerService implements OnModuleDestroy {
|
||||
DEFAULT_SESSION_CLEANUP_DELAY_MS;
|
||||
|
||||
this.logger.log(
|
||||
`AgentSpawnerService initialized with Claude SDK (max concurrent agents: ${String(this.maxConcurrentAgents)}, cleanup delay: ${String(this.sessionCleanupDelayMs)}ms)`
|
||||
`AgentSpawnerService initialized with ${this.aiProvider} AI provider (max concurrent agents: ${String(
|
||||
this.maxConcurrentAgents
|
||||
)}, cleanup delay: ${String(this.sessionCleanupDelayMs)}ms)`
|
||||
);
|
||||
}
|
||||
|
||||
private normalizeAiProvider(provider?: string): SupportedAiProvider {
|
||||
const normalizedProvider = provider?.trim().toLowerCase();
|
||||
|
||||
if (!normalizedProvider) {
|
||||
return "ollama";
|
||||
}
|
||||
|
||||
if (!SUPPORTED_AI_PROVIDERS.includes(normalizedProvider as SupportedAiProvider)) {
|
||||
this.logger.warn(`Unsupported AI provider '${normalizedProvider}'. Defaulting to 'ollama'.`);
|
||||
return "ollama";
|
||||
}
|
||||
|
||||
return normalizedProvider as SupportedAiProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all pending cleanup timers on module destroy
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user