feat(saas): add model groups for cross-provider failover
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Model Groups provide logical model names that map to multiple physical models across providers, with automatic failover when one provider's key pool is exhausted. Backend: - New model_groups + model_group_members tables with FK constraints - Full CRUD API (7 endpoints) with admin-only write permissions - Cache layer: DashMap-backed CachedModelGroup with load_from_db - Relay integration: ModelResolution enum for Direct/Group routing - Cross-provider failover: sort_candidates_by_quota + OnceLock cache - Relay failure path: record failure usage + relay_dequeue (fixes queue counter leak that caused connection pool exhaustion) - add_group_member: validate model_id exists before insert Frontend: - saas-relay-client: accept getModel() callback for dynamic model selection - connectionStore: prefer conversationStore.currentModel over first available Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -44,7 +44,7 @@ interface CloneInfo {
|
||||
*/
|
||||
export function createSaaSRelayGatewayClient(
|
||||
_saasUrl: string,
|
||||
relayModel: string,
|
||||
getModel: () => string,
|
||||
): GatewayClient {
|
||||
// saasUrl preserved for future direct API routing (currently routed through saasClient singleton)
|
||||
void _saasUrl;
|
||||
@@ -69,7 +69,7 @@ export function createSaaSRelayGatewayClient(
|
||||
emoji: t.emoji,
|
||||
personality: t.category,
|
||||
scenarios: [],
|
||||
model: relayModel,
|
||||
model: getModel(),
|
||||
status: 'active',
|
||||
templateId: t.id,
|
||||
};
|
||||
@@ -114,7 +114,7 @@ export function createSaaSRelayGatewayClient(
|
||||
|
||||
try {
|
||||
const body: Record<string, unknown> = {
|
||||
model: relayModel,
|
||||
model: getModel(),
|
||||
messages: [{ role: 'user', content: message }],
|
||||
stream: true,
|
||||
};
|
||||
@@ -229,7 +229,7 @@ export function createSaaSRelayGatewayClient(
|
||||
role: opts.role as string,
|
||||
nickname: opts.nickname as string,
|
||||
emoji: opts.emoji as string,
|
||||
model: relayModel,
|
||||
model: getModel(),
|
||||
status: 'active',
|
||||
};
|
||||
agents.set(id, clone);
|
||||
|
||||
@@ -492,12 +492,22 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
|
||||
const kernelClient = getKernelClient();
|
||||
|
||||
// Use first available model (TODO: let user choose preferred model)
|
||||
const relayModel = relayModels[0];
|
||||
// Use first available model as fallback; prefer conversationStore.currentModel if set
|
||||
const fallbackModel = relayModels[0];
|
||||
|
||||
// 优先使用 conversationStore 的 currentModel,如果设置了的话
|
||||
let preferredModel: string | undefined;
|
||||
try {
|
||||
const { useConversationStore } = require('./chat/conversationStore');
|
||||
preferredModel = useConversationStore.getState().currentModel;
|
||||
} catch {
|
||||
// conversationStore 可能尚未初始化
|
||||
}
|
||||
const modelToUse = preferredModel || fallbackModel.id;
|
||||
|
||||
kernelClient.setConfig({
|
||||
provider: 'custom',
|
||||
model: relayModel.id,
|
||||
model: modelToUse,
|
||||
apiKey: session.token,
|
||||
baseUrl: `${session.saasUrl}/api/v1/relay`,
|
||||
apiProtocol: 'openai',
|
||||
@@ -522,14 +532,23 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
|
||||
set({ gatewayVersion: 'saas-relay', connectionState: 'connected' });
|
||||
log.debug('Connected via SaaS relay (kernel backend):', {
|
||||
model: relayModel.id,
|
||||
model: modelToUse,
|
||||
baseUrl: `${session.saasUrl}/api/v1/relay`,
|
||||
});
|
||||
} else {
|
||||
// Non-Tauri (browser) — use SaaS relay gateway client for agent listing + chat
|
||||
const { createSaaSRelayGatewayClient } = await import('../lib/saas-relay-client');
|
||||
const relayModelId = relayModels[0].id;
|
||||
const relayClient = createSaaSRelayGatewayClient(session.saasUrl, relayModelId);
|
||||
const fallbackModelId = relayModels[0].id;
|
||||
const relayClient = createSaaSRelayGatewayClient(session.saasUrl, () => {
|
||||
// 每次调用时读取 conversationStore 的 currentModel,fallback 到第一个可用模型
|
||||
try {
|
||||
const { useConversationStore } = require('./chat/conversationStore');
|
||||
const current = useConversationStore.getState().currentModel;
|
||||
return current || fallbackModelId;
|
||||
} catch {
|
||||
return fallbackModelId;
|
||||
}
|
||||
});
|
||||
|
||||
set({
|
||||
connectionState: 'connected',
|
||||
@@ -540,7 +559,7 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
|
||||
const { initializeStores } = await import('./index');
|
||||
initializeStores();
|
||||
|
||||
log.debug('Connected to SaaS relay (browser mode)', { relayModel: relayModelId });
|
||||
log.debug('Connected to SaaS relay (browser mode)', { relayModel: fallbackModelId });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user