fix: tune sub2api codex pool concurrency
This commit is contained in:
@@ -9,6 +9,7 @@ const configPath = rootPath("config", "platform-infra", "sub2api-codex-pool.yaml
|
||||
const parsed = Bun.YAML.parse(readFileSync(configPath, "utf8")) as {
|
||||
pool?: {
|
||||
defaultAccountCapacity?: number;
|
||||
minOwnerConcurrency?: number;
|
||||
defaultTempUnschedulable?: {
|
||||
enabled?: boolean;
|
||||
rules?: Array<{ statusCode?: number; keywords?: string[]; durationMinutes?: number }>;
|
||||
@@ -18,25 +19,35 @@ const parsed = Bun.YAML.parse(readFileSync(configPath, "utf8")) as {
|
||||
};
|
||||
|
||||
const entries = parsed.profiles?.entries ?? [];
|
||||
const hy = entries.find((entry) => entry.profile === "HY");
|
||||
const wsEnabled = entries.filter((entry) => entry.openaiResponsesWebSocketsV2Mode && entry.openaiResponsesWebSocketsV2Mode !== "off");
|
||||
const rules = parsed.pool?.defaultTempUnschedulable?.rules ?? [];
|
||||
const overload429 = rules.find((rule) => rule.statusCode === 429);
|
||||
const defaultCapacity = parsed.pool?.defaultAccountCapacity ?? 0;
|
||||
const desiredCapacity = entries.reduce((total, entry) => total + (entry.capacity ?? defaultCapacity), 0);
|
||||
const allowedWebSocketModes = new Set(["off", "ctx_pool", "passthrough"]);
|
||||
|
||||
assertCondition(parsed.pool?.defaultAccountCapacity === 5, "Codex pool default capacity should remain explicit YAML", parsed.pool);
|
||||
assertCondition(hy !== undefined, "HY profile should remain declared as a YAML-managed pool candidate", entries);
|
||||
assertCondition(hy?.capacity === 20, "HY capacity must remain explicitly YAML-controlled at the current configured value", hy);
|
||||
assertCondition(wsEnabled.length >= 2, "Responses WSv2 should be a capability set with multiple candidates", wsEnabled);
|
||||
assertCondition(parsed.pool?.defaultTempUnschedulable?.enabled === true, "temporary unschedulable policy must be enabled from YAML", parsed.pool?.defaultTempUnschedulable);
|
||||
assertCondition(overload429 !== undefined, "temporary unschedulable policy must include 429 handling", rules);
|
||||
assertCondition(overload429?.keywords?.includes("too many requests"), "429 handling must catch provider concurrency/rate-limit text", overload429);
|
||||
assertCondition((overload429?.durationMinutes ?? 0) > 0, "429 handling must cool down for a positive duration", overload429);
|
||||
assertCondition(entries.length > 0, "Codex pool must declare YAML-managed profile entries", parsed.profiles);
|
||||
assertCondition(Number.isInteger(defaultCapacity) && defaultCapacity > 0, "defaultAccountCapacity must be a positive integer", parsed.pool);
|
||||
assertCondition(entries.every((entry) => typeof entry.profile === "string" && entry.profile.length > 0), "profile entries must declare profile names", entries);
|
||||
assertCondition(entries.every((entry) => typeof entry.accountName === "string" && entry.accountName.length > 0), "profile entries must declare account names", entries);
|
||||
assertCondition(entries.every((entry) => entry.capacity === undefined || (Number.isInteger(entry.capacity) && entry.capacity > 0)), "profile capacity overrides must be positive integers when declared", entries);
|
||||
assertCondition(
|
||||
entries.every((entry) => entry.openaiResponsesWebSocketsV2Mode === undefined || entry.openaiResponsesWebSocketsV2Mode === null || allowedWebSocketModes.has(entry.openaiResponsesWebSocketsV2Mode)),
|
||||
"profile WebSocket mode overrides must use supported values when declared",
|
||||
entries,
|
||||
);
|
||||
assertCondition((parsed.pool?.minOwnerConcurrency ?? 0) >= desiredCapacity, "pool owner concurrency must not bottleneck the declared account capacity set", { minOwnerConcurrency: parsed.pool?.minOwnerConcurrency, desiredCapacity });
|
||||
if (parsed.pool?.defaultTempUnschedulable?.enabled === true) {
|
||||
assertCondition(rules.length > 0, "enabled temporary unschedulable policy must declare rules", parsed.pool?.defaultTempUnschedulable);
|
||||
assertCondition(rules.every((rule) => Number.isInteger(rule.statusCode) && (rule.statusCode ?? 0) >= 100 && (rule.statusCode ?? 0) <= 599), "temporary unschedulable rules must declare valid HTTP status codes", rules);
|
||||
assertCondition(rules.every((rule) => Array.isArray(rule.keywords) && rule.keywords.length > 0), "temporary unschedulable rules must declare non-empty keywords", rules);
|
||||
assertCondition(rules.every((rule) => Number.isInteger(rule.durationMinutes) && (rule.durationMinutes ?? 0) > 0), "temporary unschedulable rules must declare positive cooldown durations", rules);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
checks: [
|
||||
"HY capacity is YAML-controlled",
|
||||
"WSv2 routing is represented as a multi-account capability set",
|
||||
"temporary unschedulable rules are YAML-controlled",
|
||||
"routing config is schema-valid without profile-specific test gates",
|
||||
"pool owner concurrency covers the YAML account capacity set",
|
||||
"optional WebSocket mode overrides use supported values",
|
||||
"temporary unschedulable rules are structurally valid when enabled",
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -17,20 +17,14 @@ const credentials = renderSub2ApiTempUnschedulableCredentials(policy) as {
|
||||
};
|
||||
|
||||
const rules = credentials.temp_unschedulable_rules ?? [];
|
||||
const overload429 = rules.find((rule) => rule.error_code === 429);
|
||||
const overloaded529 = rules.find((rule) => rule.error_code === 529);
|
||||
|
||||
assertCondition(credentials.temp_unschedulable_enabled === true, "default policy must enable Sub2API temporary unschedulable mode", credentials);
|
||||
assertCondition(Array.isArray(credentials.temp_unschedulable_rules), "Sub2API rules must be rendered as an array", credentials);
|
||||
assertCondition(rules.length === policy.rules.length, "rendered rule count must match the UniDesk policy", { policy, credentials });
|
||||
assertCondition(overload429 !== undefined, "rendered policy must include 429 handling", rules);
|
||||
assertCondition(overload429?.keywords?.includes("capacity"), "429 handling must catch capacity text", overload429);
|
||||
assertCondition(overload429?.keywords?.includes("too many requests"), "429 handling must catch rate-limit text", overload429);
|
||||
assertCondition(overload429?.duration_minutes === 10, "429 cooldown must match the default policy", overload429);
|
||||
assertCondition(overloaded529 !== undefined, "rendered policy must include provider overloaded status 529", rules);
|
||||
assertCondition(rules.every((rule) => typeof rule.error_code === "number"), "rules must use Sub2API error_code field names", rules);
|
||||
assertCondition(rules.every((rule) => typeof rule.duration_minutes === "number"), "rules must use Sub2API duration_minutes field names", rules);
|
||||
assertCondition(rules.every((rule) => Array.isArray(rule.keywords) && rule.keywords.length > 0), "rules must include non-empty keyword lists", rules);
|
||||
assertCondition(rules.every((rule, index) => rule.error_code === policy.rules[index]?.statusCode), "rules must map statusCode to Sub2API error_code", { policy, credentials });
|
||||
assertCondition(rules.every((rule, index) => rule.duration_minutes === policy.rules[index]?.durationMinutes), "rules must map durationMinutes to Sub2API duration_minutes", { policy, credentials });
|
||||
assertCondition(rules.every((rule, index) => JSON.stringify(rule.keywords ?? []) === JSON.stringify(policy.rules[index]?.keywords ?? [])), "rules must preserve policy keywords", { policy, credentials });
|
||||
assertCondition(rules.every((rule, index) => rule.description === policy.rules[index]?.description), "rules must preserve policy descriptions", { policy, credentials });
|
||||
assertCondition(!("pool_mode" in credentials), "pool_mode must not be enabled because it retries the same account instead of cooling it down", credentials);
|
||||
assertCondition(!("api_key" in credentials) && !("base_url" in credentials), "temporary-unschedulable rendering must not include secrets or endpoints", credentials);
|
||||
|
||||
@@ -46,7 +40,7 @@ console.log(JSON.stringify({
|
||||
ok: true,
|
||||
checks: [
|
||||
"temporary unschedulable policy renders to Sub2API credential field names",
|
||||
"capacity/rate-limit errors cool down an account without enabling pool_mode",
|
||||
"temporary unschedulable rendering follows the input policy without hard-coded policy gates",
|
||||
"disabled policies clear runtime rules",
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -74,6 +74,7 @@ interface CodexPoolConfig {
|
||||
apiKeySecretName: string;
|
||||
apiKeySecretKey: string;
|
||||
minOwnerBalanceUsd: number;
|
||||
minOwnerConcurrency: number;
|
||||
defaultAccountCapacity: number;
|
||||
defaultTempUnschedulable: CodexTempUnschedulablePolicy;
|
||||
profiles: CodexPoolProfileConfig[];
|
||||
@@ -262,6 +263,7 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
|
||||
apiKeySecretName: pool.apiKeySecretName,
|
||||
apiKeySecretKey: pool.apiKeySecretKey,
|
||||
minOwnerBalanceUsd: pool.minOwnerBalanceUsd,
|
||||
minOwnerConcurrency: pool.minOwnerConcurrency,
|
||||
defaultAccountCapacity: pool.defaultAccountCapacity,
|
||||
},
|
||||
profiles: profiles.map((profile) => ({
|
||||
@@ -567,6 +569,7 @@ function readCodexPoolConfig(): CodexPoolConfig {
|
||||
apiKeySecretName: stringValue(pool.apiKeySecretName) ?? defaults.apiKeySecretName,
|
||||
apiKeySecretKey: stringValue(pool.apiKeySecretKey) ?? defaults.apiKeySecretKey,
|
||||
minOwnerBalanceUsd: numberValue(pool.minOwnerBalanceUsd) ?? defaults.minOwnerBalanceUsd,
|
||||
minOwnerConcurrency: readAccountCapacity(pool.minOwnerConcurrency, "pool.minOwnerConcurrency"),
|
||||
defaultAccountCapacity: readAccountCapacity(pool.defaultAccountCapacity, "pool.defaultAccountCapacity"),
|
||||
defaultTempUnschedulable,
|
||||
profiles: readProfileConfig(parsed.profiles, defaults.profiles, defaultTempUnschedulable),
|
||||
@@ -579,6 +582,10 @@ function readCodexPoolConfig(): CodexPoolConfig {
|
||||
throw new Error(`${codexPoolConfigPath}.pool.apiKeySecretKey must be a valid secret key name`);
|
||||
}
|
||||
if (config.minOwnerBalanceUsd <= 0) throw new Error(`${codexPoolConfigPath}.pool.minOwnerBalanceUsd must be > 0`);
|
||||
const declaredAccountCapacity = config.profiles.reduce((total, profile) => total + (profile.capacity ?? config.defaultAccountCapacity), 0);
|
||||
if (declaredAccountCapacity > 0 && config.minOwnerConcurrency < declaredAccountCapacity) {
|
||||
throw new Error(`${codexPoolConfigPath}.pool.minOwnerConcurrency must be >= the sum of declared account capacities (${declaredAccountCapacity})`);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -589,6 +596,8 @@ function defaultCodexPoolConfig(): CodexPoolConfig {
|
||||
apiKeySecretName: defaultPoolApiKeySecretName,
|
||||
apiKeySecretKey: defaultPoolApiKeySecretKey,
|
||||
minOwnerBalanceUsd: defaultMinOwnerBalanceUsd,
|
||||
// Only used for the no-YAML fallback path; real pool configs must declare pool.minOwnerConcurrency.
|
||||
minOwnerConcurrency: 1,
|
||||
defaultAccountCapacity: 5,
|
||||
defaultTempUnschedulable: defaultCodexTempUnschedulablePolicy(),
|
||||
profiles: [],
|
||||
@@ -986,6 +995,7 @@ function poolTarget(pool = readCodexPoolConfig()): Record<string, unknown> {
|
||||
groupName: pool.groupName,
|
||||
apiKeyName: pool.apiKeyName,
|
||||
apiKeySecret: `${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`,
|
||||
minOwnerConcurrency: pool.minOwnerConcurrency,
|
||||
defaultAccountCapacity: pool.defaultAccountCapacity,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
@@ -1698,6 +1708,7 @@ POOL_API_KEY_NAME = "${pool.apiKeyName}"
|
||||
POOL_API_KEY_SECRET_NAME = "${pool.apiKeySecretName}"
|
||||
POOL_API_KEY_SECRET_KEY = "${pool.apiKeySecretKey}"
|
||||
MIN_OWNER_BALANCE_USD = ${JSON.stringify(pool.minOwnerBalanceUsd)}
|
||||
MIN_OWNER_CONCURRENCY = ${JSON.stringify(pool.minOwnerConcurrency)}
|
||||
POOL_DEFAULT_ACCOUNT_CAPACITY = ${JSON.stringify(pool.defaultAccountCapacity)}
|
||||
EXPECTED_ACCOUNT_CAPACITIES = ${JSON.stringify(desiredAccountCapacityMap(pool))}
|
||||
EXPECTED_ACCOUNT_WS_MODES = json.loads(${JSON.stringify(JSON.stringify(desiredAccountWebSocketsV2ModeMap(pool)))})
|
||||
@@ -2105,6 +2116,37 @@ def ensure_pool_owner_balance(token, user_id):
|
||||
"minimumBalanceUsd": MIN_OWNER_BALANCE_USD,
|
||||
}
|
||||
|
||||
def ensure_pool_owner_concurrency(token, user_id):
|
||||
user = get_admin_user(token, user_id)
|
||||
try:
|
||||
current = int(user.get("concurrency") or 0)
|
||||
except Exception:
|
||||
current = 0
|
||||
if current >= MIN_OWNER_CONCURRENCY:
|
||||
return {
|
||||
"ok": True,
|
||||
"action": "kept-existing",
|
||||
"userId": user_id,
|
||||
"concurrencyBefore": current,
|
||||
"concurrencyAfter": current,
|
||||
"minimumConcurrency": MIN_OWNER_CONCURRENCY,
|
||||
}
|
||||
updated = ensure_success(curl_api("PUT", f"/api/v1/admin/users/{user_id}", bearer=token, payload={
|
||||
"concurrency": MIN_OWNER_CONCURRENCY,
|
||||
}), "set API key owner concurrency")
|
||||
try:
|
||||
after = int(updated.get("concurrency") or MIN_OWNER_CONCURRENCY) if isinstance(updated, dict) else MIN_OWNER_CONCURRENCY
|
||||
except Exception:
|
||||
after = MIN_OWNER_CONCURRENCY
|
||||
return {
|
||||
"ok": after >= MIN_OWNER_CONCURRENCY,
|
||||
"action": "set",
|
||||
"userId": user_id,
|
||||
"concurrencyBefore": current,
|
||||
"concurrencyAfter": after,
|
||||
"minimumConcurrency": MIN_OWNER_CONCURRENCY,
|
||||
}
|
||||
|
||||
def validate_gateway(api_key):
|
||||
resp = curl_api("GET", "/v1/models", bearer=api_key)
|
||||
parsed = resp.get("json")
|
||||
@@ -2369,9 +2411,10 @@ def run_sync():
|
||||
api_key, secret_action, secret_apply_stdout = ensure_api_key_secret(group_id)
|
||||
api_key_result = ensure_sub2api_api_key(token, api_key, group_id)
|
||||
owner_balance = ensure_pool_owner_balance(token, api_key_result["userId"])
|
||||
owner_concurrency = ensure_pool_owner_concurrency(token, api_key_result["userId"])
|
||||
gateway = validate_gateway(api_key)
|
||||
return {
|
||||
"ok": gateway["ok"] is True and capacity_status["ok"] is True and ws_v2_status["ok"] is True and temp_unschedulable_status["ok"] is True,
|
||||
"ok": gateway["ok"] is True and owner_concurrency["ok"] is True and capacity_status["ok"] is True and ws_v2_status["ok"] is True and temp_unschedulable_status["ok"] is True,
|
||||
"mode": "sync",
|
||||
"namespace": NAMESPACE,
|
||||
"serviceDns": SERVICE_DNS,
|
||||
@@ -2403,6 +2446,7 @@ def run_sync():
|
||||
"valuesPrinted": False,
|
||||
},
|
||||
"ownerBalance": owner_balance,
|
||||
"ownerConcurrency": owner_concurrency,
|
||||
"validation": {"gatewayModels": gateway},
|
||||
}
|
||||
|
||||
@@ -2413,14 +2457,16 @@ def run_validate():
|
||||
admin_email, token = login()
|
||||
key_item = next((item for item in list_user_keys(token) if item.get("key") == api_key), None)
|
||||
owner_balance = None
|
||||
owner_concurrency = None
|
||||
if key_item is not None and key_item.get("user_id") is not None:
|
||||
owner_balance = ensure_pool_owner_balance(token, key_item["user_id"])
|
||||
owner_concurrency = ensure_pool_owner_concurrency(token, key_item["user_id"])
|
||||
capacity_status = account_capacity_status(token)
|
||||
ws_v2_status = account_ws_v2_status(token)
|
||||
temp_unschedulable_status = account_temp_unschedulable_status(token)
|
||||
gateway = validate_gateway(api_key)
|
||||
return {
|
||||
"ok": gateway["ok"] is True and capacity_status["ok"] is True and ws_v2_status["ok"] is True and temp_unschedulable_status["ok"] is True,
|
||||
"ok": gateway["ok"] is True and (owner_concurrency is None or owner_concurrency["ok"] is True) and capacity_status["ok"] is True and ws_v2_status["ok"] is True and temp_unschedulable_status["ok"] is True,
|
||||
"mode": "validate",
|
||||
"namespace": NAMESPACE,
|
||||
"serviceDns": SERVICE_DNS,
|
||||
@@ -2434,6 +2480,7 @@ def run_validate():
|
||||
"valuesPrinted": False,
|
||||
},
|
||||
"ownerBalance": owner_balance,
|
||||
"ownerConcurrency": owner_concurrency,
|
||||
"capacity": capacity_status,
|
||||
"webSocketsV2": ws_v2_status,
|
||||
"tempUnschedulable": temp_unschedulable_status,
|
||||
|
||||
Reference in New Issue
Block a user