mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 20:03:37 +00:00
- Combined CLI disconnection markers with OpenCode support - Added OpenCode auth/deauth routes and API methods - Resolved merge conflicts between feature branch and upstream v0.9.0rc
304 lines
9.3 KiB
TypeScript
304 lines
9.3 KiB
TypeScript
/**
|
|
* Provider Factory - Routes model IDs to the appropriate provider
|
|
*
|
|
* Uses a registry pattern for dynamic provider registration.
|
|
* Providers register themselves on import, making it easy to add new providers.
|
|
*/
|
|
|
|
import { BaseProvider } from './base-provider.js';
|
|
import type { InstallationStatus, ModelDefinition } from './types.js';
|
|
import { isCursorModel, isCodexModel, isOpencodeModel, type ModelProvider } from '@automaker/types';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
const DISCONNECTED_MARKERS: Record<string, string> = {
|
|
claude: '.claude-disconnected',
|
|
codex: '.codex-disconnected',
|
|
cursor: '.cursor-disconnected',
|
|
opencode: '.opencode-disconnected',
|
|
};
|
|
|
|
/**
|
|
* Check if a provider CLI is disconnected from the app
|
|
*/
|
|
export function isProviderDisconnected(providerName: string): boolean {
|
|
const markerFile = DISCONNECTED_MARKERS[providerName.toLowerCase()];
|
|
if (!markerFile) return false;
|
|
|
|
const markerPath = path.join(process.cwd(), '.automaker', markerFile);
|
|
return fs.existsSync(markerPath);
|
|
}
|
|
|
|
/**
|
|
* Provider registration entry
|
|
*/
|
|
interface ProviderRegistration {
|
|
/** Factory function to create provider instance */
|
|
factory: () => BaseProvider;
|
|
/** Aliases for this provider (e.g., 'anthropic' for 'claude') */
|
|
aliases?: string[];
|
|
/** Function to check if this provider can handle a model ID */
|
|
canHandleModel?: (modelId: string) => boolean;
|
|
/** Priority for model matching (higher = checked first) */
|
|
priority?: number;
|
|
}
|
|
|
|
/**
|
|
* Provider registry - stores registered providers
|
|
*/
|
|
const providerRegistry = new Map<string, ProviderRegistration>();
|
|
|
|
/**
|
|
* Register a provider with the factory
|
|
*
|
|
* @param name Provider name (e.g., 'claude', 'cursor')
|
|
* @param registration Provider registration config
|
|
*/
|
|
export function registerProvider(name: string, registration: ProviderRegistration): void {
|
|
providerRegistry.set(name.toLowerCase(), registration);
|
|
}
|
|
|
|
export class ProviderFactory {
|
|
/**
|
|
* Determine which provider to use for a given model
|
|
*
|
|
* @param model Model identifier
|
|
* @returns Provider name (ModelProvider type)
|
|
*/
|
|
static getProviderNameForModel(model: string): ModelProvider {
|
|
const lowerModel = model.toLowerCase();
|
|
|
|
// Get all registered providers sorted by priority (descending)
|
|
const registrations = Array.from(providerRegistry.entries()).sort(
|
|
([, a], [, b]) => (b.priority ?? 0) - (a.priority ?? 0)
|
|
);
|
|
|
|
// Check each provider's canHandleModel function
|
|
for (const [name, reg] of registrations) {
|
|
if (reg.canHandleModel?.(lowerModel)) {
|
|
return name as ModelProvider;
|
|
}
|
|
}
|
|
|
|
// Fallback: Check for explicit prefixes
|
|
for (const [name] of registrations) {
|
|
if (lowerModel.startsWith(`${name}-`)) {
|
|
return name as ModelProvider;
|
|
}
|
|
}
|
|
|
|
// Default to claude (first registered provider or claude)
|
|
return 'claude';
|
|
}
|
|
|
|
/**
|
|
* Get the appropriate provider for a given model ID
|
|
*
|
|
* @param modelId Model identifier (e.g., "claude-opus-4-5-20251101", "cursor-gpt-4o", "cursor-auto")
|
|
* @param options Optional settings
|
|
* @param options.throwOnDisconnected Throw error if provider is disconnected (default: true)
|
|
* @returns Provider instance for the model
|
|
* @throws Error if provider is disconnected and throwOnDisconnected is true
|
|
*/
|
|
static getProviderForModel(
|
|
modelId: string,
|
|
options: { throwOnDisconnected?: boolean } = {}
|
|
): BaseProvider {
|
|
const { throwOnDisconnected = true } = options;
|
|
const providerName = this.getProviderForModelName(modelId);
|
|
|
|
// Check if provider is disconnected
|
|
if (throwOnDisconnected && isProviderDisconnected(providerName)) {
|
|
throw new Error(
|
|
`${providerName.charAt(0).toUpperCase() + providerName.slice(1)} CLI is disconnected from the app. ` +
|
|
`Please go to Settings > Providers and click "Sign In" to reconnect.`
|
|
);
|
|
}
|
|
|
|
const provider = this.getProviderByName(providerName);
|
|
|
|
if (!provider) {
|
|
// Fallback to claude if provider not found
|
|
const claudeReg = providerRegistry.get('claude');
|
|
if (claudeReg) {
|
|
return claudeReg.factory();
|
|
}
|
|
throw new Error(`No provider found for model: ${modelId}`);
|
|
}
|
|
|
|
return provider;
|
|
}
|
|
|
|
/**
|
|
* Get the provider name for a given model ID (without creating provider instance)
|
|
*/
|
|
static getProviderForModelName(modelId: string): string {
|
|
const lowerModel = modelId.toLowerCase();
|
|
|
|
// Get all registered providers sorted by priority (descending)
|
|
const registrations = Array.from(providerRegistry.entries()).sort(
|
|
([, a], [, b]) => (b.priority ?? 0) - (a.priority ?? 0)
|
|
);
|
|
|
|
// Check each provider's canHandleModel function
|
|
for (const [name, reg] of registrations) {
|
|
if (reg.canHandleModel?.(lowerModel)) {
|
|
return name;
|
|
}
|
|
}
|
|
|
|
// Fallback: Check for explicit prefixes
|
|
for (const [name] of registrations) {
|
|
if (lowerModel.startsWith(`${name}-`)) {
|
|
return name;
|
|
}
|
|
}
|
|
|
|
// Default to claude (first registered provider or claude)
|
|
return 'claude';
|
|
}
|
|
|
|
/**
|
|
* Get all available providers
|
|
*/
|
|
static getAllProviders(): BaseProvider[] {
|
|
return Array.from(providerRegistry.values()).map((reg) => reg.factory());
|
|
}
|
|
|
|
/**
|
|
* Check installation status for all providers
|
|
*
|
|
* @returns Map of provider name to installation status
|
|
*/
|
|
static async checkAllProviders(): Promise<Record<string, InstallationStatus>> {
|
|
const statuses: Record<string, InstallationStatus> = {};
|
|
|
|
for (const [name, reg] of providerRegistry.entries()) {
|
|
const provider = reg.factory();
|
|
const status = await provider.detectInstallation();
|
|
statuses[name] = status;
|
|
}
|
|
|
|
return statuses;
|
|
}
|
|
|
|
/**
|
|
* Get provider by name (for direct access if needed)
|
|
*
|
|
* @param name Provider name (e.g., "claude", "cursor") or alias (e.g., "anthropic")
|
|
* @returns Provider instance or null if not found
|
|
*/
|
|
static getProviderByName(name: string): BaseProvider | null {
|
|
const lowerName = name.toLowerCase();
|
|
|
|
// Direct lookup
|
|
const directReg = providerRegistry.get(lowerName);
|
|
if (directReg) {
|
|
return directReg.factory();
|
|
}
|
|
|
|
// Check aliases
|
|
for (const [, reg] of providerRegistry.entries()) {
|
|
if (reg.aliases?.includes(lowerName)) {
|
|
return reg.factory();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get all available models from all providers
|
|
*/
|
|
static getAllAvailableModels(): ModelDefinition[] {
|
|
const providers = this.getAllProviders();
|
|
return providers.flatMap((p) => p.getAvailableModels());
|
|
}
|
|
|
|
/**
|
|
* Get list of registered provider names
|
|
*/
|
|
static getRegisteredProviderNames(): string[] {
|
|
return Array.from(providerRegistry.keys());
|
|
}
|
|
|
|
/**
|
|
* Check if a specific model supports vision/image input
|
|
*
|
|
* @param modelId Model identifier
|
|
* @returns Whether the model supports vision (defaults to true if model not found)
|
|
*/
|
|
static modelSupportsVision(modelId: string): boolean {
|
|
const provider = this.getProviderForModel(modelId);
|
|
const models = provider.getAvailableModels();
|
|
|
|
// Find the model in the available models list
|
|
for (const model of models) {
|
|
if (
|
|
model.id === modelId ||
|
|
model.modelString === modelId ||
|
|
model.id.endsWith(`-${modelId}`) ||
|
|
model.modelString.endsWith(`-${modelId}`) ||
|
|
model.modelString === modelId.replace(/^(claude|cursor|codex)-/, '') ||
|
|
model.modelString === modelId.replace(/-(claude|cursor|codex)$/, '')
|
|
) {
|
|
return model.supportsVision ?? true;
|
|
}
|
|
}
|
|
|
|
// Also try exact match with model string from provider's model map
|
|
for (const model of models) {
|
|
if (model.modelString === modelId || model.id === modelId) {
|
|
return model.supportsVision ?? true;
|
|
}
|
|
}
|
|
|
|
// Default to true (Claude SDK supports vision by default)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Provider Registrations
|
|
// =============================================================================
|
|
|
|
// Import providers for registration side-effects
|
|
import { ClaudeProvider } from './claude-provider.js';
|
|
import { CursorProvider } from './cursor-provider.js';
|
|
import { CodexProvider } from './codex-provider.js';
|
|
import { OpencodeProvider } from './opencode-provider.js';
|
|
|
|
// Register Claude provider
|
|
registerProvider('claude', {
|
|
factory: () => new ClaudeProvider(),
|
|
aliases: ['anthropic'],
|
|
canHandleModel: (model: string) => {
|
|
return (
|
|
model.startsWith('claude-') || ['opus', 'sonnet', 'haiku'].some((n) => model.includes(n))
|
|
);
|
|
},
|
|
priority: 0, // Default priority
|
|
});
|
|
|
|
// Register Cursor provider
|
|
registerProvider('cursor', {
|
|
factory: () => new CursorProvider(),
|
|
canHandleModel: (model: string) => isCursorModel(model),
|
|
priority: 10, // Higher priority - check Cursor models first
|
|
});
|
|
|
|
// Register Codex provider
|
|
registerProvider('codex', {
|
|
factory: () => new CodexProvider(),
|
|
aliases: ['openai'],
|
|
canHandleModel: (model: string) => isCodexModel(model),
|
|
priority: 5, // Medium priority - check after Cursor but before Claude
|
|
});
|
|
|
|
// Register OpenCode provider
|
|
registerProvider('opencode', {
|
|
factory: () => new OpencodeProvider(),
|
|
canHandleModel: (model: string) => isOpencodeModel(model),
|
|
priority: 3, // Between codex (5) and claude (0)
|
|
});
|