From 6d79ba6ac93da3ccf81e576feab88d168048525f Mon Sep 17 00:00:00 2001 From: Jeff Peng Date: Sat, 31 Oct 2026 11:52:50 +0800 Subject: [PATCH 1/2] fix: mark source-free IRAOP research incomplete Signed-off-by: Jeff Peng --- docs/installation.md | 14 ++ .../files/agent-documentation.tar.gz | Bin 31383 -> 31815 bytes .../openclaw-seed/extensions/iraop/index.ts | 48 ++++- .../tests/iraop-response.test.mjs | 165 ++++++++++++++++++ 4 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 helm/mosaic-stack/tests/iraop-response.test.mjs diff --git a/docs/installation.md b/docs/installation.md index 9e0334f9..5ddf38b2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -504,6 +504,20 @@ helm upgrade mosaic "$MOSAIC_CHART" \ unset NVIDIA_API_KEY IRAOP_API_KEY ``` +`iraop_query` requires a nonempty answer and at least one document reference with +a filename and positive page number. A response without that evidence returns +`status: "incomplete"` and an error event in the Research panel. Its unverified +answer is withheld from model-visible content; the full original MCP response +remains in the tool result's `details` in the OpenClaw session transcript. The +chat history API may omit `details`. OpenClaw assigns its transport-level +`isError` flag independently, so `isError: false` does not mean research passed. + +Missing sources can reflect an agent or model/tool execution problem even when +the PDF is indexed and direct search works. Check collection metadata, the +original PDF, actual search execution and citations separately. A source +reference alone does not establish that every claim or inline citation is +correct, or that a proposed cause matches the current cluster. + ### Hardware Agent The Hardware Agent reuses the Base Command Manager head address and `bcm-host-ssh-key` configured above. The chart exposes the required Base Command Manager lookup inside the cluster and generates its internal credentials. It uses NVDebug evidence and generic analysis. diff --git a/helm/mosaic-stack/files/agent-documentation.tar.gz b/helm/mosaic-stack/files/agent-documentation.tar.gz index 96008e29cd666400d223f9945a3bf947f79c4f82..f02af747e8b616d60092d93e6e45f359bfc66357 100644 GIT binary patch literal 31815 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 GxB&oRt#&*B literal 31383 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 diff --git a/helm/mosaic-stack/files/openclaw-seed/extensions/iraop/index.ts b/helm/mosaic-stack/files/openclaw-seed/extensions/iraop/index.ts index cc800b35..14fb47de 100644 --- a/helm/mosaic-stack/files/openclaw-seed/extensions/iraop/index.ts +++ b/helm/mosaic-stack/files/openclaw-seed/extensions/iraop/index.ts @@ -297,7 +297,7 @@ function toOpenClawResult(mcpResult: unknown) { .join("\n"); throw new Error(typeof text === "string" && text ? text : "iraop tool call reported isError"); } - return { content: r.content, details: mcpResult }; + return { content: [...r.content], details: mcpResult }; } const text = typeof mcpResult === "string" ? mcpResult : JSON.stringify(mcpResult, null, 2); return { content: [{ type: "text", text }], details: mcpResult }; @@ -316,6 +316,37 @@ function firstTextContent(result: { content?: unknown[] }) { .join("\n"); } +function queryEvidenceProblem(mcpResult: unknown): string | null { + const candidates: unknown[] = []; + if (mcpResult && typeof mcpResult === "object") { + const raw = mcpResult as Record; + candidates.push(raw.structuredContent, raw); + if (Array.isArray(raw.content)) { + for (const item of raw.content) { + if (!item || typeof item !== "object" || !("text" in item) || typeof item.text !== "string") continue; + try { candidates.push(JSON.parse(item.text)); } catch { /* Try the next content block. */ } + } + } + } else if (typeof mcpResult === "string") { + try { candidates.push(JSON.parse(mcpResult)); } catch { /* Report an unreadable research response below. */ } + } + const response = candidates.find((candidate) => + candidate && typeof candidate === "object" && !Array.isArray(candidate) + && "answer" in candidate, + ) as Record | undefined; + if (!response || typeof response.answer !== "string" || !response.answer.trim()) { + return "IRAOP did not return a readable, nonempty research answer with document sources."; + } + const hasSource = Array.isArray(response.sources) && response.sources.some((source) => + source && typeof source === "object" + && typeof source.source_file === "string" && source.source_file.trim() + && Number.isInteger(source.page_number) && source.page_number > 0, + ); + return hasSource ? null : "IRAOP returned no usable document sources. The research is incomplete. " + + "Do not present its answer as retrieved documentation or use it to justify a repair. " + + "Missing sources do not establish that a collection is empty."; +} + function summarizeQueryResult(result: ReturnType, elapsedMs: number) { const text = firstTextContent(result).trim(); const elapsed = Math.max(1, Math.round(elapsedMs / 1000)); @@ -398,6 +429,21 @@ export default definePluginEntry({ postSubagentEvent(subagent, "delta", message); }); const openClawResult = toOpenClawResult(result); + const evidenceProblem = queryEvidenceProblem(result); + if (evidenceProblem) { + postSubagentEvent(subagent, "error", `IRA research incomplete: ${evidenceProblem}`); + return { + // The tool transport completed. Research completion is explicit in + // content; returning normally also preserves the raw MCP details. + // OpenClaw assigns its own isError flag to the resulting message. + content: [{ type: "text", text: JSON.stringify({ + status: "incomplete", + error: evidenceProblem, + evidenceScope: "No document-grounded answer is available from this call. The full original response is retained in tool details for inspection.", + }) }], + details: result, + }; + } postSubagentEvent(subagent, "complete", summarizeQueryResult(openClawResult, Date.now() - startedAt)); openClawResult.content.unshift({ type: "text", text: "Evidence scope: documentation research. This service has not inspected the current workload. Proposed local causes remain hypotheses until checked against live evidence on the participating resources." }); return openClawResult; diff --git a/helm/mosaic-stack/tests/iraop-response.test.mjs b/helm/mosaic-stack/tests/iraop-response.test.mjs new file mode 100644 index 00000000..44b6d8fe --- /dev/null +++ b/helm/mosaic-stack/tests/iraop-response.test.mjs @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { createServer } from "node:http"; +import { stripTypeScriptTypes } from "node:module"; +import test from "node:test"; + +const source = readFileSync(new URL("../files/openclaw-seed/extensions/iraop/index.ts", import.meta.url), "utf8") + .replace('import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";', "const definePluginEntry = (entry) => entry;"); +const javascript = stripTypeScriptTypes(source, { mode: "strip", sourceUrl: "iraop-response-test.ts" }); +const { default: plugin } = await import(`data:text/javascript;base64,${Buffer.from(javascript).toString("base64")}`); + +const queryPayload = (answer, sources = []) => ({ + answer, depth: "quick", collections_searched: ["validation"], sources, +}); +const mcp = (payload) => ({ content: [{ type: "text", text: JSON.stringify(payload) }], isError: false }); +const sourceReference = { source_file: "control.pdf", page_number: 2, collection: "validation", doc_id: "control.pdf", url: "/api/v1/documents/control.pdf" }; + +async function fixture(result, run) { + const sessions = new Map(); + const events = []; + let nextSession = 0; + const server = createServer(async (req, res) => { + const url = new URL(req.url, "http://localhost"); + if (req.method === "GET" && url.pathname === "/mcp/sse") { + const id = String(++nextSession); + res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" }); + sessions.set(id, res); + res.write(`event: endpoint\ndata: /mcp/messages?session=${id}\n\n`); + req.on("close", () => sessions.delete(id)); + return; + } + let text = ""; + for await (const chunk of req) text += chunk; + const body = JSON.parse(text || "{}"); + if (url.pathname === "/events") { + events.push(body); + res.writeHead(200, { "Content-Type": "application/json" }).end('{"success":true}'); + return; + } + if (url.pathname === "/mcp/messages") { + const stream = sessions.get(url.searchParams.get("session")); + if (body.id !== undefined) { + const response = body.method === "initialize" + ? { protocolVersion: "2024-11-05", capabilities: {}, serverInfo: { name: "iraop-test", version: "1" } } + : result; + stream.write(`event: message\ndata: ${JSON.stringify({ jsonrpc: "2.0", id: body.id, result: response })}\n\n`); + } + res.writeHead(202).end(); + return; + } + res.writeHead(404).end(); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const base = `http://127.0.0.1:${server.address().port}`; + const tools = new Map(); + plugin.register({ + pluginConfig: { baseUrl: `${base}/mcp/sse`, timeoutMs: 3000, subagentEventsUrl: `${base}/events` }, + registerTool: (tool) => tools.set(tool.name, tool), + }); + try { + await run(tools, events); + for (let i = 0; i < 100 && !events.some((event) => ["complete", "error"].includes(event.phase)); i++) { + await new Promise((resolve) => setTimeout(resolve, 5)); + } + } finally { + for (const stream of sessions.values()) stream.end(); + server.closeAllConnections(); + await new Promise((resolve) => server.close(resolve)); + } + return events; +} + +async function query(result) { + let value; + const events = await fixture(result, async (tools) => { + value = await tools.get("iraop_query").execute("validation-call", { question: "Read the indexed control document.", collections: ["validation"] }); + }); + return { value, events }; +} + +test("planning text without sources is incomplete and emits an error event", async () => { + const original = mcp(queryPayload("I will search for the document.")); + const { value, events } = await query(original); + assert.equal(JSON.parse(value.content[0].text).status, "incomplete"); + assert.deepEqual(value.details, original); + assert.ok(events.some((event) => event.phase === "error" && event.content.includes("incomplete"))); + assert.ok(!events.some((event) => event.phase === "complete")); +}); + +test("fabricated source-free answer stays complete in details and is withheld from model content", async () => { + const answer = "Verified code: MVE-2026-1000002. " + "未经检索的原始输出。".repeat(600); + const original = mcp(queryPayload(answer)); + const { value } = await query(original); + assert.equal(JSON.parse(value.content[0].text).status, "incomplete"); + assert.ok(!value.content.map((item) => item.text).join("\n").includes("MVE-2026-1000002")); + assert.equal(JSON.parse(value.details.content[0].text).answer, answer); + assert.deepEqual(value.details, original); +}); + +for (const [label, sources] of [ + ["missing", undefined], ["null", null], ["wrong type", "control.pdf"], + ["empty reference", [{}]], ["invalid page", [{ ...sourceReference, page_number: 0 }]], +]) { + test(`research with ${label} sources is incomplete`, async () => { + const payload = queryPayload("A factual claim.", sources); + if (label === "missing") delete payload.sources; + const { value } = await query(mcp(payload)); + assert.equal(JSON.parse(value.content[0].text).status, "incomplete"); + assert.match(value.content[0].text, /incomplete/); + }); +} + +test("an empty answer is incomplete even when a source is present", async () => { + const { value } = await query(mcp(queryPayload(" ", [sourceReference]))); + assert.equal(JSON.parse(value.content[0].text).status, "incomplete"); +}); + +test("valid sourced answer remains complete and evidence notes do not mutate raw details", async () => { + const original = mcp(queryPayload("A documented statement. " + "完整原文。".repeat(700), [sourceReference])); + const { value, events } = await query(original); + assert.ok(!value.isError); + assert.deepEqual(value.details, original); + assert.equal(value.content.length, original.content.length + 1); + assert.notEqual(value.content, value.details.content); + assert.equal(value.content[1].text, original.content[0].text); + assert.ok(events.some((event) => event.phase === "complete")); + assert.ok(!events.some((event) => event.phase === "error")); +}); + +test("structured MCP query payload can carry the source references", async () => { + const payload = queryPayload("Documented answer.", [sourceReference]); + const original = { content: [{ type: "text", text: payload.answer }], structuredContent: payload }; + const { value } = await query(original); + assert.ok(!value.isError); + assert.deepEqual(value.details, original); +}); + +test("unreadable research payload is incomplete with the raw response retained", async () => { + const original = { content: [{ type: "text", text: "Unstructured unsupported response." }] }; + const { value } = await query(original); + assert.equal(JSON.parse(value.content[0].text).status, "incomplete"); + assert.deepEqual(value.details, original); +}); + +test("MCP error remains a rejected tool call and error event", async () => { + const original = { content: [{ type: "text", text: "Backend query failed." }], isError: true }; + const events = await fixture(original, async (tools) => { + await assert.rejects(tools.get("iraop_query").execute("error-call", { question: "Read the document." }), /Backend query failed/); + }); + assert.ok(events.some((event) => event.phase === "error")); + assert.ok(!events.some((event) => event.phase === "complete")); +}); + +test("collection listing does not require answer or sources fields", async () => { + const original = { content: [{ type: "text", text: JSON.stringify([{ name: "validation", doc_count: 4 }]) }] }; + await fixture(original, async (tools) => { + const value = await tools.get("iraop_list_collections").execute(); + assert.deepEqual(value.details, original); + assert.deepEqual(value.content, original.content); + assert.ok(!value.isError); + }); +}); From c3a2048bb65316c2663eb3d1c4a26d0a6373d6ab Mon Sep 17 00:00:00 2001 From: Jeff Peng Date: Sat, 31 Oct 2026 14:39:17 +0800 Subject: [PATCH 2/2] Fix opt-in research named tool calls through the LLM proxy Signed-off-by: Jeff Peng --- docs/installation.md | 41 +++ .../files/agent-documentation.tar.gz | Bin 31815 -> 32709 bytes helm/mosaic-stack/files/llm-compat-server.mjs | 228 +++++++++++++++++ helm/mosaic-stack/templates/llm-compat.yaml | 70 ++---- helm/mosaic-stack/tests/chart-render.test.mjs | 41 ++- helm/mosaic-stack/tests/llm-compat.test.mjs | 238 ++++++++++++++++++ helm/mosaic-stack/values.yaml | 5 + 7 files changed, 577 insertions(+), 46 deletions(-) create mode 100644 helm/mosaic-stack/files/llm-compat-server.mjs create mode 100644 helm/mosaic-stack/tests/llm-compat.test.mjs diff --git a/docs/installation.md b/docs/installation.md index 5ddf38b2..576747de 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -518,6 +518,47 @@ original PDF, actual search execution and citations separately. A source reference alone does not establish that every claim or inline citation is correct, or that a proposed cause matches the current cluster. +#### Optional research tool compatibility + +Some inference configurations return a named search tool's JSON arguments as +ordinary text, without an OpenAI `tool_calls` envelope. The research agent then +never executes the search. An opt-in workaround is available in the existing +LLM compatibility proxy: + +```bash +helm upgrade mosaic "$MOSAIC_CHART" \ + --devel -n mosaic --reuse-values \ + --set modules.research.enabled=true \ + --set llm.requestCompatibility.enabled=true \ + --set llm.requestCompatibility.researchToolCalls.enabled=true \ + --wait --timeout 12m +``` + +This setting is disabled by default. For an initial research request exposing +`task` and the supported query-only `opensearch_search` schema, it explicitly +selects that read-only search and disables thinking for this first generation. +It wraps only a complete, validated JSON argument object into the named tool +envelope; the original research agent must execute the actual search. Proper +upstream tool calls pass through. Explicit selection of another tool, synthesis +after a tool result, and unrelated OpenClaw requests retain their existing +behavior. Malformed or unfinished arguments return an error, never a fabricated +search result. Collection scope remains enforced by the research service. + +Use this workaround only after reproducing the protocol problem. For example, +the installed vLLM `Qwen3CoderToolParser` may declare named/required tool calls +unsupported; the checkpoint's documented parser configuration should also be +checked. This proxy setting does not change or restart the model server, and +does not certify document relevance, citation accuracy or general answer quality. + +For a controlled validation, additionally set +`llm.requestCompatibility.researchToolCalls.auditEnabled=true`. Full research +request bodies, raw upstream response bytes and returned response bytes are +saved as Base64 in `/audit/records` inside the proxy Pod. Authentication/cookie +headers are excluded. These traces can contain private prompts and documents; +export them to the authorized evidence location before replacing the Pod. +The volume is temporary and limited to 128 MiB; audit write failures fail the +request. Disable this tracing setting after the required evidence is exported. + ### Hardware Agent The Hardware Agent reuses the Base Command Manager head address and `bcm-host-ssh-key` configured above. The chart exposes the required Base Command Manager lookup inside the cluster and generates its internal credentials. It uses NVDebug evidence and generic analysis. diff --git a/helm/mosaic-stack/files/agent-documentation.tar.gz b/helm/mosaic-stack/files/agent-documentation.tar.gz index f02af747e8b616d60092d93e6e45f359bfc66357..278c1ec8f52e8aa9f4c808420d49a53d69729fdb 100644 GIT binary patch delta 31054 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 TpJVUrD}};AhB(L`D9C>R!rcTm delta 30106 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 { + const chunks = []; + let bytes = 0; + stream.on('data', chunk => { + bytes += chunk.length; + if (bytes > limit) { + reject(new Error('Research response exceeds the compatibility buffer limit.')); + stream.destroy(); + return; + } + chunks.push(chunk); + }); + stream.on('end', () => resolveBody(Buffer.concat(chunks))); + stream.on('error', reject); + stream.on('aborted', () => reject(new Error('Upstream response ended before completion.'))); + }); +} + +function searchSchema(tools) { + const tool = tools?.find?.(item => item.type === 'function' && item.function?.name === SEARCH); + const parameters = tool?.function?.parameters; + return parameters?.type === 'object' + && parameters.properties?.query?.type === 'string' + && Object.keys(parameters.properties).length === 1 + && parameters.required?.length === 1 && parameters.required[0] === 'query'; +} + +function prepare(method, path, raw, researchToolCalls) { + if (method !== 'POST' || !path.endsWith('/chat/completions')) return { body: raw }; + let json; + try { json = JSON.parse(raw.toString('utf8')); } catch { return { body: raw }; } + if (!json || typeof json !== 'object' || Array.isArray(json)) return { body: raw }; + json.reasoning_effort = 'none'; + const research = searchSchema(json.tools); + const named = json.tool_choice?.type === 'function' && json.tool_choice.function?.name === SEARCH; + const automatic = json.tool_choice == null || json.tool_choice === 'auto'; + const initial = json.messages?.at?.(-1)?.role === 'user' + && json.tools?.some?.(item => item.function?.name === 'task'); + const normalize = researchToolCalls && research && (json.n == null || json.n === 1) + && (named || (automatic && initial)); + const stream = json.stream === true; + if (normalize) { + json.tool_choice = { type: 'function', function: { name: SEARCH } }; + json.chat_template_kwargs = { ...json.chat_template_kwargs, enable_thinking: false }; + // Some affected providers discard named arguments in their nonstream path. + json.stream = true; + if (!stream) json.stream_options = { ...json.stream_options, include_usage: true }; + } + return { body: Buffer.from(JSON.stringify(json)), research, normalize, stream }; +} + +function decodeBody(raw, headers) { + const encoding = String(headers['content-encoding'] || 'identity').toLowerCase(); + if (encoding === 'gzip') raw = gunzipSync(raw); + else if (encoding === 'deflate') raw = inflateSync(raw); + else if (encoding === 'br') raw = brotliDecompressSync(raw); + else if (encoding !== 'identity') throw new Error('Unsupported upstream response encoding.'); + return textDecoder.decode(raw); +} + +function validArguments(text) { + let value; + try { value = JSON.parse(text); } catch { throw new Error('Named research tool arguments are not complete JSON.'); } + if (!value || typeof value !== 'object' || Array.isArray(value) + || Object.keys(value).length !== 1 || typeof value.query !== 'string' || !value.query.trim()) { + throw new Error('Named research tool arguments do not match the query-only schema.'); + } +} + +function normalizeResponse(raw, headers, stream) { + if (!String(headers['content-type']).includes('text/event-stream')) { + throw new Error('Expected a streaming named research response.'); + } + const events = []; + let done = false; + for (const frame of decodeBody(raw, headers).replace(/\r\n/g, '\n').split('\n\n')) { + const data = frame.split('\n').filter(line => line.startsWith('data:')).map(line => line.slice(5).trimStart()).join('\n'); + if (!data) continue; + if (data === '[DONE]') { done = true; continue; } + const event = JSON.parse(data); + if (event.error) throw new Error('Upstream reported an error in the research stream.'); + if (event.choices?.length > 1) throw new Error('Expected one research completion choice.'); + events.push(event); + } + const metadata = events.find(event => event.id && event.model); + if (!done || !metadata) throw new Error('Research stream did not complete.'); + let content = '', reasoning = '', finish; + const calls = new Map(); + for (const event of events) { + const choice = event.choices?.[0]; + if (!choice) continue; + if (choice.index !== 0) throw new Error('Unexpected research completion choice.'); + if (choice.finish_reason) finish = choice.finish_reason; + const delta = choice.delta || {}; + content += delta.content || ''; + reasoning += delta.reasoning || delta.reasoning_content || ''; + for (const part of delta.tool_calls || []) { + const value = calls.get(part.index) || { id: '', type: 'function', function: { name: '', arguments: '' } }; + if (part.id) value.id = part.id; + if (part.function?.name) value.function.name += part.function.name; + if (part.function?.arguments) value.function.arguments += part.function.arguments; + calls.set(part.index, value); + } + } + if (!['stop', 'tool_calls'].includes(finish)) throw new Error('Research tool generation did not finish normally.'); + let call; + const performed = calls.size === 0; + if (calls.size) { + if (calls.size !== 1 || calls.get(0)?.function.name !== SEARCH || !calls.get(0).id) { + throw new Error('Upstream returned an unexpected research tool call.'); + } + call = calls.get(0); + validArguments(call.function.arguments); + if (stream) return { body: raw, headers, performed: false, toolCallId: call.id }; + } else { + validArguments(content); + call = { id: `call_research_${randomUUID().replaceAll('-', '')}`, type: 'function', function: { name: SEARCH, arguments: content } }; + content = ''; + } + const usage = events.findLast(event => event.usage)?.usage; + const meta = { id: metadata.id, created: metadata.created, model: metadata.model }; + if (!stream) { + const message = { role: 'assistant', content: content || null, tool_calls: [call] }; + if (reasoning) message.reasoning = reasoning; + const value = { ...meta, object: 'chat.completion', choices: [{ index: 0, message, finish_reason: 'tool_calls' }], usage: usage || null }; + return { body: Buffer.from(JSON.stringify(value)), headers: { 'content-type': 'application/json' }, performed, toolCallId: call.id }; + } + const first = { role: 'assistant', content: '' }; + if (reasoning) first.reasoning = reasoning; + const chunks = [first, { tool_calls: [{ index: 0, ...call }] }, {}].map((delta, index) => ({ + ...meta, object: 'chat.completion.chunk', choices: [{ index: 0, delta, finish_reason: index === 2 ? 'tool_calls' : null }], + })); + if (usage) chunks.push({ ...meta, object: 'chat.completion.chunk', choices: [], usage }); + const body = Buffer.from(chunks.map(chunk => `data: ${JSON.stringify(chunk)}\n\n`).join('') + 'data: [DONE]\n\n'); + return { body, headers: { 'content-type': 'text/event-stream' }, performed, toolCallId: call.id }; +} + +function sendBody(res, status, headers, body) { + const output = { ...headers, 'content-length': String(body.length) }; + delete output['transfer-encoding']; + res.writeHead(status, output).end(body); +} + +export function createCompatibilityServer({ upstreamBaseUrl, researchToolCalls = false, auditDirectory = '', timeoutMs = 180_000, maxBufferedBytes = 16 * 1024 * 1024, log = () => {} }) { + const upstream = new URL(upstreamBaseUrl); + if (!['http:', 'https:'].includes(upstream.protocol) || upstream.username || upstream.password) throw new Error('Invalid upstream URL.'); + async function audit(id, record) { + if (!auditDirectory) return; + await mkdir(auditDirectory, { recursive: true, mode: 0o700 }); + await writeFile(join(auditDirectory, `${id}.json`), JSON.stringify(record, null, 2) + '\n', { mode: 0o600, flag: 'wx' }); + } + return http.createServer(async (req, res) => { + if (req.url === '/health') return res.writeHead(200).end('ok'); + const id = randomUUID(); + let record, audited = false; + try { + const target = new URL(req.url, upstream); + if (target.origin !== upstream.origin) throw new Error('Request must use the configured upstream.'); + const original = await readBody(req); + const prepared = prepare(req.method, target.pathname, original, researchToolCalls); + const shouldAudit = researchToolCalls && prepared.research && Boolean(auditDirectory); + if (shouldAudit) record = { id, at: new Date().toISOString(), originalRequestBase64: original.toString('base64'), forwardedRequestBase64: prepared.body.toString('base64'), headersExcluded: 'Authentication and cookie headers are never recorded.' }; + const headers = { ...req.headers, host: target.host, 'content-length': String(prepared.body.length) }; + delete headers['transfer-encoding']; + if (prepared.normalize) headers['accept-encoding'] = 'identity'; + const client = target.protocol === 'https:' ? https : http; + await new Promise((resolveRequest, reject) => { + const out = client.request(target, { method: req.method, headers }, async response => { + try { + if (!prepared.normalize && !shouldAudit) { + res.writeHead(response.statusCode || 502, response.headers); + response.on('error', reject); + response.on('end', resolveRequest); + response.pipe(res); + return; + } + const raw = await readBody(response, maxBufferedBytes); + if (record) Object.assign(record, { upstreamStatus: response.statusCode, upstreamResponseBase64: raw.toString('base64'), upstreamContentType: response.headers['content-type'], upstreamContentEncoding: response.headers['content-encoding'], upstreamResponseComplete: true }); + let output = { body: raw, headers: response.headers, performed: false }; + if (prepared.normalize && response.statusCode === 200) output = normalizeResponse(raw, response.headers, prepared.stream); + if (record) { + Object.assign(record, { normalizationPerformed: output.performed, toolCallId: output.toolCallId, returnedResponseBase64: output.body.toString('base64') }); + await audit(id, record); audited = true; + } + if (output.performed) log({ event: 'research-tool-normalized', requestId: id, toolCallId: output.toolCallId, upstreamBytes: raw.length, returnedBytes: output.body.length }); + sendBody(res, response.statusCode || 502, output.headers, output.body); + resolveRequest(); + } catch (error) { reject(error); } + }); + if (prepared.normalize || shouldAudit) out.setTimeout(timeoutMs, () => out.destroy(new Error('Research upstream request timed out.'))); + out.on('error', reject); + out.end(prepared.body); + }); + } catch (error) { + if (record && !audited) { + try { await audit(id, { ...record, error: String(error.message), returnedSuccess: false }); } + catch { /* The failed response below does not claim successful auditing. */ } + } + if (res.headersSent) return res.destroy(); + sendBody(res, 502, { 'content-type': 'application/json' }, Buffer.from(JSON.stringify({ error: { message: String(error.message), type: 'research_tool_compatibility_error', request_id: id } }))); + } + }); +} + +if (process.argv[1] && realpathSync(resolve(process.argv[1])) === fileURLToPath(import.meta.url)) { + createCompatibilityServer({ + upstreamBaseUrl: process.env.UPSTREAM_BASE_URL, + researchToolCalls: process.env.RESEARCH_TOOL_CALLS_ENABLED === 'true', + auditDirectory: process.env.RESEARCH_TOOL_CALLS_AUDIT_DIR || '', + log: value => console.log(JSON.stringify(value)), + }).listen(Number(process.env.PORT || 8000), '0.0.0.0'); +} diff --git a/helm/mosaic-stack/templates/llm-compat.yaml b/helm/mosaic-stack/templates/llm-compat.yaml index e763a0d9..c265d7e6 100644 --- a/helm/mosaic-stack/templates/llm-compat.yaml +++ b/helm/mosaic-stack/templates/llm-compat.yaml @@ -2,6 +2,9 @@ # SPDX-License-Identifier: Apache-2.0 {{- $c := .Values.llm.requestCompatibility -}} +{{- $r := $c.researchToolCalls | default dict -}} +{{- $research := and .Values.modules.research.enabled ($r.enabled | default false) -}} +{{- $audit := and $research ($r.auditEnabled | default false) -}} {{- if and .Values.modules.execution.enabled $c.enabled }} apiVersion: v1 kind: ConfigMap @@ -11,48 +14,8 @@ metadata: app: {{ $c.name }} {{- include "mosaic-stack.labels" . | nindent 4 }} data: - server.js: | - const http = require('http'); - const https = require('https'); - const upstream = new URL(process.env.UPSTREAM_BASE_URL); - const port = Number(process.env.PORT || {{ $c.port }}); - - function readBody(req) { - return new Promise((resolve, reject) => { - const chunks = []; - req.on('data', chunk => chunks.push(chunk)); - req.on('end', () => resolve(Buffer.concat(chunks))); - req.on('error', reject); - }); - } - - function patchBody(req, body) { - if (req.method !== 'POST' || !req.url.endsWith('/chat/completions')) return body; - try { - const json = JSON.parse(body.toString('utf8')); - json.reasoning_effort = 'none'; - return Buffer.from(JSON.stringify(json)); - } catch { - return body; - } - } - - http.createServer(async (req, res) => { - if (req.url === '/health') return res.writeHead(200).end('ok'); - const body = patchBody(req, await readBody(req)); - const target = new URL(req.url, upstream); - const headers = { ...req.headers, host: target.host, 'content-length': String(body.length) }; - const client = target.protocol === 'https:' ? https : http; - const out = client.request(target, { method: req.method, headers }, upstreamRes => { - res.writeHead(upstreamRes.statusCode || 502, upstreamRes.headers); - upstreamRes.pipe(res); - }); - out.on('error', error => { - res.writeHead(502, { 'content-type': 'application/json' }); - res.end(JSON.stringify({ error: String(error.message || error) })); - }); - out.end(body); - }).listen(port, '0.0.0.0'); + server.mjs: | + {{- .Files.Get "files/llm-compat-server.mjs" | nindent 4 }} --- apiVersion: apps/v1 kind: Deployment @@ -71,8 +34,12 @@ spec: labels: app: {{ $c.name }} annotations: - checksum/config: {{ printf "%s-%s-%s" .Chart.Version $c.name (include "mosaic-stack.llmUpstreamBaseUrl" .) | sha256sum }} + checksum/config: {{ printf "%s-%s-%s-%s-%v-%v" .Chart.Version $c.name (include "mosaic-stack.llmUpstreamBaseUrl" .) (.Files.Get "files/llm-compat-server.mjs" | sha256sum) $research $audit | sha256sum }} spec: + {{- if $audit }} + securityContext: + fsGroup: 1000 + {{- end }} {{- include "mosaic-stack.imagePullSecrets" . | nindent 6 }} {{- include "mosaic-stack.nodeSelector" . | nindent 6 }} {{- include "mosaic-stack.tolerations" . | nindent 6 }} @@ -80,12 +47,18 @@ spec: - name: proxy image: {{ include "mosaic-stack.image" .Values.mosaicUi.image | quote }} imagePullPolicy: {{ .Values.mosaicUi.image.pullPolicy }} - command: ["node", "/config/server.js"] + command: ["node", "/config/server.mjs"] env: - name: PORT value: {{ $c.port | quote }} - name: UPSTREAM_BASE_URL value: {{ include "mosaic-stack.llmUpstreamBaseUrl" . | quote }} + - name: RESEARCH_TOOL_CALLS_ENABLED + value: {{ $research | quote }} + {{- if $audit }} + - name: RESEARCH_TOOL_CALLS_AUDIT_DIR + value: /audit/records + {{- end }} {{- if and (eq .Values.llm.mode "external") .Values.llm.external.caCertificateSecret }} - name: NODE_EXTRA_CA_CERTS value: /etc/llm-ca/ca.crt @@ -102,6 +75,10 @@ spec: - name: config mountPath: /config readOnly: true + {{- if $audit }} + - name: research-audit + mountPath: /audit + {{- end }} {{- if and (eq .Values.llm.mode "external") .Values.llm.external.caCertificateSecret }} - name: llm-ca mountPath: /etc/llm-ca @@ -111,6 +88,11 @@ spec: - name: config configMap: name: {{ $c.name }} + {{- if $audit }} + - name: research-audit + emptyDir: + sizeLimit: 128Mi + {{- end }} {{- if and (eq .Values.llm.mode "external") .Values.llm.external.caCertificateSecret }} - name: llm-ca secret: diff --git a/helm/mosaic-stack/tests/chart-render.test.mjs b/helm/mosaic-stack/tests/chart-render.test.mjs index f78ace15..35da37b6 100644 --- a/helm/mosaic-stack/tests/chart-render.test.mjs +++ b/helm/mosaic-stack/tests/chart-render.test.mjs @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import { execFileSync, spawnSync } from "node:child_process"; -import { mkdtempSync, readFileSync, readdirSync, existsSync, rmSync, writeFileSync } from "node:fs"; +import { cpSync, mkdtempSync, readFileSync, readdirSync, existsSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -392,7 +392,44 @@ test("uses one no-reasoning request contract for external endpoints", () => { "--show-only", "templates/llm-compat.yaml", ); assert.match(output, /json\.reasoning_effort = 'none'/); - assert.doesNotMatch(output, /DISABLE_THINKING|VLLM_UPSTREAM|chat_template_kwargs|scrubVisibleThinking|pipeOpenAiStream/); + assert.doesNotMatch(output, /DISABLE_THINKING|VLLM_UPSTREAM|scrubVisibleThinking|pipeOpenAiStream/); + assert.match(output, /name: RESEARCH_TOOL_CALLS_ENABLED\s+value: "false"/); +}); + +test("research compatibility is opt-in and requires the research module", () => { + const args = ["--show-only", "templates/llm-compat.yaml"]; + const disabled = render(...args, "--set", "llm.requestCompatibility.researchToolCalls.enabled=true"); + assert.match(disabled, /name: RESEARCH_TOOL_CALLS_ENABLED\s+value: "false"/); + assert.doesNotMatch(disabled, /name: research-audit/); + const enabled = render(...args, "--set", "modules.research.enabled=true,llm.requestCompatibility.researchToolCalls.enabled=true,llm.requestCompatibility.researchToolCalls.auditEnabled=true"); + assert.match(enabled, /name: RESEARCH_TOOL_CALLS_ENABLED\s+value: "true"/); + assert.match(enabled, /name: RESEARCH_TOOL_CALLS_AUDIT_DIR\s+value: \/audit\/records/); + assert.match(enabled, /fsGroup: 1000/); + assert.match(enabled, /sizeLimit: 128Mi/); +}); + +test("the rendered compatibility server is the tested source file", () => { + const output = render("--show-only", "templates/llm-compat.yaml"); + const source = readFileSync(join(chart, "files/llm-compat-server.mjs"), "utf8"); + assert.equal(configMapBlock(output, "server.mjs").trimEnd(), source.trimEnd()); +}); + +test("a compatibility source change rolls the proxy without a chart version change", () => { + const directory = mkdtempSync(join(tmpdir(), "mosaic-compat-checksum-")); + try { + const copy = join(directory, "chart"); + cpSync(chart, copy, { recursive: true }); + const checksum = () => { + const output = execFileSync("helm", ["template", "mosaic", copy, "--show-only", "templates/llm-compat.yaml"], { encoding: "utf8" }); + return output.match(/checksum\/config: ([a-f0-9]{64})/)[1]; + }; + const before = checksum(); + const path = join(copy, "files/llm-compat-server.mjs"); + writeFileSync(path, readFileSync(path, "utf8") + "\n// Independent test change.\n"); + assert.notEqual(checksum(), before); + } finally { + rmSync(directory, { recursive: true, force: true }); + } }); test("configures chart-managed vLLM for non-thinking generation", () => { diff --git a/helm/mosaic-stack/tests/llm-compat.test.mjs b/helm/mosaic-stack/tests/llm-compat.test.mjs new file mode 100644 index 00000000..e6c80611 --- /dev/null +++ b/helm/mosaic-stack/tests/llm-compat.test.mjs @@ -0,0 +1,238 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import assert from 'node:assert/strict'; +import http from 'node:http'; +import { mkdtemp, readFile, readdir, rm, symlink } from 'node:fs/promises'; +import { spawn } from 'node:child_process'; +import { once } from 'node:events'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { gzipSync, deflateSync, brotliCompressSync } from 'node:zlib'; +import { fileURLToPath } from 'node:url'; +import { setTimeout as delay } from 'node:timers/promises'; +import test from 'node:test'; +import { createCompatibilityServer } from '../files/llm-compat-server.mjs'; + +const search = { type: 'function', function: { name: 'opensearch_search', parameters: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } } }; +const task = { type: 'function', function: { name: 'task', parameters: { type: 'object' } } }; +const input = () => ({ model: 'test-model', messages: [{ role: 'user', content: 'Search the indexed PDF.' }], tools: [search, task], stream: true, temperature: 1, max_completion_tokens: 4096 }); +function frames(content, { calls, finish = 'stop', done = true, usage, choices } = {}) { + const meta = { id: 'chatcmpl-real-upstream', model: 'test-model', created: 1793400000, object: 'chat.completion.chunk' }; + const delta = calls ? { tool_calls: calls } : { content }; + const result = [ + { ...meta, choices: [{ index: 0, delta: { role: 'assistant', content: '' }, finish_reason: null }] }, + { ...meta, choices: choices || [{ index: 0, delta, finish_reason: null }] }, + { ...meta, choices: [{ index: 0, delta: {}, finish_reason: finish }] }, + ]; + if (usage) result.push({ ...meta, choices: [], usage }); + return Buffer.from(result.map(value => `data: ${JSON.stringify(value)}\n\n`).join('') + (done ? 'data: [DONE]\n\n' : '')); +} +function parseFrames(body) { + return body.toString().split('\n').filter(line => line.startsWith('data: ') && line !== 'data: [DONE]').map(line => JSON.parse(line.slice(6))); +} +function returnedCall(body) { + return parseFrames(body).flatMap(event => event.choices || []).flatMap(choice => choice.delta?.tool_calls || [])[0]; +} +async function request(base, body, path = '/v1/chat/completions') { + return new Promise((resolveRequest, reject) => { + const req = http.request(base + path, { method: body === undefined ? 'GET' : 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer never-record-this-auth-value' } }, res => { + const chunks = []; + res.on('data', chunk => chunks.push(chunk)); + res.on('end', () => resolveRequest({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) })); + res.on('error', reject); + }); + req.on('error', reject); + req.end(body === undefined ? undefined : JSON.stringify(body)); + }); +} +async function fixture(options, run) { + const upstreamRequests = [], events = []; + const auditDirectory = await mkdtemp(join(tmpdir(), 'mosaic-research-compat-')); + const upstream = http.createServer(async (req, res) => { + const chunks = []; + for await (const chunk of req) chunks.push(chunk); + upstreamRequests.push({ path: req.url, headers: req.headers, body: chunks.length ? JSON.parse(Buffer.concat(chunks)) : null }); + if (options.handler) return options.handler(req, res); + const response = options.response || frames('{"query":"MOSAIC-KB page 2"}'); + res.writeHead(options.status || 200, { 'Content-Type': 'text/event-stream', ...options.headers }); + if (options.fragment) { + for (const byte of response) res.write(Buffer.from([byte])); + res.end(); + } else res.end(response); + }); + await new Promise(resolveListen => upstream.listen(0, '127.0.0.1', resolveListen)); + const proxy = createCompatibilityServer({ upstreamBaseUrl: `http://127.0.0.1:${upstream.address().port}/v1`, researchToolCalls: options.enabled !== false, auditDirectory: options.audit ? auditDirectory : '', log: value => events.push(value), ...options.server }); + await new Promise(resolveListen => proxy.listen(0, '127.0.0.1', resolveListen)); + try { + await run(`http://127.0.0.1:${proxy.address().port}`, upstreamRequests, auditDirectory, events); + } finally { + upstream.closeAllConnections(); proxy.closeAllConnections(); + await Promise.all([new Promise(resolveClose => upstream.close(resolveClose)), new Promise(resolveClose => proxy.close(resolveClose))]); + await rm(auditDirectory, { recursive: true }); + } +} + +test('disabled research compatibility preserves the existing no-reasoning contract and raw reply', async () => { + const raw = frames('{"query":"plain unexecuted text"}'); + await fixture({ enabled: false, response: raw, audit: true }, async (base, requests, directory) => { + const body = input(); + const result = await request(base, body); + assert.deepEqual(result.body, raw); + assert.deepEqual(requests[0].body, { ...body, reasoning_effort: 'none' }); + assert.deepEqual(await readdir(directory), []); + }); +}); + +test('normalizes only complete named query arguments and retains the full original audit', async () => { + const argumentsText = JSON.stringify({ query: '完整检索内容。'.repeat(1000) }, null, 2); + const raw = frames(argumentsText); + await fixture({ response: raw, audit: true, fragment: true }, async (base, requests, directory, events) => { + const body = input();body.chat_template_kwargs = { custom_setting: 'retained' }; + const result = await request(base, body); + assert.equal(result.status, 200); + const call = returnedCall(result.body); + assert.equal(call.function.name, 'opensearch_search'); + assert.equal(call.function.arguments, argumentsText); + assert.deepEqual(requests[0].body.chat_template_kwargs, { custom_setting: 'retained', enable_thinking: false }); + assert.deepEqual(requests[0].body.tool_choice, { type: 'function', function: { name: 'opensearch_search' } }); + assert.deepEqual(requests[0].body.messages, body.messages); + const files = await readdir(directory);assert.equal(files.length, 1); + const audit = JSON.parse(await readFile(join(directory, files[0]), 'utf8')); + assert.deepEqual(Buffer.from(audit.upstreamResponseBase64, 'base64'), raw); + assert.deepEqual(Buffer.from(audit.returnedResponseBase64, 'base64'), result.body); + assert.deepEqual(JSON.parse(Buffer.from(audit.originalRequestBase64, 'base64')), body); + assert.equal(audit.normalizationPerformed, true); + assert.equal(audit.upstreamResponseComplete, true); + assert.ok(!JSON.stringify(audit).includes('never-record-this-auth-value')); + assert.equal(events[0].toolCallId, call.id); + }); +}); + +test('already structured named calls pass through byte for byte', async () => { + const raw = frames('', { calls: [{ index: 0, id: 'call_actual_provider', type: 'function', function: { name: 'opensearch_search', arguments: '{"query":"actual query"}' } }], finish: 'tool_calls' }); + await fixture({ response: raw }, async (base, _requests, _directory, events) => { + const result = await request(base, input());assert.deepEqual(result.body, raw);assert.deepEqual(events, []); + }); +}); + +for (const [name, modify] of [ + ['explicit none', body => { body.tool_choice = 'none'; }], + ['another named tool', body => { body.tool_choice = { type: 'function', function: { name: 'task' } }; }], + ['post-search synthesis', body => { body.messages.push({ role: 'tool', tool_call_id: 'actual', content: 'Full retrieved document content.' }); }], + ['subagent without task', body => { body.tools = [search]; }], + ['unrelated OpenClaw tools', body => { body.tools = [task]; }], + ['multiple requested choices', body => { body.n = 2; }], + ['a different search schema', body => { body.tools = [{ ...search, function: { ...search.function, parameters: { ...search.function.parameters, required: ['query', 'collection'] } } }, task]; }], +]) { + test(`${name} is forwarded without a forced search or response rewrite`, async () => { + const raw = frames('A complete ordinary response.'); + await fixture({ response: raw }, async (base, requests) => { + const body = input();modify(body); + const result = await request(base, body);assert.deepEqual(result.body, raw); + assert.deepEqual(requests[0].body, { ...body, reasoning_effort: 'none' }); + }); + }); +} + +test('nonstream callers receive real tool arguments and complete provider usage', async () => { + const usage = { prompt_tokens: 17, completion_tokens: 8, total_tokens: 25, completion_tokens_details: { reasoning_tokens: 0 } }; + await fixture({ response: frames('{"query":"indexed doc"}', { usage }) }, async (base, requests) => { + const body = input();body.stream = false; + const result = await request(base, body);const value = JSON.parse(result.body); + assert.equal(value.object, 'chat.completion');assert.deepEqual(value.usage, usage); + assert.equal(value.choices[0].message.tool_calls[0].function.arguments, '{"query":"indexed doc"}'); + assert.equal(requests[0].body.stream, true);assert.equal(requests[0].body.stream_options.include_usage, true); + }); +}); + +for (const [encoding, compress] of [['gzip', gzipSync], ['deflate', deflateSync], ['br', brotliCompressSync]]) { + test(`handles complete ${encoding} upstream frames`, async () => { + await fixture({ response: compress(frames('{"query":"GPU 文档"}')), headers: { 'Content-Encoding': encoding } }, async base => { + const result = await request(base, input());assert.equal(result.status, 200); + assert.deepEqual(JSON.parse(returnedCall(result.body).function.arguments), { query: 'GPU 文档' }); + assert.equal(result.headers['content-encoding'], undefined); + }); + }); +} + +for (const content of ['I will search. {"query":"a"}', '{"query":', '{"query":""}', '{"query":123}', '{}', '[]', '{"query":"a","unexpected":"b"}']) { + test(`rejects unsupported arguments without inventing a call: ${content}`, async () => { + const raw = frames(content); + await fixture({ response: raw, audit: true }, async (base, _requests, directory) => { + const result = await request(base, input());assert.equal(result.status, 502); + assert.ok(!result.body.toString().includes('call_research_')); + const files = await readdir(directory);assert.equal(files.length, 1); + const audit = JSON.parse(await readFile(join(directory, files[0]), 'utf8')); + assert.deepEqual(Buffer.from(audit.upstreamResponseBase64, 'base64'), raw); + assert.equal(audit.returnedSuccess, false); + }); + }); +} + +for (const [name, options] of [['missing terminator', { done: false }], ['token exhaustion', { finish: 'length' }], ['filtered output', { finish: 'content_filter' }]]) { + test(`${name} cannot become a successful tool call`, async () => { + await fixture({ response: frames('{"query":"valid but incomplete generation"}', options) }, async base => { + assert.equal((await request(base, input())).status, 502); + }); + }); +} + +test('upstream failures preserve their status and complete body', async () => { + const body = Buffer.from('{"error":{"message":"upstream denied"}}'); + await fixture({ status: 401, response: body, headers: { 'Content-Type': 'application/json' } }, async base => { + const result = await request(base, input());assert.equal(result.status, 401);assert.deepEqual(result.body, body); + }); +}); + +test('unexpected tools cannot replace the explicitly selected read-only search', async () => { + const raw = frames('', { calls: [{ index: 0, id: 'wrong', type: 'function', function: { name: 'task', arguments: '{}' } }], finish: 'tool_calls' }); + await fixture({ response: raw }, async base => { assert.equal((await request(base, input())).status, 502); }); +}); + +test('oversized buffered responses fail instead of truncating arguments', async () => { + await fixture({ response: frames(JSON.stringify({ query: 'a'.repeat(4096) })), server: { maxBufferedBytes: 1024 } }, async base => { + assert.equal((await request(base, input())).status, 502); + }); +}); + +test('an upstream timeout returns an error rather than a fabricated tool result', async () => { + await fixture({ server: { timeoutMs: 25 }, handler: (_req, res) => { setTimeout(() => { if (!res.destroyed) res.end('late'); }, 100); } }, async base => { + assert.equal((await request(base, input())).status, 502); + }); +}); + +test('health does not call the model or create an audit', async () => { + await fixture({ audit: true }, async (base, requests, directory) => { + const result = await request(base, undefined, '/health');assert.equal(result.status, 200);assert.equal(result.body.toString(), 'ok'); + assert.deepEqual(requests, []);assert.deepEqual(await readdir(directory), []); + }); +}); + +test('the CLI starts through a ConfigMap-style symbolic link', { timeout: 10000 }, async () => { + const directory = await mkdtemp(join(tmpdir(), 'mosaic-research-entrypoint-')); + const link = join(directory, 'server.mjs'); + await symlink(fileURLToPath(new URL('../files/llm-compat-server.mjs', import.meta.url)), link); + const reserve = http.createServer(); + await new Promise(resolveListen => reserve.listen(0, '127.0.0.1', resolveListen)); + const port = reserve.address().port; + await new Promise(resolveClose => reserve.close(resolveClose)); + const child = spawn(process.execPath, [link], { env: { ...process.env, PORT: String(port), UPSTREAM_BASE_URL: 'http://127.0.0.1:1/v1', RESEARCH_TOOL_CALLS_ENABLED: 'true' }, stdio: ['ignore', 'pipe', 'pipe'] }); + const closed = once(child, 'close'); + let stderr = ''; + child.stderr.on('data', chunk => { stderr += chunk; }); + try { + let result; + for (let attempt = 0; attempt < 100; attempt++) { + assert.equal(child.exitCode, null, stderr || 'CLI exited before listening'); + try { result = await request(`http://127.0.0.1:${port}`, undefined, '/health'); break; } + catch { await delay(25); } + } + assert.equal(result?.status, 200, stderr || 'CLI did not listen'); + assert.equal(result.body.toString(), 'ok'); + } finally { + if (child.exitCode === null) child.kill('SIGTERM'); + await closed; + await rm(directory, { recursive: true }); + } +}); diff --git a/helm/mosaic-stack/values.yaml b/helm/mosaic-stack/values.yaml index 4d910b91..77bae2d5 100644 --- a/helm/mosaic-stack/values.yaml +++ b/helm/mosaic-stack/values.yaml @@ -118,6 +118,11 @@ llm: enabled: true name: llm-openai-compat port: 8000 + researchToolCalls: + # Opt in only for affected research backends returning named arguments as text. + enabled: false + # Temporary full body traces contain private prompts; export before Pod replacement. + auditEnabled: false external: baseUrl: "" model: ""