Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions deploy/coolify/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ RUN printf '{"name":"agentmemory-deploy","version":"1.0.0","private":true,"overr
&& npm install "@agentmemory/agentmemory@${AGENTMEMORY_VERSION}" --omit=optional --no-fund --no-audit \
&& ln -s /opt/agentmemory/node_modules/.bin/agentmemory /usr/local/bin/agentmemory

# Reasoning-model fallback: OpenRouter reasoning models return content:null
# with the answer in message.reasoning when finish_reason:"length". Fall back
# so mem::summarize chunk calls do not hard-fail (empty_provider_response).
COPY openrouter-reasoning-patch.mjs /tmp/openrouter-reasoning-patch.mjs
RUN node /tmp/openrouter-reasoning-patch.mjs && rm /tmp/openrouter-reasoning-patch.mjs

ENV AGENTMEMORY_III_VERSION=${III_VERSION} \
TINI_SUBREAPER=1

Expand Down
35 changes: 35 additions & 0 deletions deploy/coolify/openrouter-reasoning-patch.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";

// Reasoning models on OpenRouter (DeepSeek V4/R1, etc.) return the final
// answer in message.content but, when reasoning exhausts the token budget
// (finish_reason:"length"), content is null and only reasoning /
// reasoning_details are populated. Fall back so mem::summarize chunk calls
// do not hard-fail with empty_provider_response. The reasoning text is
// degraded (truncated) in the length case, but the chunk+reduce layer
// tolerates partial output better than a hard throw.
const dir = "/opt/agentmemory/node_modules/@agentmemory/agentmemory/dist";
const old =
"const content = data.choices?.[0]?.message?.content;\n" +
"\t\tif (!content) throw new Error(`${this.name} returned unexpected response";
const replacement =
"const _msg = data.choices?.[0]?.message;\n" +
"\t\tlet content = _msg?.content;\n" +
"\t\tif (!content) { const _rd = _msg?.reasoning_details; content = (Array.isArray(_rd) ? _rd.find(d => d && d.text)?.text : undefined) ?? _msg?.reasoning; }\n" +
"\t\tif (!content) throw new Error(`${this.name} returned unexpected response";

let patched = 0;
for (const f of readdirSync(dir)) {
if (!f.endsWith(".mjs")) continue;
const p = join(dir, f);
const s = readFileSync(p, "utf8");
if (!s.includes(old)) continue;
writeFileSync(p, s.replace(old, replacement));
console.log("openrouter-reasoning-patch: patched", p);
patched++;
}
if (patched === 0) {
console.error("openrouter-reasoning-patch: PATTERN NOT FOUND — aborting build");
process.exit(1);
}
console.log("openrouter-reasoning-patch: " + patched + " file(s) patched");
25 changes: 23 additions & 2 deletions src/providers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,30 @@ export class OpenRouterProvider implements MemoryProvider {

const data = (await response.json()) as Record<string, unknown>;
const choices = data.choices as
| Array<{ message: { content: string } }>
| Array<{
message: {
content: string | null;
reasoning?: string | null;
reasoning_details?: Array<{ text?: string }> | null;
};
}>
| undefined;
const content = choices?.[0]?.message?.content;
const msg = choices?.[0]?.message;
let content = msg?.content;
// Reasoning models on OpenRouter (DeepSeek V4/R1, etc.) return the final
// answer in `message.content` but, when reasoning exhausts the token
// budget (finish_reason:"length"), `content` is null and only
// `reasoning`/`reasoning_details` are populated. Fall back so summarize
// chunk calls do not hard-fail with empty_provider_response. The
// reasoning text is degraded (truncated) in the length case, but the
// chunk+reduce layer tolerates partial output better than a hard throw.
if (!content) {
const details = msg?.reasoning_details;
content =
(Array.isArray(details)
? details.find((d) => d.text)?.text
: undefined) ?? msg?.reasoning ?? undefined;
}
if (!content) {
throw new Error(
`${this.name} returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`,
Expand Down