Skip to content

Commit 7c40b4c

Browse files
committed
Merge PR xiaoY233#84 from upstream
2 parents fe2eb8c + bce2bfa commit 7c40b4c

5 files changed

Lines changed: 71 additions & 6 deletions

File tree

src/main/proxy/loadbalancer.ts

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -233,17 +233,62 @@ export class LoadBalancer {
233233

234234
/**
235235
* Round Robin strategy
236+
* Uses two-level round-robin: provider level + account level.
237+
* At account level, uses weighted random when weights are configured.
236238
*/
237239
private selectRoundRobin(candidates: AccountSelection[]): AccountSelection {
238-
const providerIds = [...new Set(candidates.map(c => c.provider.id))]
239-
const key = providerIds.join(',')
240+
// Group candidates by provider (preserve insertion order)
241+
const byProvider = new Map<string, AccountSelection[]>()
242+
const providerIds: string[] = []
243+
for (const c of candidates) {
244+
if (!byProvider.has(c.provider.id)) {
245+
byProvider.set(c.provider.id, [])
246+
providerIds.push(c.provider.id)
247+
}
248+
byProvider.get(c.provider.id)!.push(c)
249+
}
250+
251+
// Provider-level round-robin
252+
const providerKey = providerIds.join(',')
253+
const providerIdx = this.roundRobinIndex.get(providerKey) || 0
254+
const chosenProviderId = providerIds[providerIdx % providerIds.length]
255+
this.roundRobinIndex.set(providerKey, providerIdx + 1)
256+
257+
// Account-level: weighted random
258+
const providerAccounts = byProvider.get(chosenProviderId)!
259+
return this.selectWeightedAccount(providerAccounts, chosenProviderId)
260+
}
261+
262+
/**
263+
* Weighted random account selection.
264+
* Each account is selected with probability proportional to its configured weight.
265+
* Stateless — immune to dynamic pool changes (accounts entering/leaving).
266+
*/
267+
private selectWeightedAccount(
268+
accounts: AccountSelection[],
269+
providerId: string
270+
): AccountSelection {
271+
if (accounts.length === 1) {
272+
return accounts[0]
273+
}
240274

241-
const currentIndex = this.roundRobinIndex.get(key) || 0
242-
const selected = candidates[currentIndex % candidates.length]
275+
const config = storeManager.getConfig()
276+
const weights = config.accountWeights || {}
243277

244-
this.roundRobinIndex.set(key, (currentIndex + 1) % candidates.length)
278+
let totalWeight = 0
279+
for (const a of accounts) {
280+
totalWeight += Math.max(weights[a.account.id] ?? 100, 5)
281+
}
282+
283+
let random = Math.random() * totalWeight
284+
for (const a of accounts) {
285+
random -= Math.max(weights[a.account.id] ?? 100, 5)
286+
if (random <= 0) {
287+
return a
288+
}
289+
}
245290

246-
return selected
291+
return accounts[accounts.length - 1]
247292
}
248293

249294
/**

src/main/store/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ export interface AppConfig {
179179
proxyHost: string
180180
/** Load balance strategy */
181181
loadBalanceStrategy: LoadBalanceStrategy
182+
/** Account weights for weighted random selection (accountId → weight, 0-100) */
183+
accountWeights: Record<string, number>
182184
/** Model mapping configuration */
183185
modelMappings: Record<string, ModelMapping>
184186
/** UI theme */
@@ -733,6 +735,7 @@ export const DEFAULT_CONFIG: AppConfig = {
733735
proxyPort: 8080,
734736
proxyHost: '127.0.0.1',
735737
loadBalanceStrategy: 'round-robin',
738+
accountWeights: {},
736739
modelMappings: {},
737740
theme: 'system',
738741
autoStart: false,

src/renderer/src/components/proxy/LoadBalanceConfig.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,15 @@ export function LoadBalanceConfig({ onConfigChange }: LoadBalanceConfigProps) {
9292
setLoadBalanceStrategy(selectedStrategy)
9393
setAccountWeights(weights)
9494

95+
// Convert AccountWeight[] to Record<string, number> for backend storage
96+
const weightsRecord: Record<string, number> = {}
97+
for (const w of weights) {
98+
weightsRecord[w.accountId] = w.weight
99+
}
100+
95101
const success = await saveAppConfig({
96102
loadBalanceStrategy: selectedStrategy,
103+
accountWeights: weightsRecord,
97104
})
98105

99106
if (success) {

src/renderer/src/stores/proxyStore.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,17 @@ export const useProxyStore = create<ProxyState>((set, get) => ({
167167
set({ isLoading: true })
168168
const config = await window.electronAPI.store.get<AppConfig>('config')
169169
if (config) {
170+
// Restore account weights from backend
171+
const weights = config.accountWeights || {}
172+
const accountWeights = Object.entries(weights).map(([accountId, weight]) => ({
173+
accountId,
174+
weight,
175+
}))
176+
170177
set({
171178
appConfig: config,
172179
loadBalanceStrategy: config.loadBalanceStrategy,
180+
accountWeights,
173181
modelMappings: Object.values(config.modelMappings || {}),
174182
proxyConfig: {
175183
...DEFAULT_PROXY_CONFIG,

src/shared/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export interface AppConfig {
8787
proxyPort: number
8888
proxyHost: string
8989
loadBalanceStrategy: LoadBalanceStrategy
90+
/** Account weights for weighted random selection (accountId → weight, 0-100) */
91+
accountWeights: Record<string, number>
9092
modelMappings: Record<string, ModelMapping>
9193
theme: Theme
9294
autoStart: boolean

0 commit comments

Comments
 (0)