Files
openclaw/scripts/e2e/lib/openai-chat-tools/client.mjs
2026-05-27 05:58:15 +02:00

138 lines
3.9 KiB
JavaScript

const port = process.env.PORT;
const token = process.env.OPENCLAW_GATEWAY_TOKEN;
const backendModel = process.env.MODEL_REF || "openai/gpt-5.4-mini";
const timeoutSeconds = Number.parseInt(
process.env.OPENCLAW_OPENAI_CHAT_TOOLS_TIMEOUT_SECONDS ?? "180",
10,
);
const maxBodyBytes = Number.parseInt(
process.env.OPENCLAW_OPENAI_CHAT_TOOLS_MAX_BODY_BYTES ?? "1048576",
10,
);
if (!port || !token) {
throw new Error("missing PORT/OPENCLAW_GATEWAY_TOKEN");
}
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
throw new Error(`invalid OPENCLAW_OPENAI_CHAT_TOOLS_TIMEOUT_SECONDS: ${timeoutSeconds}`);
}
if (!Number.isFinite(maxBodyBytes) || maxBodyBytes <= 0) {
throw new Error(`invalid OPENCLAW_OPENAI_CHAT_TOOLS_MAX_BODY_BYTES: ${maxBodyBytes}`);
}
async function readBoundedResponseText(response, byteLimit) {
const reader = response.body?.getReader();
if (!reader) {
return "";
}
const chunks = [];
let totalBytes = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) {
break;
}
totalBytes += value.byteLength;
if (totalBytes > byteLimit) {
await reader.cancel();
throw new Error(`chat completions response body exceeded ${byteLimit} bytes`);
}
chunks.push(Buffer.from(value));
}
return Buffer.concat(chunks, totalBytes).toString("utf8");
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
const started = Date.now();
let response;
let text;
try {
response = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
method: "POST",
headers: {
authorization: `Bearer ${token}`,
"content-type": "application/json",
"x-openclaw-model": backendModel,
},
body: JSON.stringify({
model: "openclaw",
stream: false,
messages: [
{
role: "user",
content:
"Use the get_weather tool exactly once for Paris, France. Return the tool call only.",
},
],
tool_choice: "auto",
tools: [
{
type: "function",
function: {
name: "get_weather",
description: "Return weather for a city.",
strict: true,
parameters: {
type: "object",
additionalProperties: false,
properties: {
city: { type: "string", description: "City and country." },
},
required: ["city"],
},
},
},
],
}),
signal: controller.signal,
});
text = await readBoundedResponseText(response, maxBodyBytes);
} finally {
clearTimeout(timeout);
}
let body;
try {
body = text ? JSON.parse(text) : {};
} catch {
throw new Error(`non-JSON response ${response.status}: ${text}`);
}
if (!response.ok) {
throw new Error(`chat completions request failed ${response.status}: ${JSON.stringify(body)}`);
}
const choice = body.choices?.[0];
const toolCalls = choice?.message?.tool_calls;
if (choice?.finish_reason !== "tool_calls") {
throw new Error(`expected finish_reason tool_calls: ${JSON.stringify(body)}`);
}
if (!Array.isArray(toolCalls) || toolCalls.length !== 1) {
throw new Error(`expected exactly one tool call: ${JSON.stringify(body)}`);
}
const [toolCall] = toolCalls;
if (toolCall?.type !== "function" || toolCall?.function?.name !== "get_weather") {
throw new Error(`unexpected tool call: ${JSON.stringify(toolCall)}`);
}
let args = {};
try {
args = JSON.parse(toolCall.function.arguments || "{}");
} catch {
throw new Error(`tool arguments were not valid JSON: ${toolCall.function.arguments}`);
}
if (typeof args.city !== "string" || !/paris/i.test(args.city)) {
throw new Error(`expected Paris city argument: ${JSON.stringify(args)}`);
}
console.log(
JSON.stringify({
ok: true,
elapsedMs: Date.now() - started,
finishReason: choice.finish_reason,
toolName: toolCall.function.name,
args,
}),
);