perf: memo GridCard, fuse save fns, fix slash tab active color
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
getProviderInstance,
|
||||
getProviderConfigKeys,
|
||||
type ProviderType,
|
||||
} from '@/lib/ai/factory';
|
||||
import { applyByokToConfig } from '@/lib/byok';
|
||||
import { getAnyActiveByokForUser, hasAnyActiveByok, ByokUnavailableError } from '@/lib/byok';
|
||||
import {
|
||||
resolveAiRoute,
|
||||
type AiFeatureLane,
|
||||
@@ -17,78 +18,87 @@ export interface ProviderForUserResult {
|
||||
route: ResolvedAiRoute;
|
||||
}
|
||||
|
||||
/** Resolve the best AI provider for a user's lane — BYOK first, then admin config. */
|
||||
async function resolveProviderForLane(
|
||||
lane: AiFeatureLane,
|
||||
config: Record<string, string>,
|
||||
billingUserId?: string,
|
||||
): Promise<ProviderForUserResult> {
|
||||
const cfg = { ...config };
|
||||
const route = resolveAiRoute(lane, cfg);
|
||||
let usedByok = false;
|
||||
let byokModel: string | null = null;
|
||||
const adminRoute = resolveAiRoute(lane, cfg);
|
||||
|
||||
if (billingUserId) {
|
||||
const overlay = await applyByokToConfig(
|
||||
billingUserId,
|
||||
route.providerType,
|
||||
cfg,
|
||||
);
|
||||
Object.assign(cfg, overlay.config);
|
||||
usedByok = overlay.usedByok;
|
||||
byokModel = overlay.model;
|
||||
// Prefer admin's provider, fallback to any active BYOK key
|
||||
const byok = await getAnyActiveByokForUser(billingUserId, adminRoute.providerType, lane);
|
||||
|
||||
if (byok) {
|
||||
const { apiKeyConfigKey, baseUrlConfigKey } = getProviderConfigKeys(byok.provider);
|
||||
const byokCfg: Record<string, string> = { ...cfg };
|
||||
if (apiKeyConfigKey) byokCfg[apiKeyConfigKey] = byok.plaintext;
|
||||
if (baseUrlConfigKey && byok.baseUrl) byokCfg[baseUrlConfigKey] = byok.baseUrl;
|
||||
|
||||
const resolvedModel = (byok.model && byok.model.trim()) ? byok.model : adminRoute.modelName;
|
||||
console.log(`[byok] Using BYOK key: provider=${byok.provider} model=${resolvedModel} user=${billingUserId}`);
|
||||
|
||||
const provider = getProviderInstance(
|
||||
byok.provider as ProviderType,
|
||||
byokCfg,
|
||||
resolvedModel,
|
||||
adminRoute.embeddingModelName,
|
||||
adminRoute.ollamaBaseUrl,
|
||||
);
|
||||
|
||||
return {
|
||||
provider,
|
||||
usedByok: true,
|
||||
route: {
|
||||
...adminRoute,
|
||||
providerType: byok.provider as ResolvedAiRoute['providerType'],
|
||||
modelName: resolvedModel,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// No key resolved — if user HAS active BYOK rows, decryption failed → throw, no silent fallback
|
||||
const hasByok = await hasAnyActiveByok(billingUserId);
|
||||
if (hasByok) {
|
||||
throw new ByokUnavailableError();
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedModel = byokModel && byokModel.trim() !== '' ? byokModel : route.modelName;
|
||||
|
||||
// No BYOK configured → use admin config
|
||||
const provider = getProviderInstance(
|
||||
route.providerType as ProviderType,
|
||||
adminRoute.providerType as ProviderType,
|
||||
cfg,
|
||||
resolvedModel,
|
||||
route.embeddingModelName,
|
||||
route.ollamaBaseUrl,
|
||||
adminRoute.modelName,
|
||||
adminRoute.embeddingModelName,
|
||||
adminRoute.ollamaBaseUrl,
|
||||
);
|
||||
|
||||
const updatedRoute = { ...route, modelName: resolvedModel };
|
||||
|
||||
return { provider, usedByok, route: updatedRoute };
|
||||
return { provider, usedByok: false, route: adminRoute };
|
||||
}
|
||||
|
||||
async function getChatProviderForBillingUser(
|
||||
config: Record<string, string>,
|
||||
billingUserId?: string,
|
||||
): Promise<ProviderForUserResult> {
|
||||
return resolveProviderForLane('chat', config, billingUserId);
|
||||
}
|
||||
|
||||
async function getTagsProviderForBillingUser(
|
||||
config: Record<string, string>,
|
||||
billingUserId?: string,
|
||||
): Promise<ProviderForUserResult> {
|
||||
return resolveProviderForLane('tags', config, billingUserId);
|
||||
}
|
||||
|
||||
async function getEmbeddingsProviderForBillingUser(
|
||||
config: Record<string, string>,
|
||||
billingUserId?: string,
|
||||
): Promise<ProviderForUserResult> {
|
||||
return resolveProviderForLane('embedding', config, billingUserId);
|
||||
}
|
||||
|
||||
/** Run a lane with BYOK overlay; skips system fallback when user key is active. */
|
||||
/** Check if a lane will use BYOK for a given user. */
|
||||
export async function willUseByokForLane(
|
||||
lane: AiFeatureLane,
|
||||
config: Record<string, string>,
|
||||
billingUserId?: string,
|
||||
): Promise<{ providerType: string; usedByok: boolean }> {
|
||||
if (!billingUserId) {
|
||||
const route = resolveAiRoute(lane, config)
|
||||
return { providerType: route.providerType, usedByok: false }
|
||||
const route = resolveAiRoute(lane, config);
|
||||
return { providerType: route.providerType, usedByok: false };
|
||||
}
|
||||
const route = resolveAiRoute(lane, config)
|
||||
const overlay = await applyByokToConfig(billingUserId, route.providerType, config)
|
||||
return { providerType: route.providerType, usedByok: overlay.usedByok }
|
||||
const route = resolveAiRoute(lane, config);
|
||||
const byok = await getAnyActiveByokForUser(billingUserId, route.providerType);
|
||||
return { providerType: byok?.provider ?? route.providerType, usedByok: !!byok };
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an AI lane with BYOK priority.
|
||||
* - If user has active BYOK → uses it, no quota counted.
|
||||
* - If user has BYOK configured but it can't be loaded → throws ByokUnavailableError (no fallback).
|
||||
* - If user has no BYOK → uses admin config with system fallback.
|
||||
*/
|
||||
export async function runLaneWithBillingUser<T>(
|
||||
lane: AiFeatureLane,
|
||||
config: Record<string, string>,
|
||||
@@ -96,12 +106,8 @@ export async function runLaneWithBillingUser<T>(
|
||||
run: (provider: AIProvider) => Promise<T>,
|
||||
): Promise<{ result: T; usedByok: boolean }> {
|
||||
if (billingUserId) {
|
||||
const resolved =
|
||||
lane === 'chat'
|
||||
? await getChatProviderForBillingUser(config, billingUserId)
|
||||
: lane === 'tags'
|
||||
? await getTagsProviderForBillingUser(config, billingUserId)
|
||||
: await getEmbeddingsProviderForBillingUser(config, billingUserId);
|
||||
// May throw ByokUnavailableError — let it propagate, callers should handle it
|
||||
const resolved = await resolveProviderForLane(lane, config, billingUserId);
|
||||
|
||||
if (resolved.usedByok) {
|
||||
const result = await run(resolved.provider);
|
||||
@@ -109,6 +115,7 @@ export async function runLaneWithBillingUser<T>(
|
||||
}
|
||||
}
|
||||
|
||||
// No BYOK configured → use admin config with system fallback
|
||||
const result = await withAiProviderFallback(lane, config, run);
|
||||
return { result, usedByok: false };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user