V2 should replace provider-specific branching with a registry of allowlisted adapters. Each adapter owns discovery, status, auth launch, smoke testing, generation, and sanitization for one provider family.
The registry is not a generic shell runner. It is a typed table of supported runtimes with narrow commands and strict output boundaries.
type AiProviderKind =
| "agent_cli"
| "local_model_server"
| "api_gateway"
| "api_direct";
agent_cli: terminal-native AI coding agent, such as Claude Code, Gemini CLI, Codex CLI, Kiro CLI, Copilot CLI, Qwen Code, OpenCode, Aider, Continue, or Cursor Agent.local_model_server: local runtime with an HTTP API or SDK, such as Ollama or LM Studio.api_gateway: one API facade that routes to many models, such as LiteLLM or OpenRouter.api_direct: direct provider API integration, such as the existing Anthropic API-key provider.interface AiRuntimeProvider {
id: string;
label: string;
kind: AiProviderKind;
discover(): Promise<ProviderDiscovery>;
getStatus(): Promise<ProviderStatus>;
openLogin?(selection?: ProviderSelection): Promise<LoginLaunchResult>;
test(selection?: ProviderSelection): Promise<SmokeTestResult>;
generate(input: GenerateInput): AsyncIterable<GenerateEvent>;
sanitize(value: unknown): PublicProviderOutput;
}
type ProviderStatusValue =
| "installed"
| "missing"
| "needs_auth"
| "ready"
| "blocked"
| "failed"
| "unknown";
interface ProviderDiscovery {
providerId: string;
installed: boolean;
commandSource: "explicit" | "known_path" | "path" | "server" | "missing";
commandDisplay?: string;
versionDisplay?: string;
warnings: string[];
}
interface ProviderStatus {
providerId: string;
status: ProviderStatusValue;
message: string;
suggestedFix?: string;
capabilities: ProviderCapability[];
sanitizedDetails?: Record<string, string | number | boolean | null>;
}
type ProviderCapability =
| "headless_text"
| "streaming"
| "json_output"
| "local_only"
| "subscription_auth"
| "api_key_auth"
| "model_server"
| "file_tools"
| "shell_tools";
interface ProviderSelection {
profileId?: string;
manualConfigDir?: string;
model?: string;
endpoint?: string;
}
interface SmokeTestResult {
providerId: string;
ok: boolean;
status: "passed" | "failed" | "blocked" | "timeout" | "unknown";
message: string;
outputPreview?: string;
testedAt: string;
configFingerprint: string;
}
interface GenerateInput {
prompt: string;
context: string;
citations: Array<{ source: string; section?: string; score?: number }>;
timeoutMs: number;
abortSignal?: AbortSignal;
}
type GenerateEvent =
| { type: "text"; text: string }
| { type: "citation"; source: string; section?: string; score?: number }
| { type: "status"; message: string }
| { type: "error"; code: string; message: string; suggestedFix?: string };
interface PublicProviderOutput {
safe: true;
value: unknown;
}
The registry should:
.env.local.Each adapter should:
Planned V2 endpoints:
GET /api/settings/providersGET /api/settings/providers/:providerId/statusPOST /api/settings/providers/:providerId/testPOST /api/settings/providers/:providerId/loginPOST /api/settings/providers/activeCompatibility rule:
/api/chat fully use the registry.V2 should support both old and new config:
LLM_PROVIDER=anthropic_api
AI_PROVIDER=claude_code_cli
AI_PROVIDER_MODEL=
AI_PROVIDER_ENDPOINT=
Migration behavior:
AI_PROVIDER is missing, map LLM_PROVIDER=anthropic_api to anthropic_api.AI_PROVIDER is missing, map LLM_PROVIDER=claude_code_cli to claude_code_cli.LLM_PROVIDER until a later migration.Provider generation should produce a common event stream:
generate.GenerateEvent.For local model servers and gateways, prefer HTTP streaming if stable. For CLIs, prefer structured stream output only when official docs verify it.
Start with these normalized codes:
provider_missingprovider_not_enabledauth_missingauth_blocked_by_policypermission_requiredtimeoutinvalid_outputexecution_failedlocal_only_requiredproduction_rejectedAdapters can keep raw provider details server-side for debugging only when logs are local and sanitized before display.