Files
boss/src/lib/execution/backend-selector.ts
2026-04-03 01:36:29 +08:00

123 lines
3.5 KiB
TypeScript

import type { AiAccountStatus, AiProvider } from "@/lib/boss-data";
import {
ALIYUN_QWEN_BACKEND,
isReadyAliyunQwenBackend,
} from "@/lib/execution/backends/aliyun-qwen-backend";
import {
CLAW_BACKEND,
type ClawBackendSelectionState,
isClawRequestKindSupported,
} from "@/lib/execution/backends/claw-backend";
import {
MASTER_CODEX_NODE_BACKEND,
isReadyMasterCodexNodeBackend,
} from "@/lib/execution/backends/master-codex-node-backend";
import { OPENAI_BACKEND, isReadyOpenAiBackend } from "@/lib/execution/backends/openai-backend";
import type { ExecutionRequestKind } from "@/lib/execution/types";
export interface ExecutionBackendSelectionInput {
primary: {
provider: AiProvider;
status: AiAccountStatus;
};
backups: Array<{
provider: AiProvider;
status: AiAccountStatus;
}>;
requestKind?: ExecutionRequestKind;
requestedBackendId?: string;
claw?: ClawBackendSelectionState;
}
export type ExecutionBackendChoice =
| typeof CLAW_BACKEND
| typeof MASTER_CODEX_NODE_BACKEND
| typeof OPENAI_BACKEND
| typeof ALIYUN_QWEN_BACKEND;
function resolveBackendByProvider(provider: AiProvider): ExecutionBackendChoice {
switch (provider) {
case "master_codex_node":
return MASTER_CODEX_NODE_BACKEND;
case "openai_api":
return OPENAI_BACKEND;
case "aliyun_qwen_api":
return ALIYUN_QWEN_BACKEND;
default:
return MASTER_CODEX_NODE_BACKEND;
}
}
function isReadyBackend(choice: ExecutionBackendChoice, input: ExecutionBackendSelectionInput) {
if (choice.backendId === CLAW_BACKEND.backendId) {
const requestKind = input.requestKind;
if (!input.claw?.enabled || !requestKind) {
return false;
}
return isClawRequestKindSupported(requestKind);
}
const candidates = [
...(input.primary.provider === choice.provider ? [input.primary] : []),
...input.backups.filter((item) => item.provider === choice.provider),
];
return candidates.some((candidate) => {
switch (choice.backendId) {
case "aliyun-qwen":
return isReadyAliyunQwenBackend(candidate);
case "openai-api":
return isReadyOpenAiBackend(candidate);
case "master-codex-node":
return isReadyMasterCodexNodeBackend(candidate);
default:
return false;
}
});
}
export async function selectExecutionBackend(
input: ExecutionBackendSelectionInput,
): Promise<ExecutionBackendChoice> {
return listExecutionBackendChoices(input)[0] ?? resolveBackendByProvider(input.primary.provider);
}
export function listExecutionBackendChoices(
input: ExecutionBackendSelectionInput,
): ExecutionBackendChoice[] {
const primaryBackend = resolveBackendByProvider(input.primary.provider);
const ordered: ExecutionBackendChoice[] = [];
const seen = new Set<string>();
const pushBackend = (backend: ExecutionBackendChoice) => {
if (seen.has(backend.backendId)) {
return;
}
ordered.push(backend);
seen.add(backend.backendId);
};
if (
input.requestedBackendId === CLAW_BACKEND.backendId &&
isReadyBackend(CLAW_BACKEND, input)
) {
pushBackend(CLAW_BACKEND);
}
if (input.primary.status === "ready") {
pushBackend(primaryBackend);
}
const orderedBackends: ExecutionBackendChoice[] = [ALIYUN_QWEN_BACKEND, OPENAI_BACKEND, MASTER_CODEX_NODE_BACKEND];
for (const backend of orderedBackends) {
if (isReadyBackend(backend, input)) {
pushBackend(backend);
}
}
pushBackend(primaryBackend);
return ordered;
}
export const selectExecutionBackendForTesting = selectExecutionBackend;