refactor: add execution backend selection
This commit is contained in:
97
src/lib/execution/backend-selector.ts
Normal file
97
src/lib/execution/backend-selector.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { AiAccountStatus, AiProvider } from "@/lib/boss-data";
|
||||
import {
|
||||
ALIYUN_QWEN_BACKEND,
|
||||
isReadyAliyunQwenBackend,
|
||||
} from "@/lib/execution/backends/aliyun-qwen-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";
|
||||
|
||||
export interface ExecutionBackendSelectionInput {
|
||||
primary: {
|
||||
provider: AiProvider;
|
||||
status: AiAccountStatus;
|
||||
};
|
||||
backups: Array<{
|
||||
provider: AiProvider;
|
||||
status: AiAccountStatus;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type ExecutionBackendChoice =
|
||||
| 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) {
|
||||
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.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;
|
||||
22
src/lib/execution/backends/aliyun-qwen-backend.ts
Normal file
22
src/lib/execution/backends/aliyun-qwen-backend.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { AiAccountStatus, AiProvider } from "@/lib/boss-data";
|
||||
|
||||
export interface ExecutionBackendDescriptor {
|
||||
backendId: string;
|
||||
provider: AiProvider;
|
||||
label: string;
|
||||
mode: "local" | "api";
|
||||
}
|
||||
|
||||
export const ALIYUN_QWEN_BACKEND = {
|
||||
backendId: "aliyun-qwen",
|
||||
provider: "aliyun_qwen_api",
|
||||
label: "阿里百炼 Qwen",
|
||||
mode: "api",
|
||||
} as const satisfies ExecutionBackendDescriptor;
|
||||
|
||||
export function isReadyAliyunQwenBackend(input: {
|
||||
provider: AiProvider;
|
||||
status: AiAccountStatus;
|
||||
}) {
|
||||
return input.provider === ALIYUN_QWEN_BACKEND.provider && input.status === "ready";
|
||||
}
|
||||
22
src/lib/execution/backends/master-codex-node-backend.ts
Normal file
22
src/lib/execution/backends/master-codex-node-backend.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { AiAccountStatus, AiProvider } from "@/lib/boss-data";
|
||||
|
||||
export interface ExecutionBackendDescriptor {
|
||||
backendId: string;
|
||||
provider: AiProvider;
|
||||
label: string;
|
||||
mode: "local" | "api";
|
||||
}
|
||||
|
||||
export const MASTER_CODEX_NODE_BACKEND = {
|
||||
backendId: "master-codex-node",
|
||||
provider: "master_codex_node",
|
||||
label: "Master Codex Node",
|
||||
mode: "local",
|
||||
} as const satisfies ExecutionBackendDescriptor;
|
||||
|
||||
export function isReadyMasterCodexNodeBackend(input: {
|
||||
provider: AiProvider;
|
||||
status: AiAccountStatus;
|
||||
}) {
|
||||
return input.provider === MASTER_CODEX_NODE_BACKEND.provider && input.status === "ready";
|
||||
}
|
||||
19
src/lib/execution/backends/openai-backend.ts
Normal file
19
src/lib/execution/backends/openai-backend.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { AiAccountStatus, AiProvider } from "@/lib/boss-data";
|
||||
|
||||
export interface ExecutionBackendDescriptor {
|
||||
backendId: string;
|
||||
provider: AiProvider;
|
||||
label: string;
|
||||
mode: "local" | "api";
|
||||
}
|
||||
|
||||
export const OPENAI_BACKEND = {
|
||||
backendId: "openai-api",
|
||||
provider: "openai_api",
|
||||
label: "OpenAI API",
|
||||
mode: "api",
|
||||
} as const satisfies ExecutionBackendDescriptor;
|
||||
|
||||
export function isReadyOpenAiBackend(input: { provider: AiProvider; status: AiAccountStatus }) {
|
||||
return input.provider === OPENAI_BACKEND.provider && input.status === "ready";
|
||||
}
|
||||
Reference in New Issue
Block a user