refactor(providers): extract provider management to dedicated module
- Create src/agent/providers/ with registry.ts and resolver.ts - registry.ts: Provider metadata, status checking, login instructions - resolver.ts: API key resolution, model resolution - oauth/providers.ts now re-exports from providers/ (deprecated) - tools.ts: Remove PROVIDER_ALIAS and DEFAULT_MODELS (moved to providers/) - Update imports in runner.ts and chat.ts This separates concerns: - oauth/ only handles OAuth credential reading - providers/ manages all provider metadata and resolution
This commit is contained in:
parent
5952f22ca2
commit
6723aa8561
7 changed files with 503 additions and 382 deletions
|
|
@ -17,7 +17,7 @@ import {
|
|||
getCurrentProvider,
|
||||
getLoginInstructions,
|
||||
type ProviderInfo,
|
||||
} from "../../oauth/providers.js";
|
||||
} from "../../providers/index.js";
|
||||
|
||||
type ChatOptions = {
|
||||
profile?: string;
|
||||
|
|
|
|||
|
|
@ -1,292 +1,20 @@
|
|||
/**
|
||||
* Provider Management
|
||||
* @deprecated This file is deprecated. Import from '../providers/index.js' instead.
|
||||
*
|
||||
* Manage LLM providers with support for:
|
||||
* - API Key authentication (traditional)
|
||||
* - OAuth authentication (Claude Code, Codex)
|
||||
* This file re-exports from the new providers/ module for backwards compatibility.
|
||||
* Will be removed in a future version.
|
||||
*/
|
||||
|
||||
import { credentialManager } from "../credentials.js";
|
||||
import {
|
||||
readClaudeCliCredentials,
|
||||
readCodexCliCredentials,
|
||||
hasValidClaudeCliCredentials,
|
||||
hasValidCodexCliCredentials,
|
||||
type ClaudeCliCredential,
|
||||
type CodexCliCredential,
|
||||
} from "./cli-credentials.js";
|
||||
|
||||
// ============================================================
|
||||
// Types
|
||||
// ============================================================
|
||||
|
||||
export type AuthMethod = "api-key" | "oauth";
|
||||
|
||||
export interface ProviderInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
authMethod: AuthMethod;
|
||||
available: boolean;
|
||||
configured: boolean;
|
||||
current: boolean;
|
||||
models: string[];
|
||||
loginUrl?: string;
|
||||
loginCommand?: string;
|
||||
}
|
||||
|
||||
export interface ProviderConfig {
|
||||
provider: string;
|
||||
model?: string | undefined;
|
||||
apiKey?: string | undefined;
|
||||
baseUrl?: string | undefined;
|
||||
// OAuth specific
|
||||
accessToken?: string | undefined;
|
||||
refreshToken?: string | undefined;
|
||||
expires?: number | undefined;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Provider Registry
|
||||
// ============================================================
|
||||
|
||||
const PROVIDER_INFO: Record<string, Omit<ProviderInfo, "available" | "configured" | "current">> = {
|
||||
"anthropic": {
|
||||
id: "anthropic",
|
||||
name: "Anthropic (API Key)",
|
||||
authMethod: "api-key",
|
||||
models: ["claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-haiku-3-5-20241022"],
|
||||
loginUrl: "https://console.anthropic.com/",
|
||||
},
|
||||
"claude-code": {
|
||||
id: "claude-code",
|
||||
name: "Claude Code (OAuth)",
|
||||
authMethod: "oauth",
|
||||
models: ["claude-sonnet-4-20250514", "claude-opus-4-20250514"],
|
||||
loginCommand: "claude login",
|
||||
},
|
||||
"openai": {
|
||||
id: "openai",
|
||||
name: "OpenAI",
|
||||
authMethod: "api-key",
|
||||
models: ["gpt-4o", "gpt-4o-mini", "o1", "o1-mini"],
|
||||
loginUrl: "https://platform.openai.com/api-keys",
|
||||
},
|
||||
"openai-codex": {
|
||||
id: "openai-codex",
|
||||
name: "Codex (OAuth)",
|
||||
authMethod: "oauth",
|
||||
models: ["gpt-5.1", "gpt-5.1-codex-max"],
|
||||
loginCommand: "codex login",
|
||||
},
|
||||
"kimi-coding": {
|
||||
id: "kimi-coding",
|
||||
name: "Kimi Code",
|
||||
authMethod: "api-key",
|
||||
models: ["kimi-k2-thinking", "k2p5"],
|
||||
loginUrl: "https://kimi.moonshot.cn/",
|
||||
},
|
||||
"google": {
|
||||
id: "google",
|
||||
name: "Google AI",
|
||||
authMethod: "api-key",
|
||||
models: ["gemini-2.0-flash", "gemini-1.5-pro"],
|
||||
loginUrl: "https://aistudio.google.com/apikey",
|
||||
},
|
||||
"groq": {
|
||||
id: "groq",
|
||||
name: "Groq",
|
||||
authMethod: "api-key",
|
||||
models: ["llama-3.3-70b-versatile", "mixtral-8x7b-32768"],
|
||||
loginUrl: "https://console.groq.com/keys",
|
||||
},
|
||||
"mistral": {
|
||||
id: "mistral",
|
||||
name: "Mistral",
|
||||
authMethod: "api-key",
|
||||
models: ["mistral-large-latest", "codestral-latest"],
|
||||
loginUrl: "https://console.mistral.ai/api-keys",
|
||||
},
|
||||
"xai": {
|
||||
id: "xai",
|
||||
name: "xAI (Grok)",
|
||||
authMethod: "api-key",
|
||||
models: ["grok-beta", "grok-vision-beta"],
|
||||
loginUrl: "https://console.x.ai/",
|
||||
},
|
||||
"openrouter": {
|
||||
id: "openrouter",
|
||||
name: "OpenRouter",
|
||||
authMethod: "api-key",
|
||||
models: ["anthropic/claude-3.5-sonnet", "openai/gpt-4o"],
|
||||
loginUrl: "https://openrouter.ai/keys",
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Provider Status
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Check if a provider is configured with API key in credentials.json5
|
||||
*/
|
||||
function isApiKeyConfigured(providerId: string): boolean {
|
||||
const config = credentialManager.getLlmProviderConfig(providerId);
|
||||
return !!config?.apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OAuth provider has valid credentials
|
||||
*/
|
||||
function isOAuthAvailable(providerId: string): boolean {
|
||||
if (providerId === "claude-code") {
|
||||
return hasValidClaudeCliCredentials();
|
||||
}
|
||||
if (providerId === "openai-codex") {
|
||||
return hasValidCodexCliCredentials();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current provider from credentials
|
||||
*/
|
||||
export function getCurrentProvider(): string {
|
||||
return credentialManager.getLlmProvider() ?? "kimi-coding";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of all providers with their status
|
||||
*/
|
||||
export function getProviderList(): ProviderInfo[] {
|
||||
const currentProvider = getCurrentProvider();
|
||||
|
||||
return Object.values(PROVIDER_INFO).map((info) => {
|
||||
const isOAuth = info.authMethod === "oauth";
|
||||
const available = isOAuth ? isOAuthAvailable(info.id) : isApiKeyConfigured(info.id);
|
||||
const configured = isOAuth ? isOAuthAvailable(info.id) : isApiKeyConfigured(info.id);
|
||||
|
||||
// Check if this is the current provider
|
||||
// For claude-code, check if current is "anthropic" and OAuth is available
|
||||
let isCurrent = currentProvider === info.id;
|
||||
if (info.id === "claude-code" && currentProvider === "anthropic") {
|
||||
// If anthropic is current and claude-code OAuth is available, mark both
|
||||
isCurrent = hasValidClaudeCliCredentials();
|
||||
}
|
||||
|
||||
return {
|
||||
...info,
|
||||
available,
|
||||
configured,
|
||||
current: isCurrent,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available providers only
|
||||
*/
|
||||
export function getAvailableProviders(): ProviderInfo[] {
|
||||
return getProviderList().filter((p) => p.available);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Provider Resolution
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get provider config for making API calls
|
||||
*/
|
||||
export function resolveProviderConfig(providerId: string): ProviderConfig | null {
|
||||
const info = PROVIDER_INFO[providerId];
|
||||
if (!info) return null;
|
||||
|
||||
if (info.authMethod === "oauth") {
|
||||
if (providerId === "claude-code") {
|
||||
const creds = readClaudeCliCredentials();
|
||||
if (!creds) return null;
|
||||
|
||||
const accessToken = creds.type === "oauth" ? creds.access : creds.token;
|
||||
return {
|
||||
provider: "anthropic", // Use anthropic API
|
||||
apiKey: accessToken,
|
||||
accessToken,
|
||||
refreshToken: creds.type === "oauth" ? creds.refresh : undefined,
|
||||
expires: creds.expires,
|
||||
};
|
||||
}
|
||||
|
||||
if (providerId === "openai-codex") {
|
||||
const creds = readCodexCliCredentials();
|
||||
if (!creds) return null;
|
||||
|
||||
return {
|
||||
provider: "openai-codex",
|
||||
accessToken: creds.access,
|
||||
refreshToken: creds.refresh,
|
||||
expires: creds.expires,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// API Key based
|
||||
const config = credentialManager.getLlmProviderConfig(providerId);
|
||||
if (!config?.apiKey) return null;
|
||||
|
||||
return {
|
||||
provider: providerId,
|
||||
model: config.model,
|
||||
apiKey: config.apiKey,
|
||||
baseUrl: config.baseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format provider for display
|
||||
*/
|
||||
export function formatProviderStatus(provider: ProviderInfo): string {
|
||||
const status = provider.available ? "✓" : "✗";
|
||||
const current = provider.current ? " (current)" : "";
|
||||
const auth = provider.authMethod === "oauth" ? " [OAuth]" : "";
|
||||
return `${status} ${provider.name}${auth}${current}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get login instructions for a provider
|
||||
*/
|
||||
export function getLoginInstructions(providerId: string): string {
|
||||
const info = PROVIDER_INFO[providerId];
|
||||
if (!info) return `Unknown provider: ${providerId}`;
|
||||
|
||||
if (info.authMethod === "oauth") {
|
||||
if (info.loginCommand) {
|
||||
return `Run: ${info.loginCommand}\nThen restart Super Multica to use the credentials.`;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.loginUrl) {
|
||||
return `Get your API key at: ${info.loginUrl}\nThen add it to ~/.super-multica/credentials.json5`;
|
||||
}
|
||||
|
||||
return "No login instructions available.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider uses OAuth authentication
|
||||
*/
|
||||
export function isOAuthProvider(providerId: string): boolean {
|
||||
const info = PROVIDER_INFO[providerId];
|
||||
return info?.authMethod === "oauth";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if provider is available (has valid credentials)
|
||||
*/
|
||||
export function isProviderAvailable(providerId: string): boolean {
|
||||
const info = PROVIDER_INFO[providerId];
|
||||
if (!info) return false;
|
||||
|
||||
if (info.authMethod === "oauth") {
|
||||
return isOAuthAvailable(providerId);
|
||||
}
|
||||
return isApiKeyConfigured(providerId);
|
||||
}
|
||||
export {
|
||||
type AuthMethod,
|
||||
type ProviderInfo,
|
||||
type ProviderConfig,
|
||||
isOAuthProvider,
|
||||
isProviderAvailable,
|
||||
getCurrentProvider,
|
||||
getProviderList,
|
||||
getAvailableProviders,
|
||||
formatProviderStatus,
|
||||
getLoginInstructions,
|
||||
resolveProviderConfig,
|
||||
} from "../providers/index.js";
|
||||
|
|
|
|||
34
src/agent/providers/index.ts
Normal file
34
src/agent/providers/index.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* Provider Management
|
||||
*
|
||||
* Unified exports for LLM provider management:
|
||||
* - Registry: Provider metadata, status checking, listing
|
||||
* - Resolver: API key resolution, model resolution
|
||||
*/
|
||||
|
||||
// Registry exports
|
||||
export {
|
||||
type AuthMethod,
|
||||
type ProviderInfo,
|
||||
type ProviderMeta,
|
||||
PROVIDER_ALIAS,
|
||||
isOAuthProvider,
|
||||
isProviderAvailable,
|
||||
getCurrentProvider,
|
||||
getProviderMeta,
|
||||
getDefaultModel,
|
||||
getProviderList,
|
||||
getAvailableProviders,
|
||||
formatProviderStatus,
|
||||
getLoginInstructions,
|
||||
} from "./registry.js";
|
||||
|
||||
// Resolver exports
|
||||
export {
|
||||
type ProviderConfig,
|
||||
resolveProviderConfig,
|
||||
resolveApiKey,
|
||||
resolveBaseUrl,
|
||||
resolveModelId,
|
||||
resolveModel,
|
||||
} from "./resolver.js";
|
||||
275
src/agent/providers/registry.ts
Normal file
275
src/agent/providers/registry.ts
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
/**
|
||||
* Provider Registry
|
||||
*
|
||||
* Central registry for all LLM providers with metadata,
|
||||
* status checking, and display formatting.
|
||||
*/
|
||||
|
||||
import { credentialManager } from "../credentials.js";
|
||||
import {
|
||||
hasValidClaudeCliCredentials,
|
||||
hasValidCodexCliCredentials,
|
||||
} from "../oauth/cli-credentials.js";
|
||||
|
||||
// ============================================================
|
||||
// Types
|
||||
// ============================================================
|
||||
|
||||
export type AuthMethod = "api-key" | "oauth";
|
||||
|
||||
export interface ProviderInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
authMethod: AuthMethod;
|
||||
available: boolean;
|
||||
configured: boolean;
|
||||
current: boolean;
|
||||
defaultModel: string;
|
||||
models: string[];
|
||||
loginUrl?: string | undefined;
|
||||
loginCommand?: string | undefined;
|
||||
}
|
||||
|
||||
/** Static provider metadata (without runtime status) */
|
||||
export interface ProviderMeta {
|
||||
id: string;
|
||||
name: string;
|
||||
authMethod: AuthMethod;
|
||||
defaultModel: string;
|
||||
models: string[];
|
||||
loginUrl?: string | undefined;
|
||||
loginCommand?: string | undefined;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Provider Registry
|
||||
// ============================================================
|
||||
|
||||
const PROVIDER_REGISTRY: Record<string, ProviderMeta> = {
|
||||
"claude-code": {
|
||||
id: "claude-code",
|
||||
name: "Claude Code (OAuth)",
|
||||
authMethod: "oauth",
|
||||
defaultModel: "claude-sonnet-4-20250514",
|
||||
models: ["claude-sonnet-4-20250514", "claude-opus-4-20250514"],
|
||||
loginCommand: "claude login",
|
||||
},
|
||||
"openai-codex": {
|
||||
id: "openai-codex",
|
||||
name: "Codex (OAuth)",
|
||||
authMethod: "oauth",
|
||||
defaultModel: "gpt-5.1",
|
||||
models: ["gpt-5.1", "gpt-5.1-codex-max"],
|
||||
loginCommand: "codex login",
|
||||
},
|
||||
"anthropic": {
|
||||
id: "anthropic",
|
||||
name: "Anthropic (API Key)",
|
||||
authMethod: "api-key",
|
||||
defaultModel: "claude-sonnet-4-20250514",
|
||||
models: ["claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-haiku-3-5-20241022"],
|
||||
loginUrl: "https://console.anthropic.com/",
|
||||
},
|
||||
"openai": {
|
||||
id: "openai",
|
||||
name: "OpenAI",
|
||||
authMethod: "api-key",
|
||||
defaultModel: "gpt-4o",
|
||||
models: ["gpt-4o", "gpt-4o-mini", "o1", "o1-mini"],
|
||||
loginUrl: "https://platform.openai.com/api-keys",
|
||||
},
|
||||
"kimi-coding": {
|
||||
id: "kimi-coding",
|
||||
name: "Kimi Code",
|
||||
authMethod: "api-key",
|
||||
defaultModel: "kimi-k2-thinking",
|
||||
models: ["kimi-k2-thinking", "k2p5"],
|
||||
loginUrl: "https://kimi.moonshot.cn/",
|
||||
},
|
||||
"google": {
|
||||
id: "google",
|
||||
name: "Google AI",
|
||||
authMethod: "api-key",
|
||||
defaultModel: "gemini-2.0-flash",
|
||||
models: ["gemini-2.0-flash", "gemini-1.5-pro"],
|
||||
loginUrl: "https://aistudio.google.com/apikey",
|
||||
},
|
||||
"groq": {
|
||||
id: "groq",
|
||||
name: "Groq",
|
||||
authMethod: "api-key",
|
||||
defaultModel: "llama-3.3-70b-versatile",
|
||||
models: ["llama-3.3-70b-versatile", "mixtral-8x7b-32768"],
|
||||
loginUrl: "https://console.groq.com/keys",
|
||||
},
|
||||
"mistral": {
|
||||
id: "mistral",
|
||||
name: "Mistral",
|
||||
authMethod: "api-key",
|
||||
defaultModel: "mistral-large-latest",
|
||||
models: ["mistral-large-latest", "codestral-latest"],
|
||||
loginUrl: "https://console.mistral.ai/api-keys",
|
||||
},
|
||||
"xai": {
|
||||
id: "xai",
|
||||
name: "xAI (Grok)",
|
||||
authMethod: "api-key",
|
||||
defaultModel: "grok-beta",
|
||||
models: ["grok-beta", "grok-vision-beta"],
|
||||
loginUrl: "https://console.x.ai/",
|
||||
},
|
||||
"openrouter": {
|
||||
id: "openrouter",
|
||||
name: "OpenRouter",
|
||||
authMethod: "api-key",
|
||||
defaultModel: "anthropic/claude-3.5-sonnet",
|
||||
models: ["anthropic/claude-3.5-sonnet", "openai/gpt-4o"],
|
||||
loginUrl: "https://openrouter.ai/keys",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider alias mapping for OAuth providers.
|
||||
* Maps friendly names to actual pi-ai provider names.
|
||||
*/
|
||||
export const PROVIDER_ALIAS: Record<string, string> = {
|
||||
"claude-code": "anthropic", // Claude Code OAuth uses anthropic API
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Status Checking
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Check if a provider is configured with API key in credentials.json5
|
||||
*/
|
||||
function isApiKeyConfigured(providerId: string): boolean {
|
||||
const config = credentialManager.getLlmProviderConfig(providerId);
|
||||
return !!config?.apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OAuth provider has valid credentials
|
||||
*/
|
||||
function isOAuthAvailable(providerId: string): boolean {
|
||||
if (providerId === "claude-code") {
|
||||
return hasValidClaudeCliCredentials();
|
||||
}
|
||||
if (providerId === "openai-codex") {
|
||||
return hasValidCodexCliCredentials();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider uses OAuth authentication
|
||||
*/
|
||||
export function isOAuthProvider(providerId: string): boolean {
|
||||
const info = PROVIDER_REGISTRY[providerId];
|
||||
return info?.authMethod === "oauth";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if provider is available (has valid credentials)
|
||||
*/
|
||||
export function isProviderAvailable(providerId: string): boolean {
|
||||
const info = PROVIDER_REGISTRY[providerId];
|
||||
if (!info) return false;
|
||||
|
||||
if (info.authMethod === "oauth") {
|
||||
return isOAuthAvailable(providerId);
|
||||
}
|
||||
return isApiKeyConfigured(providerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current provider from credentials
|
||||
*/
|
||||
export function getCurrentProvider(): string {
|
||||
return credentialManager.getLlmProvider() ?? "kimi-coding";
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Provider Listing
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get static provider metadata
|
||||
*/
|
||||
export function getProviderMeta(providerId: string): ProviderMeta | undefined {
|
||||
return PROVIDER_REGISTRY[providerId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default model for a provider
|
||||
*/
|
||||
export function getDefaultModel(providerId: string): string | undefined {
|
||||
return PROVIDER_REGISTRY[providerId]?.defaultModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of all providers with their runtime status
|
||||
*/
|
||||
export function getProviderList(): ProviderInfo[] {
|
||||
const currentProvider = getCurrentProvider();
|
||||
|
||||
return Object.values(PROVIDER_REGISTRY).map((meta) => {
|
||||
const isOAuth = meta.authMethod === "oauth";
|
||||
const available = isOAuth ? isOAuthAvailable(meta.id) : isApiKeyConfigured(meta.id);
|
||||
|
||||
// Check if this is the current provider
|
||||
// For claude-code, check if current is "anthropic" and OAuth is available
|
||||
let isCurrent = currentProvider === meta.id;
|
||||
if (meta.id === "claude-code" && currentProvider === "anthropic") {
|
||||
isCurrent = hasValidClaudeCliCredentials();
|
||||
}
|
||||
|
||||
return {
|
||||
...meta,
|
||||
available,
|
||||
configured: available,
|
||||
current: isCurrent,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available providers only
|
||||
*/
|
||||
export function getAvailableProviders(): ProviderInfo[] {
|
||||
return getProviderList().filter((p) => p.available);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Display Helpers
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Format provider for display
|
||||
*/
|
||||
export function formatProviderStatus(provider: ProviderInfo): string {
|
||||
const status = provider.available ? "✓" : "✗";
|
||||
const current = provider.current ? " (current)" : "";
|
||||
const auth = provider.authMethod === "oauth" ? " [OAuth]" : "";
|
||||
return `${status} ${provider.name}${auth}${current}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get login instructions for a provider
|
||||
*/
|
||||
export function getLoginInstructions(providerId: string): string {
|
||||
const info = PROVIDER_REGISTRY[providerId];
|
||||
if (!info) return `Unknown provider: ${providerId}`;
|
||||
|
||||
if (info.authMethod === "oauth") {
|
||||
if (info.loginCommand) {
|
||||
return `Run: ${info.loginCommand}\nThen restart Super Multica to use the credentials.`;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.loginUrl) {
|
||||
return `Get your API key at: ${info.loginUrl}\nThen add it to ~/.super-multica/credentials.json5`;
|
||||
}
|
||||
|
||||
return "No login instructions available.";
|
||||
}
|
||||
166
src/agent/providers/resolver.ts
Normal file
166
src/agent/providers/resolver.ts
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* Provider Resolver
|
||||
*
|
||||
* Resolves provider configuration for making API calls,
|
||||
* including API keys, OAuth tokens, and model selection.
|
||||
*/
|
||||
|
||||
import { getModel } from "@mariozechner/pi-ai";
|
||||
import { credentialManager } from "../credentials.js";
|
||||
import {
|
||||
readClaudeCliCredentials,
|
||||
readCodexCliCredentials,
|
||||
} from "../oauth/cli-credentials.js";
|
||||
import {
|
||||
PROVIDER_ALIAS,
|
||||
getProviderMeta,
|
||||
getDefaultModel,
|
||||
isOAuthProvider,
|
||||
} from "./registry.js";
|
||||
import type { AgentOptions } from "../types.js";
|
||||
|
||||
// ============================================================
|
||||
// Types
|
||||
// ============================================================
|
||||
|
||||
export interface ProviderConfig {
|
||||
provider: string;
|
||||
model?: string | undefined;
|
||||
apiKey?: string | undefined;
|
||||
baseUrl?: string | undefined;
|
||||
// OAuth specific
|
||||
accessToken?: string | undefined;
|
||||
refreshToken?: string | undefined;
|
||||
expires?: number | undefined;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Provider Config Resolution
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get provider config for making API calls.
|
||||
* Handles both OAuth and API Key authentication.
|
||||
*/
|
||||
export function resolveProviderConfig(providerId: string): ProviderConfig | null {
|
||||
const meta = getProviderMeta(providerId);
|
||||
if (!meta) return null;
|
||||
|
||||
if (meta.authMethod === "oauth") {
|
||||
if (providerId === "claude-code") {
|
||||
const creds = readClaudeCliCredentials();
|
||||
if (!creds) return null;
|
||||
|
||||
const accessToken = creds.type === "oauth" ? creds.access : creds.token;
|
||||
return {
|
||||
provider: "anthropic", // Use anthropic API
|
||||
apiKey: accessToken,
|
||||
accessToken,
|
||||
refreshToken: creds.type === "oauth" ? creds.refresh : undefined,
|
||||
expires: creds.expires,
|
||||
};
|
||||
}
|
||||
|
||||
if (providerId === "openai-codex") {
|
||||
const creds = readCodexCliCredentials();
|
||||
if (!creds) return null;
|
||||
|
||||
return {
|
||||
provider: "openai-codex",
|
||||
accessToken: creds.access,
|
||||
refreshToken: creds.refresh,
|
||||
expires: creds.expires,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// API Key based
|
||||
const config = credentialManager.getLlmProviderConfig(providerId);
|
||||
if (!config?.apiKey) return null;
|
||||
|
||||
return {
|
||||
provider: providerId,
|
||||
model: config.model,
|
||||
apiKey: config.apiKey,
|
||||
baseUrl: config.baseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// API Key Resolution
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get API Key based on provider.
|
||||
* Priority: explicit key > OAuth credentials > credentials.json5 config.
|
||||
*/
|
||||
export function resolveApiKey(provider: string, explicitKey?: string): string | undefined {
|
||||
if (explicitKey) return explicitKey;
|
||||
|
||||
// Try OAuth providers first (claude-code, openai-codex)
|
||||
const providerConfig = resolveProviderConfig(provider);
|
||||
if (providerConfig?.apiKey) {
|
||||
return providerConfig.apiKey;
|
||||
}
|
||||
if (providerConfig?.accessToken) {
|
||||
return providerConfig.accessToken;
|
||||
}
|
||||
|
||||
// Fall back to credentials.json5
|
||||
return credentialManager.getLlmProviderConfig(provider)?.apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Base URL based on provider.
|
||||
* Priority: explicit URL > credentials.json5 config.
|
||||
*/
|
||||
export function resolveBaseUrl(provider: string, explicitUrl?: string): string | undefined {
|
||||
if (explicitUrl) return explicitUrl;
|
||||
return credentialManager.getLlmProviderConfig(provider)?.baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Model ID based on provider.
|
||||
* Priority: explicit model > credentials.json5 config > default.
|
||||
*/
|
||||
export function resolveModelId(provider: string, explicitModel?: string): string | undefined {
|
||||
if (explicitModel) return explicitModel;
|
||||
return credentialManager.getLlmProviderConfig(provider)?.model ?? getDefaultModel(provider);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Model Resolution
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Resolve model for pi-ai based on provider and options.
|
||||
*/
|
||||
export function resolveModel(options: AgentOptions) {
|
||||
if (options.provider && options.model) {
|
||||
// Map provider alias (e.g., claude-code -> anthropic)
|
||||
const actualProvider = PROVIDER_ALIAS[options.provider] ?? options.provider;
|
||||
|
||||
// Type assertion needed because provider/model come from dynamic user config
|
||||
return (getModel as (p: string, m: string) => ReturnType<typeof getModel>)(
|
||||
actualProvider,
|
||||
options.model,
|
||||
);
|
||||
}
|
||||
|
||||
// If only provider specified, use default model for that provider
|
||||
if (options.provider) {
|
||||
const actualProvider = PROVIDER_ALIAS[options.provider] ?? options.provider;
|
||||
const defaultModel = getDefaultModel(options.provider) ?? getDefaultModel(actualProvider);
|
||||
if (defaultModel) {
|
||||
return (getModel as (p: string, m: string) => ReturnType<typeof getModel>)(
|
||||
actualProvider,
|
||||
defaultModel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return getModel("kimi-coding", "kimi-k2-thinking");
|
||||
}
|
||||
|
||||
// Re-export for convenience
|
||||
export { isOAuthProvider };
|
||||
|
|
@ -7,7 +7,13 @@ import { SessionManager } from "./session/session-manager.js";
|
|||
import { ProfileManager } from "./profile/index.js";
|
||||
import { SkillManager } from "./skills/index.js";
|
||||
import { credentialManager, getCredentialsPath } from "./credentials.js";
|
||||
import { resolveProviderConfig, isOAuthProvider, getLoginInstructions } from "./oauth/providers.js";
|
||||
import {
|
||||
resolveApiKey,
|
||||
resolveBaseUrl,
|
||||
resolveModelId,
|
||||
isOAuthProvider,
|
||||
getLoginInstructions,
|
||||
} from "./providers/index.js";
|
||||
import {
|
||||
checkContextWindow,
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
|
|
@ -15,47 +21,6 @@ import {
|
|||
} from "./context-window/index.js";
|
||||
import { mergeToolsConfig, type ToolsConfig } from "./tools/policy.js";
|
||||
|
||||
/**
|
||||
* Get API Key based on provider.
|
||||
* Priority: explicit key > OAuth credentials > credentials.json5 config.
|
||||
*
|
||||
* Supports OAuth providers like "claude-code" and "openai-codex" by
|
||||
* reading credentials from their respective CLI tools.
|
||||
*/
|
||||
function resolveApiKey(provider: string, explicitKey?: string): string | undefined {
|
||||
if (explicitKey) return explicitKey;
|
||||
|
||||
// Try OAuth providers first (claude-code, openai-codex)
|
||||
const providerConfig = resolveProviderConfig(provider);
|
||||
if (providerConfig?.apiKey) {
|
||||
return providerConfig.apiKey;
|
||||
}
|
||||
if (providerConfig?.accessToken) {
|
||||
return providerConfig.accessToken;
|
||||
}
|
||||
|
||||
// Fall back to credentials.json5
|
||||
return credentialManager.getLlmProviderConfig(provider)?.apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Base URL based on provider.
|
||||
* Priority: explicit URL > provider-specific env var > generic env var format.
|
||||
*/
|
||||
function resolveBaseUrl(provider: string, explicitUrl?: string): string | undefined {
|
||||
if (explicitUrl) return explicitUrl;
|
||||
return credentialManager.getLlmProviderConfig(provider)?.baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Model ID based on provider.
|
||||
* Priority: explicit model > provider-specific env var > generic env var format.
|
||||
*/
|
||||
function resolveModelId(provider: string, explicitModel?: string): string | undefined {
|
||||
if (explicitModel) return explicitModel;
|
||||
return credentialManager.getLlmProviderConfig(provider)?.model;
|
||||
}
|
||||
|
||||
export class Agent {
|
||||
private readonly agent: PiAgentCore;
|
||||
private readonly output;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { AgentOptions } from "./types.js";
|
||||
import { getModel } from "@mariozechner/pi-ai";
|
||||
import { createCodingTools } from "@mariozechner/pi-coding-agent";
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import { createExecTool } from "./tools/exec.js";
|
||||
|
|
@ -9,62 +8,16 @@ import { createWebFetchTool, createWebSearchTool } from "./tools/web/index.js";
|
|||
import { createMemoryTools } from "./tools/memory/index.js";
|
||||
import { filterTools } from "./tools/policy.js";
|
||||
|
||||
/**
|
||||
* Provider alias mapping for OAuth providers.
|
||||
* Maps friendly names to actual pi-ai provider names.
|
||||
*/
|
||||
const PROVIDER_ALIAS: Record<string, string> = {
|
||||
"claude-code": "anthropic", // Claude Code OAuth uses anthropic API
|
||||
};
|
||||
|
||||
/**
|
||||
* Default models for each provider.
|
||||
*/
|
||||
const DEFAULT_MODELS: Record<string, string> = {
|
||||
"anthropic": "claude-sonnet-4-20250514",
|
||||
"claude-code": "claude-sonnet-4-20250514",
|
||||
"openai": "gpt-4o",
|
||||
"openai-codex": "gpt-5.1",
|
||||
"kimi-coding": "kimi-k2-thinking",
|
||||
"google": "gemini-2.0-flash",
|
||||
"groq": "llama-3.3-70b-versatile",
|
||||
"mistral": "mistral-large-latest",
|
||||
};
|
||||
|
||||
export function resolveModel(options: AgentOptions) {
|
||||
if (options.provider && options.model) {
|
||||
// Map provider alias (e.g., claude-code -> anthropic)
|
||||
const actualProvider = PROVIDER_ALIAS[options.provider] ?? options.provider;
|
||||
|
||||
// Type assertion needed because provider/model come from dynamic user config
|
||||
return (getModel as (p: string, m: string) => ReturnType<typeof getModel>)(
|
||||
actualProvider,
|
||||
options.model,
|
||||
);
|
||||
}
|
||||
|
||||
// If only provider specified, use default model for that provider
|
||||
if (options.provider) {
|
||||
const actualProvider = PROVIDER_ALIAS[options.provider] ?? options.provider;
|
||||
const defaultModel = DEFAULT_MODELS[options.provider] ?? DEFAULT_MODELS[actualProvider];
|
||||
if (defaultModel) {
|
||||
return (getModel as (p: string, m: string) => ReturnType<typeof getModel>)(
|
||||
actualProvider,
|
||||
defaultModel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return getModel("kimi-coding", "kimi-k2-thinking");
|
||||
}
|
||||
// Re-export resolveModel from providers for backwards compatibility
|
||||
export { resolveModel } from "./providers/index.js";
|
||||
|
||||
/** Options for creating tools */
|
||||
export interface CreateToolsOptions {
|
||||
cwd: string;
|
||||
/** Profile ID for memory tools (optional) */
|
||||
profileId?: string;
|
||||
profileId?: string | undefined;
|
||||
/** Base directory for profiles (optional) */
|
||||
profileBaseDir?: string;
|
||||
profileBaseDir?: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue