mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
- Changed model identifier from `claude-opus-4-5-20251101` to `claude-opus-4-6` across various files, including documentation and code references. - Updated the SDK to support adaptive thinking for Opus 4.6, allowing the model to determine its own reasoning depth. - Enhanced the thinking level options to include 'adaptive' and adjusted related components to reflect this change. - Updated tests to ensure compatibility with the new model and its features. These changes improve the model's capabilities and user experience by leveraging adaptive reasoning.
331 lines
10 KiB
TypeScript
331 lines
10 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,
|
|
isGeminiModel,
|
|
isCopilotModel,
|
|
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',
|
|
gemini: '.gemini-disconnected',
|
|
copilot: '.copilot-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-6", "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|gemini)-/, '') ||
|
|
model.modelString === modelId.replace(/-(claude|cursor|codex|gemini)$/, '')
|
|
) {
|
|
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';
|
|
import { GeminiProvider } from './gemini-provider.js';
|
|
import { CopilotProvider } from './copilot-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)
|
|
});
|
|
|
|
// Register Gemini provider
|
|
registerProvider('gemini', {
|
|
factory: () => new GeminiProvider(),
|
|
aliases: ['google'],
|
|
canHandleModel: (model: string) => isGeminiModel(model),
|
|
priority: 4, // Between opencode (3) and codex (5)
|
|
});
|
|
|
|
// Register Copilot provider (GitHub Copilot SDK)
|
|
registerProvider('copilot', {
|
|
factory: () => new CopilotProvider(),
|
|
aliases: ['github-copilot', 'github'],
|
|
canHandleModel: (model: string) => isCopilotModel(model),
|
|
priority: 6, // High priority - check before Codex since both can handle GPT models
|
|
});
|