Problems fixed: 1. before_message_write treated empty content (isEmpty) as NO_REPLY. Tool-call-only assistant messages (thinking + toolCall, no text) had empty extracted text, causing false NO_REPLY detection. In single-agent channels this immediately set turn to dormant, blocking all subsequent responses for the entire model run. 2. Added explicit toolCall/tool_call/tool_use detection in before_message_write — skip turn processing for intermediate tool-call messages entirely. 3. no-reply-api/server.mjs: default model name changed from 'dirigent-no-reply-v1' to 'no-reply' to match the configured model id in openclaw.json, fixing model list discovery. Changes: - plugin/hooks/before-message-write.ts: toolCall detection + remove isEmpty - plugin/hooks/message-sent.ts: remove isEmpty from wasNoReply - no-reply-api/server.mjs: fix default model name - dist/dirigent/index.ts: same fixes applied to monolithic build - dist/no-reply-api/server.mjs: same model name fix
113 lines
2.8 KiB
JavaScript
113 lines
2.8 KiB
JavaScript
import http from "node:http";
|
|
|
|
const port = Number(process.env.PORT || 8787);
|
|
const modelName = process.env.NO_REPLY_MODEL || "no-reply";
|
|
const authToken = process.env.AUTH_TOKEN || "";
|
|
|
|
function sendJson(res, status, payload) {
|
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
res.end(JSON.stringify(payload));
|
|
}
|
|
|
|
function isAuthorized(req) {
|
|
if (!authToken) return true;
|
|
const header = req.headers.authorization || "";
|
|
return header === `Bearer ${authToken}`;
|
|
}
|
|
|
|
function noReplyChatCompletion(reqBody) {
|
|
return {
|
|
id: `chatcmpl_dirigent_${Date.now()}`,
|
|
object: "chat.completion",
|
|
created: Math.floor(Date.now() / 1000),
|
|
model: reqBody?.model || modelName,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
message: { role: "assistant", content: "NO_REPLY" },
|
|
finish_reason: "stop"
|
|
}
|
|
],
|
|
usage: { prompt_tokens: 0, completion_tokens: 1, total_tokens: 1 }
|
|
};
|
|
}
|
|
|
|
function noReplyResponses(reqBody) {
|
|
return {
|
|
id: `resp_dirigent_${Date.now()}`,
|
|
object: "response",
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
model: reqBody?.model || modelName,
|
|
output: [
|
|
{
|
|
type: "message",
|
|
role: "assistant",
|
|
content: [{ type: "output_text", text: "NO_REPLY" }]
|
|
}
|
|
],
|
|
usage: { input_tokens: 0, output_tokens: 1, total_tokens: 1 }
|
|
};
|
|
}
|
|
|
|
function listModels() {
|
|
return {
|
|
object: "list",
|
|
data: [
|
|
{
|
|
id: modelName,
|
|
object: "model",
|
|
created: Math.floor(Date.now() / 1000),
|
|
owned_by: "dirigent"
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
const server = http.createServer((req, res) => {
|
|
if (req.method === "GET" && req.url === "/health") {
|
|
return sendJson(res, 200, { ok: true, service: "dirigent-no-reply-api", model: modelName });
|
|
}
|
|
|
|
if (req.method === "GET" && req.url === "/v1/models") {
|
|
if (!isAuthorized(req)) return sendJson(res, 401, { error: "unauthorized" });
|
|
return sendJson(res, 200, listModels());
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return sendJson(res, 404, { error: "not_found" });
|
|
}
|
|
|
|
if (!isAuthorized(req)) {
|
|
return sendJson(res, 401, { error: "unauthorized" });
|
|
}
|
|
|
|
let body = "";
|
|
req.on("data", (chunk) => {
|
|
body += chunk;
|
|
if (body.length > 1_000_000) req.destroy();
|
|
});
|
|
|
|
req.on("end", () => {
|
|
let parsed = {};
|
|
try {
|
|
parsed = body ? JSON.parse(body) : {};
|
|
} catch {
|
|
return sendJson(res, 400, { error: "invalid_json" });
|
|
}
|
|
|
|
if (req.url === "/v1/chat/completions") {
|
|
return sendJson(res, 200, noReplyChatCompletion(parsed));
|
|
}
|
|
|
|
if (req.url === "/v1/responses") {
|
|
return sendJson(res, 200, noReplyResponses(parsed));
|
|
}
|
|
|
|
return sendJson(res, 404, { error: "not_found" });
|
|
});
|
|
});
|
|
|
|
server.listen(port, () => {
|
|
console.log(`[dirigent-no-reply-api] listening on :${port}`);
|
|
});
|