AI_PROJECT / src /proxy /api-endpoint-pool.mjs
chenchenaoyang's picture
Deploy HF Space with dataset backend
476094d verified
import fs from "node:fs/promises";
import path from "node:path";
import { classifyFailure } from "./codex-account-pool.mjs";
const COOLDOWN_SECONDS = {
auth: 1800,
quota: 900,
rate_limit: 120,
server: 45,
network: 30,
invalid: 300,
};
function nowMs(nowFn = Date.now) {
return Number(nowFn());
}
function isoFromMs(ms) {
return new Date(ms).toISOString();
}
function normalizeProvider(value) {
const input = String(value || "").trim().toLowerCase();
if (input === "codex") return "codex";
if (input === "claude-code" || input === "claude_code" || input === "claude") {
return "claude-code";
}
return "";
}
function prefersOpenAICompatibility(endpoint) {
const model = String(endpoint?.model || "").trim().toLowerCase();
return (
model.startsWith("gpt-") ||
model.startsWith("o1") ||
model.startsWith("o3") ||
model.startsWith("o4") ||
model.includes("codex")
);
}
function makeEndpoint(raw, filePath) {
return {
id: path.basename(filePath),
filePath,
raw,
name: raw.name || path.basename(filePath, path.extname(filePath)),
type: normalizeProvider(raw.type),
baseUrl: String(raw.baseUrl || "").trim(),
apiKey: String(raw.apiKey || "").trim(),
model: String(raw.model || "").trim(),
probePath: String(raw.probePath || "").trim(),
disabled: Boolean(raw.disabled),
lastValidation: null,
lastFailureReason: "",
consecutiveFailures: 0,
cooldownUntilMs: 0,
healthy: false,
};
}
function makeEndpointFromEntry(raw, filePath, index = 0) {
const endpoint = makeEndpoint(raw, filePath);
if (index > 0) {
endpoint.id = `${path.basename(filePath)}#${index + 1}`;
}
if (!endpoint.name) {
endpoint.name = `${path.basename(filePath, path.extname(filePath))}-${index + 1}`;
}
return endpoint;
}
function normalizeRawEntries(raw) {
if (Array.isArray(raw)) {
return raw.filter((entry) => entry && typeof entry === "object");
}
if (raw && typeof raw === "object") {
return [raw];
}
return [];
}
export function normalizeEndpointType(value) {
return normalizeProvider(value);
}
export function isEndpointStructurallyEligible(endpoint, provider = "") {
if (!endpoint) return false;
if (endpoint.disabled) return false;
if (!endpoint.baseUrl || !endpoint.apiKey) return false;
if (!["codex", "claude-code"].includes(endpoint.type)) return false;
if (provider && endpoint.type !== normalizeProvider(provider)) return false;
return true;
}
export class ApiEndpointPool {
constructor({
poolDir,
provider,
fetchFn = fetch,
nowFn = Date.now,
logger = () => {},
loadSnapshot = null,
sourcePath = "",
}) {
this.poolDir = poolDir;
this.provider = normalizeProvider(provider);
this.fetchFn = fetchFn;
this.nowFn = nowFn;
this.logger = logger;
this.loadSnapshot = loadSnapshot;
this.sourcePath = sourcePath || (poolDir ? path.join(poolDir, "pool.json") : "pool.json");
this.endpoints = [];
this.activeEndpointId = null;
this.activeEndpointVersion = 0;
}
listEndpoints() {
return [...this.endpoints];
}
getActiveEndpoint() {
if (!this.activeEndpointId) return null;
return this.endpoints.find((endpoint) => endpoint.id === this.activeEndpointId) || null;
}
getActiveEndpointVersion() {
return this.activeEndpointVersion;
}
async readSnapshots() {
if (typeof this.loadSnapshot === "function") {
const loaded = await this.loadSnapshot();
if (!loaded) return [];
if (Array.isArray(loaded)) {
return [{ entries: loaded, sourcePath: this.sourcePath }];
}
return [
{
entries: Array.isArray(loaded.entries) ? loaded.entries : [],
sourcePath: loaded.sourcePath || this.sourcePath,
},
];
}
const dirEntries = await fs.readdir(this.poolDir, { withFileTypes: true });
const allFiles = dirEntries
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
.map((entry) => path.join(this.poolDir, entry.name))
.sort((left, right) => left.localeCompare(right));
const prioritizedPoolFile = allFiles.find((filePath) => path.basename(filePath) === "pool.json");
const files = prioritizedPoolFile ? [prioritizedPoolFile] : allFiles;
const snapshots = [];
for (const filePath of files) {
try {
const raw = JSON.parse(await fs.readFile(filePath, "utf8"));
snapshots.push({
entries: normalizeRawEntries(raw),
sourcePath: filePath,
});
} catch {
snapshots.push({
entries: [],
sourcePath: filePath,
});
}
}
return snapshots;
}
async load() {
const snapshots = await this.readSnapshots();
const loaded = [];
for (const snapshot of snapshots) {
const entries = normalizeRawEntries(snapshot.entries);
if (entries.length === 0) {
this.logger("load:skip", {
file: path.basename(snapshot.sourcePath),
reason: "empty-or-invalid-json",
});
continue;
}
for (const [index, entry] of entries.entries()) {
const endpoint = makeEndpointFromEntry(entry, snapshot.sourcePath, index);
if (!isEndpointStructurallyEligible(endpoint, this.provider)) {
this.logger("load:skip", {
file: path.basename(snapshot.sourcePath),
index,
reason: "structurally-ineligible",
});
continue;
}
loaded.push(endpoint);
this.logger("load:endpoint", {
file: path.basename(snapshot.sourcePath),
index,
provider: endpoint.type,
baseUrl: endpoint.baseUrl,
});
}
}
this.endpoints = loaded;
if (!this.activeEndpointId && this.endpoints.length > 0) {
this.activeEndpointId = this.endpoints[0].id;
}
if (
this.activeEndpointId &&
!this.endpoints.find((endpoint) => endpoint.id === this.activeEndpointId)
) {
this.activeEndpointId = this.endpoints[0]?.id || null;
}
}
isCoolingDown(endpoint) {
return endpoint.cooldownUntilMs > nowMs(this.nowFn);
}
pickNextHealthyEndpoint(excluded = new Set()) {
const ordered = this.endpoints;
const activeId = this.activeEndpointId;
const startIdx = activeId
? Math.max(ordered.findIndex((endpoint) => endpoint.id === activeId), 0)
: 0;
const rotated = ordered
.slice(startIdx + 1)
.concat(ordered.slice(0, startIdx + 1));
return (
rotated.find((endpoint) => !excluded.has(endpoint.id) && !this.isCoolingDown(endpoint)) ||
null
);
}
pickNextRotationCandidate(excluded = new Set()) {
if (this.endpoints.length <= 1) return null;
const ordered = this.endpoints;
const activeId = this.activeEndpointId;
const startIdx = activeId
? Math.max(ordered.findIndex((endpoint) => endpoint.id === activeId), 0)
: 0;
const rotated = ordered
.slice(startIdx + 1)
.concat(ordered.slice(0, startIdx + 1));
return (
rotated.find(
(endpoint) =>
endpoint.id !== activeId &&
!excluded.has(endpoint.id) &&
!endpoint.disabled &&
!this.isCoolingDown(endpoint),
) || null
);
}
recordHealthy(endpoint) {
endpoint.healthy = true;
endpoint.consecutiveFailures = 0;
endpoint.lastFailureReason = "";
endpoint.cooldownUntilMs = 0;
endpoint.lastValidation = isoFromMs(nowMs(this.nowFn));
}
setActiveEndpoint(endpoint, mode = "failover", { expectedVersion = null, expectedId } = {}) {
if (
expectedVersion !== null &&
(this.activeEndpointVersion !== expectedVersion || this.activeEndpointId !== expectedId)
) {
return false;
}
const previousId = this.activeEndpointId;
const previous =
previousId ? this.endpoints.find((item) => item.id === previousId) : null;
const previousFailure = previous?.lastFailureReason || "";
this.activeEndpointId = endpoint.id;
if (previousId !== endpoint.id) {
this.activeEndpointVersion += 1;
}
if (previousId && previousId !== endpoint.id) {
if (mode === "scheduled") {
this.logger("pool:active-endpoint:scheduled", {
message: `活跃节点定时切换:${previousId}${endpoint.id} (${endpoint.name || ""})`,
id: endpoint.id,
name: endpoint.name,
baseUrl: endpoint.baseUrl,
model: endpoint.model,
previousId,
});
} else {
this.logger("pool:active-endpoint", {
message: `活跃节点切换:${previousId}${endpoint.id} (${endpoint.name || ""}),上一个节点失败原因:${previousFailure || "未知"}`,
id: endpoint.id,
name: endpoint.name,
baseUrl: endpoint.baseUrl,
model: endpoint.model,
previousId,
previousFailure,
});
}
}
return true;
}
markSuccess(endpoint, options = {}) {
this.recordHealthy(endpoint);
return this.setActiveEndpoint(endpoint, "failover", options);
}
markScheduledSwitch(endpoint) {
this.recordHealthy(endpoint);
return this.setActiveEndpoint(endpoint, "scheduled");
}
markFailure(endpoint, category, reason) {
endpoint.healthy = false;
endpoint.consecutiveFailures += 1;
endpoint.lastFailureReason = `${category}:${reason}`;
const cooldownSeconds = COOLDOWN_SECONDS[category] || COOLDOWN_SECONDS.invalid;
endpoint.cooldownUntilMs = nowMs(this.nowFn) + cooldownSeconds * 1000;
}
classifyProbeFailure({ status = 0, detail = "" }) {
return classifyFailure({ status, detail });
}
resolveProbeRequest(endpoint) {
if (endpoint.probePath) {
return {
method: "GET",
path: endpoint.probePath,
headers: {},
body: null,
};
}
if (endpoint.type === "claude-code") {
if (endpoint.model) {
return {
method: "POST",
path: "/v1/messages",
headers: {
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: endpoint.model,
max_tokens: 1,
messages: [{ role: "user", content: "ping" }],
}),
};
}
return {
method: "GET",
path: "/v1/models",
headers: {},
body: null,
};
}
if (endpoint.model) {
return {
method: "POST",
path: "/v1/responses",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
model: endpoint.model,
input: "Reply with OK.",
max_output_tokens: 8,
}),
};
}
return {
method: "GET",
path: "/v1/models",
headers: {},
body: null,
};
}
async probeEndpoint(endpoint, options = {}) {
const activateOnSuccess = options.activateOnSuccess !== false;
const activationMode = options.activationMode || "failover";
const probe = this.resolveProbeRequest(endpoint);
const url = new URL(probe.path, endpoint.baseUrl.endsWith("/") ? endpoint.baseUrl : `${endpoint.baseUrl}/`);
this.logger("probe:start", {
id: endpoint.id,
provider: endpoint.type,
url: url.toString(),
});
let response;
try {
response = await this.fetchFn(url, {
method: probe.method,
headers: {
authorization: `Bearer ${endpoint.apiKey}`,
"x-api-key": endpoint.apiKey,
...probe.headers,
},
body: probe.body,
});
} catch (error) {
const detail = error?.message || String(error);
this.markFailure(endpoint, "network", detail);
return {
ok: false,
status: 0,
category: "network",
reason: "network",
detail,
};
}
if (response.ok) {
this.recordHealthy(endpoint);
if (activateOnSuccess) {
this.setActiveEndpoint(endpoint, activationMode);
}
return { ok: true, status: response.status, category: "ok", reason: "probe-ok" };
}
const detail = await response.text();
const classified = this.classifyProbeFailure({ status: response.status, detail });
this.markFailure(endpoint, classified.category, detail || classified.reason);
return {
ok: false,
status: response.status,
category: classified.category,
reason: classified.reason,
detail,
};
}
async getInitialEndpoint() {
if (this.endpoints.length === 0) return null;
const excluded = new Set();
for (let i = 0; i < this.endpoints.length; i += 1) {
const currentActive = this.getActiveEndpoint();
const candidate =
i === 0 && currentActive && !excluded.has(currentActive.id)
? currentActive
: this.pickNextHealthyEndpoint(excluded);
if (!candidate) break;
excluded.add(candidate.id);
if (this.isCoolingDown(candidate)) continue;
const probe = await this.probeEndpoint(candidate, {
activateOnSuccess: true,
activationMode: "initial",
});
if (probe.ok) {
this.activeEndpointId = candidate.id;
return candidate;
}
}
return null;
}
}