mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 16:45:01 +00:00
chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
@@ -5,10 +5,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||
import {
|
||||
discoverAuthStorage,
|
||||
discoverModels,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent";
|
||||
import { describe, it } from "vitest";
|
||||
import { resolveClawdbotAgentDir } from "../agents/agent-paths.js";
|
||||
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
@@ -27,10 +24,7 @@ import { getApiKeyForModel } from "../agents/model-auth.js";
|
||||
import { ensureClawdbotModelsJson } from "../agents/models-config.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import type { ClawdbotConfig, ModelProviderConfig } from "../config/types.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-channel.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||
import { GatewayClient } from "./client.js";
|
||||
import { renderCatNoncePngBase64 } from "./live-image-probe.js";
|
||||
import { startGatewayServer } from "./server.js";
|
||||
@@ -67,8 +61,7 @@ function assertNoReasoningTags(params: {
|
||||
}): void {
|
||||
if (!params.text) return;
|
||||
if (THINKING_TAG_RE.test(params.text) || FINAL_TAG_RE.test(params.text)) {
|
||||
const snippet =
|
||||
params.text.length > 200 ? `${params.text.slice(0, 200)}…` : params.text;
|
||||
const snippet = params.text.length > 200 ? `${params.text.slice(0, 200)}…` : params.text;
|
||||
throw new Error(
|
||||
`[${params.label}] reasoning tag leak (${params.model} / ${params.phase}): ${snippet}`,
|
||||
);
|
||||
@@ -79,11 +72,7 @@ function extractPayloadText(result: unknown): string {
|
||||
const record = result as Record<string, unknown>;
|
||||
const payloads = Array.isArray(record.payloads) ? record.payloads : [];
|
||||
const texts = payloads
|
||||
.map((p) =>
|
||||
p && typeof p === "object"
|
||||
? (p as Record<string, unknown>).text
|
||||
: undefined,
|
||||
)
|
||||
.map((p) => (p && typeof p === "object" ? (p as Record<string, unknown>).text : undefined))
|
||||
.filter((t): t is string => typeof t === "string" && t.trim().length > 0);
|
||||
return texts.join("\n").trim();
|
||||
}
|
||||
@@ -196,9 +185,9 @@ async function getFreeGatewayPort(): Promise<number> {
|
||||
for (let attempt = 0; attempt < 25; attempt += 1) {
|
||||
const port = await getFreePort();
|
||||
const candidates = [port, port + 1, port + 2, port + 4];
|
||||
const ok = (
|
||||
await Promise.all(candidates.map((candidate) => isPortFree(candidate)))
|
||||
).every(Boolean);
|
||||
const ok = (await Promise.all(candidates.map((candidate) => isPortFree(candidate)))).every(
|
||||
Boolean,
|
||||
);
|
||||
if (ok) return port;
|
||||
}
|
||||
throw new Error("failed to acquire a free gateway port block");
|
||||
@@ -231,10 +220,7 @@ async function connectClient(params: { url: string; token: string }) {
|
||||
onClose: (code, reason) =>
|
||||
stop(new Error(`gateway closed during connect (${code}): ${reason}`)),
|
||||
});
|
||||
const timer = setTimeout(
|
||||
() => stop(new Error("gateway connect timeout")),
|
||||
10_000,
|
||||
);
|
||||
const timer = setTimeout(() => stop(new Error("gateway connect timeout")), 10_000);
|
||||
timer.unref();
|
||||
client.start();
|
||||
});
|
||||
@@ -270,8 +256,7 @@ function buildLiveGatewayConfig(params: {
|
||||
: {}),
|
||||
...providerOverrides,
|
||||
};
|
||||
const providers =
|
||||
Object.keys(nextProviders).length > 0 ? nextProviders : baseProviders;
|
||||
const providers = Object.keys(nextProviders).length > 0 ? nextProviders : baseProviders;
|
||||
return {
|
||||
...params.cfg,
|
||||
agents: {
|
||||
@@ -285,15 +270,11 @@ function buildLiveGatewayConfig(params: {
|
||||
// Live tests should avoid Docker sandboxing so tool probes can
|
||||
// operate on the temporary probe files we create in the host workspace.
|
||||
sandbox: { mode: "off" },
|
||||
models: Object.fromEntries(
|
||||
params.candidates.map((m) => [`${m.provider}/${m.id}`, {}]),
|
||||
),
|
||||
models: Object.fromEntries(params.candidates.map((m) => [`${m.provider}/${m.id}`, {}])),
|
||||
},
|
||||
},
|
||||
models:
|
||||
Object.keys(providers).length > 0
|
||||
? { ...params.cfg.models, providers }
|
||||
: params.cfg.models,
|
||||
Object.keys(providers).length > 0 ? { ...params.cfg.models, providers } : params.cfg.models,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -342,12 +323,7 @@ function buildMinimaxProviderOverride(params: {
|
||||
baseUrl: string;
|
||||
}): ModelProviderConfig | null {
|
||||
const existing = params.cfg.models?.providers?.minimax;
|
||||
if (
|
||||
!existing ||
|
||||
!Array.isArray(existing.models) ||
|
||||
existing.models.length === 0
|
||||
)
|
||||
return null;
|
||||
if (!existing || !Array.isArray(existing.models) || existing.models.length === 0) return null;
|
||||
return {
|
||||
...existing,
|
||||
api: params.api,
|
||||
@@ -392,9 +368,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
lastGood: hostStore.lastGood ? { ...hostStore.lastGood } : undefined,
|
||||
usageStats: hostStore.usageStats ? { ...hostStore.usageStats } : undefined,
|
||||
};
|
||||
tempStateDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "clawdbot-live-state-"),
|
||||
);
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-live-state-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
tempAgentDir = path.join(tempStateDir, "agents", "main", "agent");
|
||||
saveAuthProfileStore(sanitizedStore, tempAgentDir);
|
||||
@@ -405,10 +379,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
const nonceA = randomUUID();
|
||||
const nonceB = randomUUID();
|
||||
const toolProbePath = path.join(
|
||||
workspaceDir,
|
||||
`.clawdbot-live-tool-probe.${nonceA}.txt`,
|
||||
);
|
||||
const toolProbePath = path.join(workspaceDir, `.clawdbot-live-tool-probe.${nonceA}.txt`);
|
||||
await fs.writeFile(toolProbePath, `nonceA=${nonceA}\nnonceB=${nonceB}\n`);
|
||||
|
||||
const agentDir = resolveClawdbotAgentDir();
|
||||
@@ -447,9 +418,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
const anthropicKeys = collectAnthropicApiKeys();
|
||||
if (anthropicKeys.length > 0) {
|
||||
process.env.ANTHROPIC_API_KEY = anthropicKeys[0];
|
||||
logProgress(
|
||||
`[${params.label}] anthropic keys loaded: ${anthropicKeys.length}`,
|
||||
);
|
||||
logProgress(`[${params.label}] anthropic keys loaded: ${anthropicKeys.length}`);
|
||||
}
|
||||
const sessionKey = `agent:${agentId}:${params.label}`;
|
||||
const failures: Array<{ model: string; error: string }> = [];
|
||||
@@ -461,9 +430,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
const progressLabel = `[${params.label}] ${index + 1}/${total} ${modelKey}`;
|
||||
|
||||
const attemptMax =
|
||||
model.provider === "anthropic" && anthropicKeys.length > 0
|
||||
? anthropicKeys.length
|
||||
: 1;
|
||||
model.provider === "anthropic" && anthropicKeys.length > 0 ? anthropicKeys.length : 1;
|
||||
|
||||
for (let attempt = 0; attempt < attemptMax; attempt += 1) {
|
||||
if (model.provider === "anthropic" && anthropicKeys.length > 0) {
|
||||
@@ -504,9 +471,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
isEmptyStreamText(text) &&
|
||||
(model.provider === "minimax" || model.provider === "openai-codex")
|
||||
) {
|
||||
logProgress(
|
||||
`${progressLabel}: skip (${model.provider} empty response)`,
|
||||
);
|
||||
logProgress(`${progressLabel}: skip (${model.provider} empty response)`);
|
||||
break;
|
||||
}
|
||||
if (model.provider === "google" && isGoogleModelNotFoundText(text)) {
|
||||
@@ -522,10 +487,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
label: params.label,
|
||||
});
|
||||
if (!isMeaningful(text)) throw new Error(`not meaningful: ${text}`);
|
||||
if (
|
||||
!/\bmicro\s*-?\s*tasks?\b/i.test(text) ||
|
||||
!/\bmacro\s*-?\s*tasks?\b/i.test(text)
|
||||
) {
|
||||
if (!/\bmicro\s*-?\s*tasks?\b/i.test(text) || !/\bmacro\s*-?\s*tasks?\b/i.test(text)) {
|
||||
throw new Error(`missing required keywords: ${text}`);
|
||||
}
|
||||
|
||||
@@ -547,18 +509,14 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
{ expectFinal: true },
|
||||
);
|
||||
if (toolProbe?.status !== "ok") {
|
||||
throw new Error(
|
||||
`tool probe failed: status=${String(toolProbe?.status)}`,
|
||||
);
|
||||
throw new Error(`tool probe failed: status=${String(toolProbe?.status)}`);
|
||||
}
|
||||
const toolText = extractPayloadText(toolProbe?.result);
|
||||
if (
|
||||
isEmptyStreamText(toolText) &&
|
||||
(model.provider === "minimax" || model.provider === "openai-codex")
|
||||
) {
|
||||
logProgress(
|
||||
`${progressLabel}: skip (${model.provider} empty response)`,
|
||||
);
|
||||
logProgress(`${progressLabel}: skip (${model.provider} empty response)`);
|
||||
break;
|
||||
}
|
||||
assertNoReasoningTags({
|
||||
@@ -593,19 +551,14 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
{ expectFinal: true },
|
||||
);
|
||||
if (execReadProbe?.status !== "ok") {
|
||||
throw new Error(
|
||||
`exec+read probe failed: status=${String(execReadProbe?.status)}`,
|
||||
);
|
||||
throw new Error(`exec+read probe failed: status=${String(execReadProbe?.status)}`);
|
||||
}
|
||||
const execReadText = extractPayloadText(execReadProbe?.result);
|
||||
if (
|
||||
isEmptyStreamText(execReadText) &&
|
||||
(model.provider === "minimax" ||
|
||||
model.provider === "openai-codex")
|
||||
(model.provider === "minimax" || model.provider === "openai-codex")
|
||||
) {
|
||||
logProgress(
|
||||
`${progressLabel}: skip (${model.provider} empty response)`,
|
||||
);
|
||||
logProgress(`${progressLabel}: skip (${model.provider} empty response)`);
|
||||
break;
|
||||
}
|
||||
assertNoReasoningTags({
|
||||
@@ -652,19 +605,14 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
// Best-effort: do not fail the whole live suite on flaky image handling.
|
||||
// (We still keep prompt + tool probes as hard checks.)
|
||||
if (imageProbe?.status !== "ok") {
|
||||
logProgress(
|
||||
`${progressLabel}: image skip (status=${String(imageProbe?.status)})`,
|
||||
);
|
||||
logProgress(`${progressLabel}: image skip (status=${String(imageProbe?.status)})`);
|
||||
} else {
|
||||
const imageText = extractPayloadText(imageProbe?.result);
|
||||
if (
|
||||
isEmptyStreamText(imageText) &&
|
||||
(model.provider === "minimax" ||
|
||||
model.provider === "openai-codex")
|
||||
(model.provider === "minimax" || model.provider === "openai-codex")
|
||||
) {
|
||||
logProgress(
|
||||
`${progressLabel}: image skip (${model.provider} empty response)`,
|
||||
);
|
||||
logProgress(`${progressLabel}: image skip (${model.provider} empty response)`);
|
||||
} else {
|
||||
assertNoReasoningTags({
|
||||
text: imageText,
|
||||
@@ -675,11 +623,9 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
if (!/\bcat\b/i.test(imageText)) {
|
||||
logProgress(`${progressLabel}: image skip (missing 'cat')`);
|
||||
} else {
|
||||
const candidates =
|
||||
imageText.toUpperCase().match(/[A-Z0-9]{6,20}/g) ?? [];
|
||||
const candidates = imageText.toUpperCase().match(/[A-Z0-9]{6,20}/g) ?? [];
|
||||
const bestDistance = candidates.reduce((best, cand) => {
|
||||
if (Math.abs(cand.length - imageCode.length) > 2)
|
||||
return best;
|
||||
if (Math.abs(cand.length - imageCode.length) > 2) return best;
|
||||
return Math.min(best, editDistance(cand, imageCode));
|
||||
}, Number.POSITIVE_INFINITY);
|
||||
// OCR / image-read flake: allow a small edit distance, but still require the "cat" token above.
|
||||
@@ -694,8 +640,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
// Regression: tool-call-only turn followed by a user message (OpenAI responses bug class).
|
||||
if (
|
||||
(model.provider === "openai" && model.api === "openai-responses") ||
|
||||
(model.provider === "openai-codex" &&
|
||||
model.api === "openai-codex-responses")
|
||||
(model.provider === "openai-codex" && model.api === "openai-codex-responses")
|
||||
) {
|
||||
logProgress(`${progressLabel}: tool-only regression`);
|
||||
const runId2 = randomUUID();
|
||||
@@ -711,9 +656,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
{ expectFinal: true },
|
||||
);
|
||||
if (first?.status !== "ok") {
|
||||
throw new Error(
|
||||
`tool-only turn failed: status=${String(first?.status)}`,
|
||||
);
|
||||
throw new Error(`tool-only turn failed: status=${String(first?.status)}`);
|
||||
}
|
||||
const firstText = extractPayloadText(first?.result);
|
||||
assertNoReasoningTags({
|
||||
@@ -735,9 +678,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
{ expectFinal: true },
|
||||
);
|
||||
if (second?.status !== "ok") {
|
||||
throw new Error(
|
||||
`post-tool message failed: status=${String(second?.status)}`,
|
||||
);
|
||||
throw new Error(`post-tool message failed: status=${String(second?.status)}`);
|
||||
}
|
||||
const reply = extractPayloadText(second?.result);
|
||||
assertNoReasoningTags({
|
||||
@@ -763,14 +704,9 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
logProgress(`${progressLabel}: rate limit, retrying with next key`);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
model.provider === "anthropic" &&
|
||||
isAnthropicBillingError(message)
|
||||
) {
|
||||
if (model.provider === "anthropic" && isAnthropicBillingError(message)) {
|
||||
if (attempt + 1 < attemptMax) {
|
||||
logProgress(
|
||||
`${progressLabel}: billing issue, retrying with next key`,
|
||||
);
|
||||
logProgress(`${progressLabel}: billing issue, retrying with next key`);
|
||||
continue;
|
||||
}
|
||||
logProgress(`${progressLabel}: skip (anthropic billing)`);
|
||||
@@ -781,9 +717,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
isEmptyStreamText(message) &&
|
||||
attempt + 1 < attemptMax
|
||||
) {
|
||||
logProgress(
|
||||
`${progressLabel}: empty response, retrying with next key`,
|
||||
);
|
||||
logProgress(`${progressLabel}: empty response, retrying with next key`);
|
||||
continue;
|
||||
}
|
||||
if (model.provider === "anthropic" && isEmptyStreamText(message)) {
|
||||
@@ -792,10 +726,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
break;
|
||||
}
|
||||
// OpenAI Codex refresh tokens can become single-use; skip instead of failing all live tests.
|
||||
if (
|
||||
model.provider === "openai-codex" &&
|
||||
isRefreshTokenReused(message)
|
||||
) {
|
||||
if (model.provider === "openai-codex" && isRefreshTokenReused(message)) {
|
||||
logProgress(`${progressLabel}: skip (codex refresh token reused)`);
|
||||
break;
|
||||
}
|
||||
@@ -821,9 +752,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
||||
.slice(0, 20)
|
||||
.map((f) => `- ${f.model}: ${f.error}`)
|
||||
.join("\n");
|
||||
throw new Error(
|
||||
`gateway live model failures (${failures.length}):\n${preview}`,
|
||||
);
|
||||
throw new Error(`gateway live model failures (${failures.length}):\n${preview}`);
|
||||
}
|
||||
if (skippedCount === total) {
|
||||
logProgress(`[${params.label}] skipped all models (missing profiles)`);
|
||||
@@ -868,15 +797,12 @@ describeLive("gateway live (dev agent, profile keys)", () => {
|
||||
const all = modelRegistry.getAll() as Array<Model<Api>>;
|
||||
|
||||
const rawModels = process.env.CLAWDBOT_LIVE_GATEWAY_MODELS?.trim();
|
||||
const useModern =
|
||||
!rawModels || rawModels === "modern" || rawModels === "all";
|
||||
const useModern = !rawModels || rawModels === "modern" || rawModels === "all";
|
||||
const useExplicit = Boolean(rawModels) && !useModern;
|
||||
const filter = useExplicit ? parseFilter(rawModels) : null;
|
||||
const wanted = filter
|
||||
? all.filter((m) => filter.has(`${m.provider}/${m.id}`))
|
||||
: all.filter((m) =>
|
||||
isModernModelRef({ provider: m.provider, id: m.id }),
|
||||
);
|
||||
: all.filter((m) => isModernModelRef({ provider: m.provider, id: m.id }));
|
||||
|
||||
const candidates: Array<Model<Api>> = [];
|
||||
for (const model of wanted) {
|
||||
@@ -902,16 +828,10 @@ describeLive("gateway live (dev agent, profile keys)", () => {
|
||||
logProgress("[all-models] no API keys found; skipping");
|
||||
return;
|
||||
}
|
||||
logProgress(
|
||||
`[all-models] selection=${useExplicit ? "explicit" : "modern"}`,
|
||||
);
|
||||
const imageCandidates = candidates.filter((m) =>
|
||||
m.input?.includes("image"),
|
||||
);
|
||||
logProgress(`[all-models] selection=${useExplicit ? "explicit" : "modern"}`);
|
||||
const imageCandidates = candidates.filter((m) => m.input?.includes("image"));
|
||||
if (imageCandidates.length === 0) {
|
||||
logProgress(
|
||||
"[all-models] no image-capable models selected; image probe will be skipped",
|
||||
);
|
||||
logProgress("[all-models] no image-capable models selected; image probe will be skipped");
|
||||
}
|
||||
await runGatewayModelSuite({
|
||||
label: "all-models",
|
||||
@@ -922,13 +842,9 @@ describeLive("gateway live (dev agent, profile keys)", () => {
|
||||
thinkingLevel: THINKING_LEVEL,
|
||||
});
|
||||
|
||||
const minimaxCandidates = candidates.filter(
|
||||
(model) => model.provider === "minimax",
|
||||
);
|
||||
const minimaxCandidates = candidates.filter((model) => model.provider === "minimax");
|
||||
if (minimaxCandidates.length === 0) {
|
||||
logProgress(
|
||||
"[minimax] no candidates with keys; skipping dual endpoint probes",
|
||||
);
|
||||
logProgress("[minimax] no candidates with keys; skipping dual endpoint probes");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -948,9 +864,7 @@ describeLive("gateway live (dev agent, profile keys)", () => {
|
||||
providerOverrides: { minimax: minimaxAnthropic },
|
||||
});
|
||||
} else {
|
||||
logProgress(
|
||||
"[minimax-anthropic] missing minimax provider config; skipping",
|
||||
);
|
||||
logProgress("[minimax-anthropic] missing minimax provider config; skipping");
|
||||
}
|
||||
},
|
||||
20 * 60 * 1000,
|
||||
@@ -981,10 +895,7 @@ describeLive("gateway live (dev agent, profile keys)", () => {
|
||||
const agentDir = resolveClawdbotAgentDir();
|
||||
const authStorage = discoverAuthStorage(agentDir);
|
||||
const modelRegistry = discoverModels(authStorage, agentDir);
|
||||
const anthropic = modelRegistry.find(
|
||||
"anthropic",
|
||||
"claude-opus-4-5",
|
||||
) as Model<Api> | null;
|
||||
const anthropic = modelRegistry.find("anthropic", "claude-opus-4-5") as Model<Api> | null;
|
||||
const zai = modelRegistry.find("zai", "glm-4.7") as Model<Api> | null;
|
||||
|
||||
if (!anthropic || !zai) return;
|
||||
@@ -1000,10 +911,7 @@ describeLive("gateway live (dev agent, profile keys)", () => {
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
const nonceA = randomUUID();
|
||||
const nonceB = randomUUID();
|
||||
const toolProbePath = path.join(
|
||||
workspaceDir,
|
||||
`.clawdbot-live-zai-fallback.${nonceA}.txt`,
|
||||
);
|
||||
const toolProbePath = path.join(workspaceDir, `.clawdbot-live-zai-fallback.${nonceA}.txt`);
|
||||
await fs.writeFile(toolProbePath, `nonceA=${nonceA}\nnonceB=${nonceB}\n`);
|
||||
|
||||
const port = await getFreeGatewayPort();
|
||||
@@ -1044,9 +952,7 @@ describeLive("gateway live (dev agent, profile keys)", () => {
|
||||
{ expectFinal: true },
|
||||
);
|
||||
if (toolProbe?.status !== "ok") {
|
||||
throw new Error(
|
||||
`anthropic tool probe failed: status=${String(toolProbe?.status)}`,
|
||||
);
|
||||
throw new Error(`anthropic tool probe failed: status=${String(toolProbe?.status)}`);
|
||||
}
|
||||
const toolText = extractPayloadText(toolProbe?.result);
|
||||
assertNoReasoningTags({
|
||||
@@ -1079,9 +985,7 @@ describeLive("gateway live (dev agent, profile keys)", () => {
|
||||
{ expectFinal: true },
|
||||
);
|
||||
if (followup?.status !== "ok") {
|
||||
throw new Error(
|
||||
`zai followup failed: status=${String(followup?.status)}`,
|
||||
);
|
||||
throw new Error(`zai followup failed: status=${String(followup?.status)}`);
|
||||
}
|
||||
const followupText = extractPayloadText(followup?.result);
|
||||
assertNoReasoningTags({
|
||||
|
||||
Reference in New Issue
Block a user