chore: 合并固定主工作区并行提交
This commit is contained in:
@@ -88,6 +88,11 @@ def classify_trace_event(item, account_names_by_id):
|
||||
"error": item.get("error"),
|
||||
"excludedAccountCount": item.get("excluded_account_count"),
|
||||
})
|
||||
elif "openai.forward_failed" in message:
|
||||
event.update({
|
||||
"type": "forward-failed",
|
||||
"error": item.get("error"),
|
||||
})
|
||||
elif "account_upstream_error" in message:
|
||||
event.update({
|
||||
"type": "upstream-error",
|
||||
@@ -160,10 +165,15 @@ def trace_reason(events, final_event):
|
||||
failovers = [item for item in events if item.get("type") == "failover"]
|
||||
select_failures = [item for item in events if item.get("type") == "select-failed"]
|
||||
upstream_errors = [item for item in events if item.get("type") == "upstream-error"]
|
||||
forward_failures = [item for item in events if item.get("type") == "forward-failed"]
|
||||
if failover_budget_exhausted(failovers, final_event):
|
||||
return "failover-budget-exhausted"
|
||||
if failovers and select_failures:
|
||||
return "failover-attempted-no-candidate"
|
||||
if forward_failures and isinstance(final_event, dict) and isinstance(final_event.get("statusCode"), int) and final_event.get("statusCode") < 400:
|
||||
return "forward-failed-http-success"
|
||||
if forward_failures:
|
||||
return "forward-failed"
|
||||
if failovers:
|
||||
return "failover-attempted"
|
||||
if select_failures:
|
||||
@@ -217,6 +227,16 @@ def trace_untried_schedulable_accounts(failovers, final_event, account_snapshot)
|
||||
})
|
||||
return result
|
||||
|
||||
def ops_start_time_for_since(since):
|
||||
if not isinstance(since, str):
|
||||
return None
|
||||
match = re.fullmatch(r"\s*(\d+)\s*([smhd])\s*", since.lower())
|
||||
if match is None:
|
||||
return None
|
||||
value = int(match.group(1))
|
||||
multiplier = {"s": 1, "m": 60, "h": 3600, "d": 86400}[match.group(2)]
|
||||
return (datetime.now(timezone.utc) - timedelta(seconds=value * multiplier)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
def run_trace():
|
||||
payload = json.loads(base64.b64decode(PAYLOAD_B64).decode("utf-8")) if PAYLOAD_B64 else {}
|
||||
request_id = payload.get("requestId")
|
||||
@@ -227,6 +247,89 @@ def run_trace():
|
||||
if not isinstance(request_id, str) or not request_id:
|
||||
raise RuntimeError("trace payload missing requestId")
|
||||
admin_email, token, admin_compliance = login()
|
||||
request_query = quote(request_id, safe="")
|
||||
native_request_status = {"status": "unavailable"}
|
||||
native_request = None
|
||||
try:
|
||||
request_data = ensure_success(
|
||||
curl_api("GET", f"/api/v1/admin/ops/requests?request_id={request_query}&kind=all&page=1&page_size=100", bearer=token),
|
||||
"get ops request details",
|
||||
)
|
||||
request_items = extract_items(request_data)
|
||||
native_request = next((item for item in request_items if isinstance(item, dict) and item.get("request_id") == request_id), None)
|
||||
native_request_status = {
|
||||
"status": "available",
|
||||
"matched": native_request is not None,
|
||||
"returned": len(request_items),
|
||||
"total": request_data.get("total", len(request_items)) if isinstance(request_data, dict) else len(request_items),
|
||||
}
|
||||
if native_request is None:
|
||||
ops_start_time = ops_start_time_for_since(since)
|
||||
error_query = f"/api/v1/admin/ops/request-errors?q={request_query}&view=all&page=1&page_size=20"
|
||||
if isinstance(ops_start_time, str):
|
||||
error_query += "&start_time=" + quote(ops_start_time, safe="")
|
||||
error_data = ensure_success(
|
||||
curl_api("GET", error_query, bearer=token),
|
||||
"get ops request error fallback",
|
||||
)
|
||||
error_items = extract_items(error_data)
|
||||
native_request = next((item for item in error_items if isinstance(item, dict) and item.get("request_id") == request_id), None)
|
||||
native_request_status.update({
|
||||
"fallback": "sub2api-native-admin-ops-request-errors",
|
||||
"fallbackStartTime": ops_start_time,
|
||||
"fallbackReturned": len(error_items),
|
||||
"fallbackTotal": error_data.get("total", len(error_items)) if isinstance(error_data, dict) else len(error_items),
|
||||
})
|
||||
if isinstance(native_request, dict):
|
||||
native_request = dict(native_request)
|
||||
native_request["kind"] = "error"
|
||||
error_id = native_request.get("id")
|
||||
if isinstance(error_id, int):
|
||||
try:
|
||||
detail = ensure_success(curl_api("GET", f"/api/v1/admin/ops/request-errors/{error_id}", bearer=token), "get ops request error detail")
|
||||
if isinstance(detail, dict):
|
||||
native_request.update(detail)
|
||||
except Exception:
|
||||
pass
|
||||
native_request_status.update({"matched": True, "source": "sub2api-native-admin-ops-request-errors"})
|
||||
except Exception as exc:
|
||||
native_request_status = {"status": "unavailable", "reason": text(str(exc), 500)}
|
||||
|
||||
native_system_log_status = {"status": "unavailable"}
|
||||
native_matched = []
|
||||
try:
|
||||
system_log_data = ensure_success(
|
||||
curl_api("GET", f"/api/v1/admin/ops/system-logs?request_id={request_query}&page=1&page_size=200", bearer=token),
|
||||
"get ops request system logs",
|
||||
)
|
||||
system_log_items = extract_items(system_log_data)
|
||||
for row in system_log_items:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
parsed = dict(row.get("extra") or {}) if isinstance(row.get("extra"), dict) else {}
|
||||
for source_key in ("request_id", "client_request_id", "account_id", "platform", "model"):
|
||||
if parsed.get(source_key) is None and row.get(source_key) is not None:
|
||||
parsed[source_key] = row.get(source_key)
|
||||
parsed["_at"] = row.get("created_at")
|
||||
parsed["_message"] = row.get("message")
|
||||
parsed["_line"] = json.dumps({
|
||||
"created_at": row.get("created_at"),
|
||||
"message": row.get("message"),
|
||||
"request_id": row.get("request_id"),
|
||||
"account_id": row.get("account_id"),
|
||||
"platform": row.get("platform"),
|
||||
"model": row.get("model"),
|
||||
"extra": parsed,
|
||||
}, ensure_ascii=False, default=str)
|
||||
native_matched.append(parsed)
|
||||
native_system_log_status = {
|
||||
"status": "available",
|
||||
"returned": len(system_log_items),
|
||||
"total": system_log_data.get("total", len(system_log_items)) if isinstance(system_log_data, dict) else len(system_log_items),
|
||||
}
|
||||
except Exception as exc:
|
||||
native_system_log_status = {"status": "unavailable", "reason": text(str(exc), 500)}
|
||||
|
||||
account_snapshot, account_snapshot_error = account_snapshot_from_runtime(token)
|
||||
account_names_by_id = {}
|
||||
for row in account_snapshot:
|
||||
@@ -235,17 +338,21 @@ def run_trace():
|
||||
account_id = int(account_id)
|
||||
if isinstance(account_id, int) and isinstance(row.get("accountName"), str):
|
||||
account_names_by_id[account_id] = row.get("accountName")
|
||||
proc = kubectl(["-n", NAMESPACE, "logs", "deployment/sub2api", f"--since={since}", f"--tail={tail}"])
|
||||
stdout = proc.stdout.decode("utf-8", errors="replace")
|
||||
proc = runtime_logs(since, tail)
|
||||
log_bytes = proc.stdout + b"\\n" + proc.stderr
|
||||
stdout = log_bytes.decode("utf-8", errors="replace")
|
||||
parsed_lines = []
|
||||
matched = []
|
||||
runtime_matched = []
|
||||
for line in stdout.splitlines():
|
||||
parsed = parse_log_line(line)
|
||||
if parsed is None:
|
||||
continue
|
||||
parsed_lines.append(parsed)
|
||||
if request_id in line:
|
||||
matched.append(parsed)
|
||||
runtime_matched.append(parsed)
|
||||
matched = native_matched if native_matched else runtime_matched
|
||||
matched.sort(key=lambda item: (log_time_epoch(item) is None, log_time_epoch(item) or 0))
|
||||
trace_event_source = "sub2api-native-admin-ops-system-logs" if native_matched else "runtime-logs-fallback"
|
||||
first_epoch = None
|
||||
last_epoch = None
|
||||
for item in matched:
|
||||
@@ -266,34 +373,94 @@ def run_trace():
|
||||
window_lines = matched
|
||||
events = [classify_trace_event(item, account_names_by_id) for item in matched]
|
||||
request_start = next((item for item in events if item.get("type") == "request-start"), None)
|
||||
if isinstance(native_request, dict):
|
||||
if not isinstance(request_start, dict):
|
||||
request_start = {
|
||||
"type": "request-start",
|
||||
"at": native_request.get("created_at"),
|
||||
"requestId": native_request.get("request_id"),
|
||||
"path": native_request.get("request_path") or native_request.get("inbound_endpoint"),
|
||||
"model": native_request.get("model"),
|
||||
"accountId": native_request.get("account_id"),
|
||||
"accountName": account_names_by_id.get(native_request.get("account_id")),
|
||||
"stream": native_request.get("stream"),
|
||||
"source": "sub2api-native-admin-ops-requests",
|
||||
}
|
||||
else:
|
||||
if request_start.get("stream") is None:
|
||||
request_start["stream"] = native_request.get("stream")
|
||||
if not request_start.get("model"):
|
||||
request_start["model"] = native_request.get("model")
|
||||
final_event = next((item for item in reversed(events) if item.get("type") == "final"), None)
|
||||
if not isinstance(final_event, dict) and isinstance(native_request, dict) and isinstance(native_request.get("status_code"), int):
|
||||
final_event = {
|
||||
"type": "final",
|
||||
"at": native_request.get("created_at"),
|
||||
"requestId": native_request.get("request_id"),
|
||||
"path": native_request.get("request_path") or native_request.get("inbound_endpoint"),
|
||||
"model": native_request.get("model"),
|
||||
"accountId": native_request.get("account_id"),
|
||||
"accountName": account_names_by_id.get(native_request.get("account_id")),
|
||||
"statusCode": native_request.get("status_code"),
|
||||
"latencyMs": native_request.get("duration_ms"),
|
||||
"source": "sub2api-native-admin-ops-requests",
|
||||
}
|
||||
reference_at = request_start.get("at") if isinstance(request_start, dict) else None
|
||||
reference_epoch = log_time_epoch({"_at": reference_at}) if isinstance(reference_at, str) else first_epoch
|
||||
reference_source = "request-start" if isinstance(reference_at, str) and reference_epoch is not None else "first-matched-event"
|
||||
native_duration_ms = native_request.get("duration_ms") if isinstance(native_request, dict) else None
|
||||
if not isinstance(native_duration_ms, int) and isinstance(final_event, dict):
|
||||
native_duration_ms = final_event.get("latencyMs")
|
||||
if (
|
||||
isinstance(request_start, dict)
|
||||
and request_start.get("source") == "sub2api-native-admin-ops-requests"
|
||||
and isinstance(native_duration_ms, int)
|
||||
and native_duration_ms >= 0
|
||||
and reference_epoch is not None
|
||||
):
|
||||
reference_epoch -= native_duration_ms / 1000
|
||||
reference_at = datetime.fromtimestamp(reference_epoch, timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
reference_source = "native-completion-minus-duration"
|
||||
for event in events:
|
||||
epoch = log_time_epoch({"_at": event.get("at")}) if isinstance(event.get("at"), str) else None
|
||||
event["elapsedMs"] = round((epoch - reference_epoch) * 1000) if epoch is not None and reference_epoch is not None else None
|
||||
failovers = [item for item in events if item.get("type") == "failover"]
|
||||
select_failures = [item for item in events if item.get("type") == "select-failed"]
|
||||
upstream_errors = [item for item in events if item.get("type") == "upstream-error"]
|
||||
forward_failures = [item for item in events if item.get("type") == "forward-failed"]
|
||||
temp_unsched = [with_trace_phase(classify_trace_event(item, account_names_by_id), first_epoch, last_epoch) for item in window_lines if "account_temp_unschedulable" in str(item.get("_message") or "")]
|
||||
admin_sched = [with_trace_phase(classify_trace_event(item, account_names_by_id), first_epoch, last_epoch) for item in window_lines if ("schedulable" in str(item.get("_message") or "") or "/schedulable" in str(item.get("path") or ""))]
|
||||
window_events = [classify_trace_event(item, account_names_by_id) for item in window_lines]
|
||||
final_errors = [item for item in window_events if item.get("type") == "final" and isinstance(item.get("statusCode"), int) and item.get("statusCode") >= 400]
|
||||
window_failovers = [item for item in window_events if item.get("type") == "failover"]
|
||||
window_select_failures = [item for item in window_events if item.get("type") == "select-failed"]
|
||||
window_forward_failures = [item for item in window_events if item.get("type") == "forward-failed"]
|
||||
untried_schedulable_accounts = trace_untried_schedulable_accounts(failovers, final_event or {}, account_snapshot)
|
||||
first_failover_elapsed = failovers[0].get("elapsedMs") if failovers else None
|
||||
first_select_failed_elapsed = select_failures[0].get("elapsedMs") if select_failures else None
|
||||
final_epoch = log_time_epoch({"_at": final_event.get("at")}) if isinstance(final_event, dict) and isinstance(final_event.get("at"), str) else None
|
||||
final_elapsed = round((final_epoch - reference_epoch) * 1000) if final_epoch is not None and reference_epoch is not None else None
|
||||
failover_to_final = final_elapsed - first_failover_elapsed if isinstance(final_elapsed, int) and isinstance(first_failover_elapsed, int) else None
|
||||
reason = trace_reason(events, final_event)
|
||||
if not matched:
|
||||
if not matched and not isinstance(native_request, dict):
|
||||
outcome = "not-found"
|
||||
elif isinstance(final_event, dict) and isinstance(final_event.get("statusCode"), int) and final_event.get("statusCode") < 400:
|
||||
outcome = "succeeded"
|
||||
outcome = "degraded" if forward_failures else ("succeeded-with-failover" if failovers else "succeeded")
|
||||
elif isinstance(final_event, dict):
|
||||
outcome = "failed"
|
||||
else:
|
||||
outcome = "incomplete"
|
||||
return {
|
||||
"ok": proc.returncode == 0 and len(matched) > 0,
|
||||
"ok": proc.returncode == 0 and (len(matched) > 0 or isinstance(native_request, dict)),
|
||||
"mode": "trace",
|
||||
"targetId": TARGET_ID,
|
||||
"runtimeMode": RUNTIME_MODE,
|
||||
"namespace": NAMESPACE,
|
||||
"serviceDns": SERVICE_DNS,
|
||||
"appPod": APP_POD,
|
||||
"admin": {"email": admin_email, "tokenPrinted": False, "compliance": admin_compliance},
|
||||
"requestId": request_id,
|
||||
"eventSource": trace_event_source,
|
||||
"summary": {
|
||||
"outcome": outcome,
|
||||
"reason": reason,
|
||||
@@ -311,10 +478,16 @@ def run_trace():
|
||||
},
|
||||
"request": request_start or {},
|
||||
"final": final_event or {},
|
||||
"nativeOps": {
|
||||
"requestDetails": native_request_status,
|
||||
"request": native_request,
|
||||
"systemLogs": native_system_log_status,
|
||||
},
|
||||
"events": events,
|
||||
"failovers": failovers,
|
||||
"selectFailures": select_failures,
|
||||
"upstreamErrors": upstream_errors,
|
||||
"forwardFailures": forward_failures,
|
||||
"tempUnschedulable": temp_unsched,
|
||||
"adminSchedulable": admin_sched[-20:],
|
||||
"windowStats": {
|
||||
@@ -323,20 +496,35 @@ def run_trace():
|
||||
"finalErrorCount": len(final_errors),
|
||||
"failoverCount": len(window_failovers),
|
||||
"selectFailedCount": len(window_select_failures),
|
||||
"forwardFailedCount": len(window_forward_failures),
|
||||
"matchedForwardFailedCount": len(forward_failures),
|
||||
"tempUnschedulableCount": len(temp_unsched),
|
||||
"adminSchedulableCount": len(admin_sched),
|
||||
},
|
||||
"diagnostics": {
|
||||
"failoverBudgetExhausted": failover_budget_exhausted(failovers, final_event or {}),
|
||||
"untriedSchedulableAccounts": untried_schedulable_accounts,
|
||||
"timing": {
|
||||
"referenceAt": reference_at if isinstance(reference_at, str) else (events[0].get("at") if events else None),
|
||||
"referenceSource": reference_source,
|
||||
"firstFailoverElapsedMs": first_failover_elapsed,
|
||||
"firstSelectFailedElapsedMs": first_select_failed_elapsed,
|
||||
"finalElapsedMs": final_elapsed,
|
||||
"failoverToFinalMs": failover_to_final,
|
||||
"clientDeadlineAvailable": False,
|
||||
"clientDeadlineRemainingMs": None,
|
||||
"attribution": "Elapsed values come from indexed event timestamps. The client deadline is not present in Sub2API Ops records, so remaining request budget cannot be calculated.",
|
||||
},
|
||||
},
|
||||
"accountSnapshot": account_snapshot,
|
||||
"accountSnapshotError": account_snapshot_error,
|
||||
"rawLines": [{"line": item.get("_line")} for item in matched[-30:]] if show_lines else [],
|
||||
"showLines": show_lines,
|
||||
"logs": {
|
||||
"source": trace_event_source,
|
||||
"contextSource": f"host-docker:{HOST_DOCKER_APP_CONTAINER}" if RUNTIME_MODE == "host-docker" else f"k3s:{NAMESPACE}/deployment/sub2api",
|
||||
"exitCode": proc.returncode,
|
||||
"stderrTail": text(proc.stderr, 1000),
|
||||
"stderrTail": text(proc.stderr, 1000) if proc.returncode != 0 else "",
|
||||
"stdoutLineCount": len(stdout.splitlines()),
|
||||
},
|
||||
"valuesPrinted": False,
|
||||
|
||||
@@ -398,8 +398,12 @@ export function renderTraceReport(
|
||||
const failovers = recordArray(parsed.failovers);
|
||||
const selectFailures = recordArray(parsed.selectFailures);
|
||||
const upstreamErrors = recordArray(parsed.upstreamErrors);
|
||||
const forwardFailures = recordArray(parsed.forwardFailures);
|
||||
const tempUnschedulable = recordArray(parsed.tempUnschedulable);
|
||||
const windowStats = isRecord(parsed.windowStats) ? parsed.windowStats : {};
|
||||
const diagnostics = isRecord(parsed.diagnostics) ? parsed.diagnostics : {};
|
||||
const timing = isRecord(diagnostics.timing) ? diagnostics.timing : {};
|
||||
const logs = isRecord(parsed.logs) ? parsed.logs : {};
|
||||
const accountSnapshot = recordArray(parsed.accountSnapshot);
|
||||
const lines: string[] = [];
|
||||
lines.push([
|
||||
@@ -409,6 +413,17 @@ export function renderTraceReport(
|
||||
`outcome=${stringValue(summary.outcome) ?? "-"}`,
|
||||
`reason=${stringValue(summary.reason) ?? "-"}`,
|
||||
].join(" "));
|
||||
lines.push([
|
||||
"SOURCE",
|
||||
`target=${textValue(parsed.targetId)}`,
|
||||
`runtime=${textValue(parsed.runtimeMode)}`,
|
||||
`logs=${textValue(logs.source)}`,
|
||||
`context=${textValue(logs.contextSource)}`,
|
||||
`exit=${textValue(logs.exitCode)}`,
|
||||
].join(" "));
|
||||
if (parsed.ok !== true && stringValue(logs.stderrTail) !== null) {
|
||||
lines.push(`LOG ERROR ${shorten(stringValue(logs.stderrTail) ?? "", 240)}`);
|
||||
}
|
||||
lines.push([
|
||||
"REQUEST",
|
||||
`path=${request.path ?? final.path ?? "-"}`,
|
||||
@@ -426,13 +441,23 @@ export function renderTraceReport(
|
||||
`events=${textValue(summary.eventCount)}`,
|
||||
`window=${window.beforeSeconds ?? "?"}s/${window.afterSeconds ?? "?"}s`,
|
||||
].join(" "));
|
||||
lines.push(renderTable([
|
||||
["TIME_REF", "FIRST_FAILOVER_MS", "SELECT_FAILED_MS", "FINAL_MS", "FAILOVER_TO_FINAL_MS", "CLIENT_DEADLINE", "REMAINING_MS"],
|
||||
[
|
||||
textValue(timing.referenceSource), textValue(timing.firstFailoverElapsedMs), textValue(timing.firstSelectFailedElapsedMs),
|
||||
textValue(timing.finalElapsedMs), textValue(timing.failoverToFinalMs), timing.clientDeadlineAvailable === true ? "available" : "unavailable",
|
||||
textValue(timing.clientDeadlineRemainingMs),
|
||||
],
|
||||
]));
|
||||
if (typeof timing.attribution === "string") lines.push(`NOTE: ${timing.attribution}`);
|
||||
if (failovers.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("FAILOVER");
|
||||
lines.push(renderTable([
|
||||
["AT", "ACCOUNT", "UPSTREAM", "SWITCH", "MAX"],
|
||||
["AT", "ELAPSED_MS", "ACCOUNT", "UPSTREAM", "SWITCH", "MAX"],
|
||||
...failovers.map((item) => [
|
||||
shortIso(item.at),
|
||||
textValue(item.elapsedMs),
|
||||
formatAccountRef(item),
|
||||
textValue(item.upstreamStatus),
|
||||
textValue(item.switchCount),
|
||||
@@ -444,14 +469,29 @@ export function renderTraceReport(
|
||||
lines.push("");
|
||||
lines.push("SELECT-FAILED");
|
||||
lines.push(renderTable([
|
||||
["AT", "ERROR", "EXCLUDED"],
|
||||
["AT", "ELAPSED_MS", "ERROR", "EXCLUDED"],
|
||||
...selectFailures.map((item) => [
|
||||
shortIso(item.at),
|
||||
textValue(item.elapsedMs),
|
||||
shorten(stringValue(item.error) ?? "-", 56),
|
||||
textValue(item.excludedAccountCount),
|
||||
]),
|
||||
]));
|
||||
}
|
||||
if (forwardFailures.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("FORWARD-FAILED");
|
||||
lines.push(renderTable([
|
||||
["AT", "ELAPSED_MS", "ACCOUNT", "STATUS", "ERROR"],
|
||||
...forwardFailures.map((item) => [
|
||||
shortIso(item.at),
|
||||
textValue(item.elapsedMs),
|
||||
formatAccountRef(item),
|
||||
textValue(item.statusCode),
|
||||
shorten(stringValue(item.error) ?? "-", 72),
|
||||
]),
|
||||
]));
|
||||
}
|
||||
if (upstreamErrors.length > 0 || tempUnschedulable.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("ACCOUNT SIGNALS");
|
||||
@@ -482,13 +522,14 @@ export function renderTraceReport(
|
||||
lines.push("");
|
||||
lines.push("WINDOW STATS");
|
||||
lines.push(renderTable([
|
||||
["MATCH", "EVENTS", "FINAL_4XX_5XX", "FAILOVER", "SELECT_FAIL", "TEMP_UNSCHED", "ADMIN_SCHED"],
|
||||
["MATCH", "EVENTS", "FINAL_4XX_5XX", "FAILOVER", "SELECT_FAIL", "FORWARD_FAIL", "TEMP_UNSCHED", "ADMIN_SCHED"],
|
||||
[
|
||||
textValue(windowStats.matchedLines),
|
||||
textValue(windowStats.eventCount),
|
||||
textValue(windowStats.finalErrorCount),
|
||||
textValue(windowStats.failoverCount),
|
||||
textValue(windowStats.selectFailedCount),
|
||||
textValue(windowStats.forwardFailedCount),
|
||||
textValue(windowStats.tempUnschedulableCount),
|
||||
textValue(windowStats.adminSchedulableCount),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { defaultCodexPoolRuntimeTargetId } from "./runtime-target";
|
||||
|
||||
export type RuntimeAction = "list" | "get" | "errors" | "infrastructure" | "apply" | "delete";
|
||||
|
||||
export interface RuntimeOptions {
|
||||
action: RuntimeAction;
|
||||
account: string | null;
|
||||
accounts: string[];
|
||||
group: string | null;
|
||||
allGroups: boolean;
|
||||
platform: string | null;
|
||||
pageToken: string | null;
|
||||
template: string | null;
|
||||
kind: "temp-unschedulable";
|
||||
confirm: boolean;
|
||||
full: boolean;
|
||||
raw: boolean;
|
||||
json: boolean;
|
||||
since: string;
|
||||
tail: number;
|
||||
targetId: string;
|
||||
}
|
||||
|
||||
export function parseRuntimeOptions(args: string[]): RuntimeOptions {
|
||||
const [actionRaw = "list", ...rest] = args;
|
||||
if (!(["list", "get", "errors", "infrastructure", "apply", "delete"] as string[]).includes(actionRaw)) {
|
||||
throw new Error("runtime usage: list|get|errors|infrastructure|apply|delete [--account <name-or-id>|--accounts <selectors>] [--group <name-or-id>|--all-groups] [--platform <name>] [--page-token <token>] [--since 24h] [--tail 50000] [--template <id>] [--kind temp-unschedulable] [--confirm] [--target <id>] [--json|--full|--raw]");
|
||||
}
|
||||
let account: string | null = null;
|
||||
let accounts: string[] = [];
|
||||
let group: string | null = null;
|
||||
let allGroups = false;
|
||||
let platform: string | null = null;
|
||||
let pageToken: string | null = null;
|
||||
let template: string | null = null;
|
||||
let kind = "temp-unschedulable" as const;
|
||||
let confirm = false;
|
||||
let full = false;
|
||||
let raw = false;
|
||||
let json = false;
|
||||
let since = "24h";
|
||||
let tail = 50_000;
|
||||
let targetId = defaultCodexPoolRuntimeTargetId();
|
||||
for (let index = 0; index < rest.length; index += 1) {
|
||||
const arg = rest[index]!;
|
||||
const readValue = (name: string): string => {
|
||||
const value = rest[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
||||
index += 1;
|
||||
return value.trim();
|
||||
};
|
||||
if (arg === "--confirm") confirm = true;
|
||||
else if (arg === "--full") full = true;
|
||||
else if (arg === "--raw") raw = true;
|
||||
else if (arg === "--json") json = true;
|
||||
else if (arg === "-o" && readValue("-o") === "json") json = true;
|
||||
else if (arg === "--account") account = readValue("--account");
|
||||
else if (arg.startsWith("--account=")) account = arg.slice("--account=".length).trim();
|
||||
else if (arg === "--accounts") accounts = parseAccountSelectors(readValue("--accounts"));
|
||||
else if (arg.startsWith("--accounts=")) accounts = parseAccountSelectors(arg.slice("--accounts=".length));
|
||||
else if (arg === "--group") group = readValue("--group");
|
||||
else if (arg.startsWith("--group=")) group = arg.slice("--group=".length).trim();
|
||||
else if (arg === "--all-groups") allGroups = true;
|
||||
else if (arg === "--platform") platform = readValue("--platform");
|
||||
else if (arg.startsWith("--platform=")) platform = arg.slice("--platform=".length).trim();
|
||||
else if (arg === "--page-token") pageToken = readValue("--page-token");
|
||||
else if (arg.startsWith("--page-token=")) pageToken = arg.slice("--page-token=".length).trim();
|
||||
else if (arg === "--template") template = readValue("--template");
|
||||
else if (arg.startsWith("--template=")) template = arg.slice("--template=".length).trim();
|
||||
else if (arg === "--kind") kind = parseRuntimeKind(readValue("--kind"));
|
||||
else if (arg.startsWith("--kind=")) kind = parseRuntimeKind(arg.slice("--kind=".length));
|
||||
else if (arg === "--target") targetId = readValue("--target");
|
||||
else if (arg.startsWith("--target=")) targetId = arg.slice("--target=".length).trim();
|
||||
else if (arg === "--since") since = readValue("--since");
|
||||
else if (arg.startsWith("--since=")) since = arg.slice("--since=".length).trim();
|
||||
else if (arg === "--tail") tail = parseRuntimeTail(readValue("--tail"));
|
||||
else if (arg.startsWith("--tail=")) tail = parseRuntimeTail(arg.slice("--tail=".length));
|
||||
else throw new Error(`unsupported runtime option: ${arg}`);
|
||||
}
|
||||
const action = actionRaw as RuntimeAction;
|
||||
if (account !== null && accounts.length > 0) throw new Error("use only one of --account or --accounts");
|
||||
if ((action === "get" || action === "infrastructure") && !account) throw new Error(`runtime ${action} requires --account <name-or-id>`);
|
||||
if ((action === "apply" || action === "delete") && account === null && accounts.length === 0) throw new Error(`runtime ${action} requires --account or --accounts`);
|
||||
if (accounts.length > 0 && action !== "apply" && action !== "delete") throw new Error(`runtime ${action} does not accept --accounts`);
|
||||
if (account !== null && (account.length > 256 || /[\r\n]/u.test(account))) throw new Error("--account has an unsupported format");
|
||||
if (group !== null && (group.length > 256 || /[\r\n]/u.test(group))) throw new Error("--group has an unsupported format");
|
||||
if (platform !== null && (!/^[A-Za-z0-9._-]+$/u.test(platform) || platform.length > 64)) throw new Error("--platform has an unsupported format");
|
||||
if (pageToken !== null && (!/^\d+$/u.test(pageToken) || pageToken.length > 20)) throw new Error("--page-token must be a numeric group cursor");
|
||||
if (group !== null && allGroups) throw new Error("use only one of --group or --all-groups");
|
||||
if ((allGroups || pageToken !== null) && action !== "errors") throw new Error(`runtime ${action} does not accept all-groups pagination options`);
|
||||
if ((group !== null || platform !== null) && action !== "errors" && action !== "infrastructure") throw new Error(`runtime ${action} does not accept group scope options`);
|
||||
if (pageToken !== null && !allGroups) throw new Error("--page-token requires --all-groups");
|
||||
if (account !== null && allGroups) throw new Error("--account cannot be combined with --all-groups; use --group first");
|
||||
if (action === "apply" && !template) throw new Error("runtime apply requires --template <id>");
|
||||
if (action !== "apply" && template !== null) throw new Error(`runtime ${action} does not accept --template`);
|
||||
if (action !== "errors" && action !== "infrastructure" && (since !== "24h" || tail !== 50_000)) throw new Error(`runtime ${action} does not accept --since or --tail`);
|
||||
if (!/^\d+[mhd]$/u.test(since)) throw new Error("--since must use <number>m, <number>h, or <number>d");
|
||||
if (["list", "get", "errors", "infrastructure"].includes(action) && confirm) throw new Error(`runtime ${action} does not accept --confirm`);
|
||||
if ([full, raw, json].filter(Boolean).length > 1) throw new Error("use only one of --json, --full, or --raw");
|
||||
return { action, account, accounts, group, allGroups, platform, pageToken, template, kind, confirm, full, raw, json, since, tail, targetId };
|
||||
}
|
||||
|
||||
function parseAccountSelectors(value: string): string[] {
|
||||
const selectors = value.split(",").map((item) => item.trim()).filter(Boolean);
|
||||
if (selectors.length === 0 || selectors.length > 100) throw new Error("--accounts requires 1 to 100 comma-separated selectors");
|
||||
if (selectors.some((item) => item.length > 256 || /[\r\n]/u.test(item))) throw new Error("--accounts has an unsupported selector");
|
||||
if (new Set(selectors).size !== selectors.length) throw new Error("--accounts contains duplicate selectors");
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function parseRuntimeKind(value: string): "temp-unschedulable" {
|
||||
if (value !== "temp-unschedulable") throw new Error("--kind must be temp-unschedulable");
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseRuntimeTail(value: string): number {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isInteger(parsed) || parsed < 100 || parsed > 200_000) throw new Error("--tail must be an integer from 100 to 200000");
|
||||
return parsed;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -357,9 +357,9 @@ export function codexPoolHelp(): unknown {
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool plan --target D601",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool sync [--target D601] --confirm [--prune-removed]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool validate [--target D601] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime list [--target PK01] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime get --account <name-or-id> [--target PK01] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors [--account <name-or-id>] [--since 24h] [--tail 50000] [--target PK01] [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime list [--target PK01] [--json|--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime get --account <name-or-id> [--target PK01] [--json|--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors [--group <name-or-id>|--all-groups] [--platform <name>] [--page-token <group-id>] [--account <name-or-id>] [--since 24h] [--tail 50000] [--target PK01] [--json|--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool faults [--level P0|P1|P2] [--group <name-or-id>] [--account <name-or-id>] [--model <model>] [--stream sync|stream] [--endpoint <path>] [--request-id <id>] [--page-token <token>] [--target PK01] [--json]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --account <name-or-id> --template <id> [--target PK01] [--confirm]",
|
||||
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --accounts <name-or-id,...> --template <id> [--target PK01] [--confirm] [--full|--raw]",
|
||||
|
||||
Reference in New Issue
Block a user