fix(chatbot): disable thinking + use non-streaming LLM call

- Set stream:false on the /chat/completions request so the bot gets one
  complete response instead of an SSE token stream.
- Add reasoning_effort:"none" to suppress extended-thinking/reasoning tokens
  (ignored by non-reasoning models like gemini-flash-lite).
- Add parseResponse(): handles both the JSON object 9router returns for
  stream:false and the SSE text it may still emit, delegating SSE to parseSse.
  Verified live: omniroute returns 200 application/json with message.content.
This commit is contained in:
asepharyana
2026-08-16 17:08:29 +07:00
parent 0ace758c79
commit cf1ec25c71
@@ -158,7 +158,13 @@ Gaya ngobrol:
tool_choice: "auto", tool_choice: "auto",
max_tokens: 600, max_tokens: 600,
temperature: 0.4, temperature: 0.4,
stream: true, // Non-streaming: request a single complete response. 9router may
// still emit SSE even with stream:false, so the parser below
// handle both raw-JSON and SSE bodies.
stream: false,
// Disable extended thinking / reasoning tokens so the bot answers
// directly (ignored by non-reasoning models).
reasoning_effort: "none",
}, },
{ {
headers: { headers: {
@@ -166,14 +172,14 @@ Gaya ngobrol:
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
timeout: 45_000, timeout: 45_000,
// 9router returns SSE even without stream:true; force stream:true
// in the body and read the raw SSE text.
responseType: "text", responseType: "text",
}, },
); );
// Parse SSE `data:` lines → content + tool_calls. // Parse the body into content + tool_calls. 9router may return either
const { content, toolCalls } = this.parseSse(response.data as string); // a single JSON object (stream:false honored) or SSE text (stream
// implied) — parseResponse handles both.
const { content, toolCalls } = this.parseResponse(response.data as string);
logger.debug( logger.debug(
{ {
@@ -242,6 +248,60 @@ Gaya ngobrol:
} }
} }
/**
* Parse an LLM HTTP body into content + tool_calls. Handles both shapes
* 9router can return: a single JSON object (stream:false honored) or SSE
* text (stream implied). For SSE we delegate to parseSse.
*/
private parseResponse(body: string): {
content: string;
toolCalls: Array<{
id: string;
name: string;
arguments: string;
args: Record<string, unknown>;
}>;
} {
const trimmed = body.trim();
// Non-streaming response: a single JSON object.
if (trimmed.startsWith("{")) {
try {
const json = JSON.parse(trimmed) as {
choices?: Array<{
message?: {
content?: string | null;
tool_calls?: Array<{
id?: string;
type?: string;
function?: { name?: string; arguments?: string };
}>;
};
delta?: unknown;
}>;
};
const msg = json.choices?.[0]?.message;
// If the router returned SSE-style shape under `choices[].delta`
// (rare), fall through to the SSE parser.
if (msg) {
const content = msg.content ?? "";
const toolCalls = (msg.tool_calls ?? []).map((tc, i) => {
const id = tc.id || `tool_${i}_${Date.now()}`;
return {
id,
name: tc.function?.name ?? "",
arguments: tc.function?.arguments ?? "",
args: this.safeJsonParse(tc.function?.arguments ?? ""),
};
});
return { content: content.trim(), toolCalls };
}
} catch {
// Not valid JSON after all — treat as SSE below.
}
}
return this.parseSse(body);
}
/** /**
* Parse an SSE stream body into accumulated content + any tool_calls. * Parse an SSE stream body into accumulated content + any tool_calls.
* 9router (and most OpenAI-compatible routers) emit `data: {json}` lines * 9router (and most OpenAI-compatible routers) emit `data: {json}` lines