mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-19 03:51:31 +00:00
refactor(providers): move auth status checks into provider runtimes
Move provider authentication status logic out of the CLI auth route so auth checks live with the provider implementations that understand each provider's install and credential model. Add provider-specific auth runtime classes for Claude, Codex, Cursor, and Gemini, and expose them through the shared provider contract as `provider.auth`. Add a provider auth service that resolves providers through the registry and delegates status checks via `auth.getStatus()`. Keep the existing `/api/cli/<provider>/status` endpoints, but make them thin route adapters over the new provider auth service. This removes duplicated route-local credential parsing and makes auth status a first-class provider capability beside MCP and message handling.
This commit is contained in:
126
server/modules/providers/list/claude/claude-auth.provider.ts
Normal file
126
server/modules/providers/list/claude/claude-auth.provider.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import spawn from 'cross-spawn';
|
||||
|
||||
import type { IProviderAuthRuntime } from '@/shared/interfaces.js';
|
||||
import type { ProviderAuthStatus } from '@/shared/types.js';
|
||||
import { readObjectRecord, readOptionalString } from '@/shared/utils.js';
|
||||
|
||||
type ClaudeCredentialsStatus = {
|
||||
authenticated: boolean;
|
||||
email: string | null;
|
||||
method: string | null;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export class ClaudeAuthProvider implements IProviderAuthRuntime {
|
||||
/**
|
||||
* Checks whether the Claude Code CLI is available on this host.
|
||||
*/
|
||||
private checkInstalled(): boolean {
|
||||
const cliPath = process.env.CLAUDE_CLI_PATH || 'claude';
|
||||
try {
|
||||
spawn.sync(cliPath, ['--version'], { stdio: 'ignore', timeout: 5000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Claude installation and credential status using Claude Code's auth priority.
|
||||
*/
|
||||
async getStatus(): Promise<ProviderAuthStatus> {
|
||||
console.log("Checking Claude authentication status...")
|
||||
const installed = this.checkInstalled();
|
||||
|
||||
if (!installed) {
|
||||
return {
|
||||
installed,
|
||||
provider: 'claude',
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: null,
|
||||
error: 'Claude Code CLI is not installed',
|
||||
};
|
||||
}
|
||||
|
||||
const credentials = await this.checkCredentials();
|
||||
|
||||
console.log("Credientials status for Claude:", credentials)
|
||||
return {
|
||||
installed,
|
||||
provider: 'claude',
|
||||
authenticated: credentials.authenticated,
|
||||
email: credentials.authenticated ? credentials.email || 'Authenticated' : credentials.email,
|
||||
method: credentials.method,
|
||||
error: credentials.authenticated ? undefined : credentials.error || 'Not authenticated',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads Claude settings env values that the CLI can use even when the server process env is empty.
|
||||
*/
|
||||
private async loadSettingsEnv(): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
||||
const content = await readFile(settingsPath, 'utf8');
|
||||
const settings = readObjectRecord(JSON.parse(content));
|
||||
console.log("Settings env for Claude:", settings)
|
||||
return readObjectRecord(settings?.env) ?? {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks Claude credentials in the same priority order used by Claude Code.
|
||||
*/
|
||||
private async checkCredentials(): Promise<ClaudeCredentialsStatus> {
|
||||
if (process.env.ANTHROPIC_API_KEY?.trim()) {
|
||||
return { authenticated: true, email: 'API Key Auth', method: 'api_key' };
|
||||
}
|
||||
|
||||
const settingsEnv = await this.loadSettingsEnv();
|
||||
if (readOptionalString(settingsEnv.ANTHROPIC_API_KEY)) {
|
||||
return { authenticated: true, email: 'API Key Auth', method: 'api_key' };
|
||||
}
|
||||
|
||||
if (readOptionalString(settingsEnv.ANTHROPIC_AUTH_TOKEN)) {
|
||||
return { authenticated: true, email: 'Configured via settings.json', method: 'api_key' };
|
||||
}
|
||||
|
||||
try {
|
||||
const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
|
||||
const content = await readFile(credPath, 'utf8');
|
||||
const creds = readObjectRecord(JSON.parse(content)) ?? {};
|
||||
const oauth = readObjectRecord(creds.claudeAiOauth);
|
||||
const accessToken = readOptionalString(oauth?.accessToken);
|
||||
|
||||
if (accessToken) {
|
||||
const expiresAt = typeof oauth?.expiresAt === 'number' ? oauth.expiresAt : undefined;
|
||||
const email = readOptionalString(creds.email) ?? readOptionalString(creds.user) ?? null;
|
||||
if (!expiresAt || Date.now() < expiresAt) {
|
||||
return {
|
||||
authenticated: true,
|
||||
email,
|
||||
method: 'credentials_file',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authenticated: false,
|
||||
email,
|
||||
method: 'credentials_file',
|
||||
error: 'OAuth token has expired. Please re-authenticate with claude login',
|
||||
};
|
||||
}
|
||||
|
||||
return { authenticated: false, email: null, method: null };
|
||||
} catch {
|
||||
return { authenticated: false, email: null, method: null };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { getSessionMessages } from '@/projects.js';
|
||||
import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
|
||||
import { ClaudeAuthProvider } from '@/modules/providers/list/claude/claude-auth.provider.js';
|
||||
import { ClaudeMcpProvider } from '@/modules/providers/list/claude/claude-mcp.provider.js';
|
||||
import type { IProviderAuthRuntime } from '@/shared/interfaces.js';
|
||||
import type { FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
|
||||
import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
|
||||
|
||||
@@ -55,6 +57,7 @@ function readRawProviderMessage(raw: unknown): RawProviderMessage | null {
|
||||
|
||||
export class ClaudeProvider extends AbstractProvider {
|
||||
readonly mcp = new ClaudeMcpProvider();
|
||||
readonly auth: IProviderAuthRuntime = new ClaudeAuthProvider();
|
||||
|
||||
constructor() {
|
||||
super('claude');
|
||||
|
||||
100
server/modules/providers/list/codex/codex-auth.provider.ts
Normal file
100
server/modules/providers/list/codex/codex-auth.provider.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import spawn from 'cross-spawn';
|
||||
|
||||
import type { IProviderAuthRuntime } from '@/shared/interfaces.js';
|
||||
import type { ProviderAuthStatus } from '@/shared/types.js';
|
||||
import { readObjectRecord, readOptionalString } from '@/shared/utils.js';
|
||||
|
||||
type CodexCredentialsStatus = {
|
||||
authenticated: boolean;
|
||||
email: string | null;
|
||||
method: string | null;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export class CodexAuthProvider implements IProviderAuthRuntime {
|
||||
/**
|
||||
* Checks whether Codex is available to the server runtime.
|
||||
*/
|
||||
private checkInstalled(): boolean {
|
||||
try {
|
||||
spawn.sync('codex', ['--version'], { stdio: 'ignore', timeout: 5000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Codex SDK availability and credential status.
|
||||
*/
|
||||
async getStatus(): Promise<ProviderAuthStatus> {
|
||||
const installed = this.checkInstalled();
|
||||
const credentials = await this.checkCredentials();
|
||||
|
||||
return {
|
||||
installed,
|
||||
provider: 'codex',
|
||||
authenticated: credentials.authenticated,
|
||||
email: credentials.email,
|
||||
method: credentials.method,
|
||||
error: credentials.authenticated ? undefined : credentials.error || 'Not authenticated',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads Codex auth.json and checks OAuth tokens or an API key fallback.
|
||||
*/
|
||||
private async checkCredentials(): Promise<CodexCredentialsStatus> {
|
||||
try {
|
||||
const authPath = path.join(os.homedir(), '.codex', 'auth.json');
|
||||
const content = await readFile(authPath, 'utf8');
|
||||
const auth = readObjectRecord(JSON.parse(content)) ?? {};
|
||||
const tokens = readObjectRecord(auth.tokens) ?? {};
|
||||
const idToken = readOptionalString(tokens.id_token);
|
||||
const accessToken = readOptionalString(tokens.access_token);
|
||||
|
||||
if (idToken || accessToken) {
|
||||
return {
|
||||
authenticated: true,
|
||||
email: idToken ? this.readEmailFromIdToken(idToken) : 'Authenticated',
|
||||
method: 'credentials_file',
|
||||
};
|
||||
}
|
||||
|
||||
if (readOptionalString(auth.OPENAI_API_KEY)) {
|
||||
return { authenticated: true, email: 'API Key Auth', method: 'api_key' };
|
||||
}
|
||||
|
||||
return { authenticated: false, email: null, method: null, error: 'No valid tokens found' };
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
return {
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: null,
|
||||
error: code === 'ENOENT' ? 'Codex not configured' : error instanceof Error ? error.message : 'Failed to read Codex auth',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the user email from a Codex id_token when a readable JWT payload exists.
|
||||
*/
|
||||
private readEmailFromIdToken(idToken: string): string {
|
||||
try {
|
||||
const parts = idToken.split('.');
|
||||
if (parts.length >= 2) {
|
||||
const payload = readObjectRecord(JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')));
|
||||
return readOptionalString(payload?.email) ?? readOptionalString(payload?.user) ?? 'Authenticated';
|
||||
}
|
||||
} catch {
|
||||
// Fall back to a generic authenticated marker if the token payload is not readable.
|
||||
}
|
||||
|
||||
return 'Authenticated';
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { getCodexSessionMessages } from '@/projects.js';
|
||||
import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
|
||||
import { CodexAuthProvider } from '@/modules/providers/list/codex/codex-auth.provider.js';
|
||||
import { CodexMcpProvider } from '@/modules/providers/list/codex/codex-mcp.provider.js';
|
||||
import type { IProviderAuthRuntime } from '@/shared/interfaces.js';
|
||||
import type { FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
|
||||
import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
|
||||
|
||||
@@ -29,6 +31,7 @@ function readRawProviderMessage(raw: unknown): RawProviderMessage | null {
|
||||
|
||||
export class CodexProvider extends AbstractProvider {
|
||||
readonly mcp = new CodexMcpProvider();
|
||||
readonly auth: IProviderAuthRuntime = new CodexAuthProvider();
|
||||
|
||||
constructor() {
|
||||
super('codex');
|
||||
|
||||
143
server/modules/providers/list/cursor/cursor-auth.provider.ts
Normal file
143
server/modules/providers/list/cursor/cursor-auth.provider.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import spawn from 'cross-spawn';
|
||||
|
||||
import type { IProviderAuthRuntime } from '@/shared/interfaces.js';
|
||||
import type { ProviderAuthStatus } from '@/shared/types.js';
|
||||
|
||||
type CursorLoginStatus = {
|
||||
authenticated: boolean;
|
||||
email: string | null;
|
||||
method: string | null;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export class CursorAuthProvider implements IProviderAuthRuntime {
|
||||
/**
|
||||
* Checks whether the cursor-agent CLI is available on this host.
|
||||
*/
|
||||
private checkInstalled(): boolean {
|
||||
try {
|
||||
spawn.sync('cursor-agent', ['--version'], { stdio: 'ignore', timeout: 5000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Cursor CLI installation and login status.
|
||||
*/
|
||||
async getStatus(): Promise<ProviderAuthStatus> {
|
||||
const installed = this.checkInstalled();
|
||||
|
||||
if (!installed) {
|
||||
return {
|
||||
installed,
|
||||
provider: 'cursor',
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: null,
|
||||
error: 'Cursor CLI is not installed',
|
||||
};
|
||||
}
|
||||
|
||||
const login = await this.checkCursorLogin();
|
||||
|
||||
return {
|
||||
installed,
|
||||
provider: 'cursor',
|
||||
authenticated: login.authenticated,
|
||||
email: login.email,
|
||||
method: login.method,
|
||||
error: login.authenticated ? undefined : login.error || 'Not logged in',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs cursor-agent status and parses the login marker from stdout.
|
||||
*/
|
||||
private checkCursorLogin(): Promise<CursorLoginStatus> {
|
||||
return new Promise((resolve) => {
|
||||
let processCompleted = false;
|
||||
let childProcess: ReturnType<typeof spawn> | undefined;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
if (!processCompleted) {
|
||||
processCompleted = true;
|
||||
childProcess?.kill();
|
||||
resolve({
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: null,
|
||||
error: 'Command timeout',
|
||||
});
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
try {
|
||||
childProcess = spawn('cursor-agent', ['status']);
|
||||
} catch {
|
||||
clearTimeout(timeout);
|
||||
processCompleted = true;
|
||||
resolve({
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: null,
|
||||
error: 'Cursor CLI not found or not installed',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
childProcess.stdout?.on('data', (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
childProcess.stderr?.on('data', (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
childProcess.on('close', (code) => {
|
||||
if (processCompleted) {
|
||||
return;
|
||||
}
|
||||
processCompleted = true;
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (code === 0) {
|
||||
const emailMatch = stdout.match(/Logged in as ([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i);
|
||||
if (emailMatch?.[1]) {
|
||||
resolve({ authenticated: true, email: emailMatch[1], method: 'cli' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (stdout.includes('Logged in')) {
|
||||
resolve({ authenticated: true, email: 'Logged in', method: 'cli' });
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({ authenticated: false, email: null, method: null, error: 'Not logged in' });
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({ authenticated: false, email: null, method: null, error: stderr || 'Not logged in' });
|
||||
});
|
||||
|
||||
childProcess.on('error', () => {
|
||||
if (processCompleted) {
|
||||
return;
|
||||
}
|
||||
processCompleted = true;
|
||||
clearTimeout(timeout);
|
||||
|
||||
resolve({
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: null,
|
||||
error: 'Cursor CLI not found or not installed',
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
|
||||
import { CursorAuthProvider } from '@/modules/providers/list/cursor/cursor-auth.provider.js';
|
||||
import { CursorMcpProvider } from '@/modules/providers/list/cursor/cursor-mcp.provider.js';
|
||||
import type { IProviderAuthRuntime } from '@/shared/interfaces.js';
|
||||
import type { FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
|
||||
import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
|
||||
|
||||
@@ -34,6 +36,7 @@ function readRawProviderMessage(raw: unknown): RawProviderMessage | null {
|
||||
|
||||
export class CursorProvider extends AbstractProvider {
|
||||
readonly mcp = new CursorMcpProvider();
|
||||
readonly auth: IProviderAuthRuntime = new CursorAuthProvider();
|
||||
|
||||
constructor() {
|
||||
super('cursor');
|
||||
|
||||
151
server/modules/providers/list/gemini/gemini-auth.provider.ts
Normal file
151
server/modules/providers/list/gemini/gemini-auth.provider.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import spawn from 'cross-spawn';
|
||||
|
||||
import type { IProviderAuthRuntime } from '@/shared/interfaces.js';
|
||||
import type { ProviderAuthStatus } from '@/shared/types.js';
|
||||
import { readObjectRecord, readOptionalString } from '@/shared/utils.js';
|
||||
|
||||
type GeminiCredentialsStatus = {
|
||||
authenticated: boolean;
|
||||
email: string | null;
|
||||
method: string | null;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export class GeminiAuthProvider implements IProviderAuthRuntime {
|
||||
/**
|
||||
* Checks whether the Gemini CLI is available on this host.
|
||||
*/
|
||||
private checkInstalled(): boolean {
|
||||
const cliPath = process.env.GEMINI_PATH || 'gemini';
|
||||
try {
|
||||
spawn.sync(cliPath, ['--version'], { stdio: 'ignore', timeout: 5000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Gemini CLI installation and credential status.
|
||||
*/
|
||||
async getStatus(): Promise<ProviderAuthStatus> {
|
||||
const installed = this.checkInstalled();
|
||||
|
||||
if (!installed) {
|
||||
return {
|
||||
installed,
|
||||
provider: 'gemini',
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: null,
|
||||
error: 'Gemini CLI is not installed',
|
||||
};
|
||||
}
|
||||
|
||||
const credentials = await this.checkCredentials();
|
||||
|
||||
return {
|
||||
installed,
|
||||
provider: 'gemini',
|
||||
authenticated: credentials.authenticated,
|
||||
email: credentials.email,
|
||||
method: credentials.method,
|
||||
error: credentials.authenticated ? undefined : credentials.error || 'Not authenticated',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks Gemini credentials from API key env vars or local OAuth credential files.
|
||||
*/
|
||||
private async checkCredentials(): Promise<GeminiCredentialsStatus> {
|
||||
if (process.env.GEMINI_API_KEY?.trim()) {
|
||||
return { authenticated: true, email: 'API Key Auth', method: 'api_key' };
|
||||
}
|
||||
|
||||
try {
|
||||
const credsPath = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
|
||||
const content = await readFile(credsPath, 'utf8');
|
||||
const creds = readObjectRecord(JSON.parse(content)) ?? {};
|
||||
const accessToken = readOptionalString(creds.access_token);
|
||||
|
||||
if (!accessToken) {
|
||||
return {
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: null,
|
||||
error: 'No valid tokens found in oauth_creds',
|
||||
};
|
||||
}
|
||||
|
||||
const refreshToken = readOptionalString(creds.refresh_token);
|
||||
const tokenInfo = await this.getTokenInfoEmail(accessToken);
|
||||
if (tokenInfo.valid) {
|
||||
return {
|
||||
authenticated: true,
|
||||
email: tokenInfo.email || 'OAuth Session',
|
||||
method: 'credentials_file',
|
||||
};
|
||||
}
|
||||
|
||||
if (!refreshToken) {
|
||||
return {
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: 'credentials_file',
|
||||
error: 'Access token invalid and no refresh token found',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
email: await this.getActiveAccountEmail() || 'OAuth Session',
|
||||
method: 'credentials_file',
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: null,
|
||||
error: 'Gemini CLI not configured',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a Gemini OAuth access token and returns an email when Google reports one.
|
||||
*/
|
||||
private async getTokenInfoEmail(accessToken: string): Promise<{ valid: boolean; email: string | null }> {
|
||||
try {
|
||||
const tokenRes = await fetch(`https://oauth2.googleapis.com/tokeninfo?access_token=${accessToken}`);
|
||||
if (!tokenRes.ok) {
|
||||
return { valid: false, email: null };
|
||||
}
|
||||
|
||||
const tokenInfo = readObjectRecord(await tokenRes.json());
|
||||
return {
|
||||
valid: true,
|
||||
email: readOptionalString(tokenInfo?.email) ?? null,
|
||||
};
|
||||
} catch {
|
||||
return { valid: false, email: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads Gemini's active local Google account as an offline fallback for display.
|
||||
*/
|
||||
private async getActiveAccountEmail(): Promise<string | null> {
|
||||
try {
|
||||
const accPath = path.join(os.homedir(), '.gemini', 'google_accounts.json');
|
||||
const accContent = await readFile(accPath, 'utf8');
|
||||
const accounts = readObjectRecord(JSON.parse(accContent));
|
||||
return readOptionalString(accounts?.active) ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import sessionManager from '@/sessionManager.js';
|
||||
import { getGeminiCliSessionMessages } from '@/projects.js';
|
||||
import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
|
||||
import { GeminiAuthProvider } from '@/modules/providers/list/gemini/gemini-auth.provider.js';
|
||||
import { GeminiMcpProvider } from '@/modules/providers/list/gemini/gemini-mcp.provider.js';
|
||||
import type { IProviderAuthRuntime } from '@/shared/interfaces.js';
|
||||
import type { FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
|
||||
import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
|
||||
|
||||
@@ -15,6 +17,7 @@ function readRawProviderMessage(raw: unknown): RawProviderMessage | null {
|
||||
|
||||
export class GeminiProvider extends AbstractProvider {
|
||||
readonly mcp = new GeminiMcpProvider();
|
||||
readonly auth: IProviderAuthRuntime = new GeminiAuthProvider();
|
||||
|
||||
constructor() {
|
||||
super('gemini');
|
||||
|
||||
12
server/modules/providers/services/provider-auth.service.ts
Normal file
12
server/modules/providers/services/provider-auth.service.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { providerRegistry } from '@/modules/providers/provider.registry.js';
|
||||
import type { ProviderAuthStatus } from '@/shared/types.js';
|
||||
|
||||
export const providerAuthService = {
|
||||
/**
|
||||
* Resolves a provider and returns its installation/authentication status.
|
||||
*/
|
||||
async getProviderAuthStatus(providerName: string): Promise<ProviderAuthStatus> {
|
||||
const provider = providerRegistry.resolveProvider(providerName);
|
||||
return provider.auth.getStatus();
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IProvider, IProviderMcpRuntime } from '@/shared/interfaces.js';
|
||||
import type { IProvider, IProviderAuthRuntime, IProviderMcpRuntime } from '@/shared/interfaces.js';
|
||||
import type {
|
||||
FetchHistoryOptions,
|
||||
FetchHistoryResult,
|
||||
@@ -9,12 +9,14 @@ import type {
|
||||
/**
|
||||
* Shared provider base.
|
||||
*
|
||||
* Concrete providers must implement message normalization and history loading
|
||||
* because both behaviors depend on each provider's native SDK/CLI event format.
|
||||
* Concrete providers must expose auth/MCP runtimes and implement message
|
||||
* normalization/history loading because those behaviors depend on native
|
||||
* SDK/CLI formats.
|
||||
*/
|
||||
export abstract class AbstractProvider implements IProvider {
|
||||
readonly id: LLMProvider;
|
||||
abstract readonly mcp: IProviderMcpRuntime;
|
||||
abstract readonly auth: IProviderAuthRuntime;
|
||||
|
||||
protected constructor(id: LLMProvider) {
|
||||
this.id = id;
|
||||
|
||||
Reference in New Issue
Block a user