Skip to content

Fix #1611: classifyTopic fails when summarizer uses Anthropic endpoint (Kimi Code)#2133

Open
Memtensor-AI wants to merge 3 commits into
MemTensor:dev-v2.0.25from
Memtensor-AI:feature/autodev-1611-20260721033002951
Open

Fix #1611: classifyTopic fails when summarizer uses Anthropic endpoint (Kimi Code)#2133
Memtensor-AI wants to merge 3 commits into
MemTensor:dev-v2.0.25from
Memtensor-AI:feature/autodev-1611-20260721033002951

Conversation

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

Description

Fixed issue #1611: Summarizer.classifyTopic and Summarizer.arbitrateTopicSplit in the memos-local-openclaw plugin used to unconditionally dispatch to the OpenAI implementation, so any summarizer configured against an Anthropic-only endpoint (e.g. Kimi Code's /coding/v1/messages) had every classification call return 404 — the OpenAI helper appended /chat/completions to a URL that doesn't exist. The fix implements Option A from the issue: added six new native transport helpers (classifyTopic{Anthropic,Gemini,Bedrock} and arbitrateTopicSplit{Anthropic,Gemini,Bedrock}) mirroring the shape of the existing judgeNewTopic* helpers, and rewired both callTopicClassifier and callTopicArbitration switch statements so the anthropic / gemini / bedrock arms route to their native implementations instead of falling through to the OpenAI helpers.

To prevent prompt drift across providers, TOPIC_CLASSIFIER_PROMPT / TOPIC_ARBITRATION_PROMPT are exported from openai.ts and re-imported by the three provider files; parseTopicClassifyResult continues as the single JSON parser. The byte-similar duplicate at packages/memos-core/src/ingest/providers/ received identical edits so a future re-sync between the two trees cannot silently regress the bug. Bedrock helpers require cfg.endpoint, matching every other Bedrock helper in the file.

Tests: added apps/memos-local-openclaw/tests/topic-classifier-dispatch.test.ts with 8 cases that stub globalThis.fetch and assert each provider hits the correct URL + body shape. TDD baseline was 7/8 failing against the buggy code — the anthropic case surfaces the exact OpenAI topic-classifier failed (404) string from the issue. Post-fix: 8/8 pass. Existing topic-judge-minimax-1315.test.ts (5/5) continues to pass, and task-processor.test.ts regressions match baseline exactly. tsc --noEmit on the modified provider files is clean.

Files: 5 modified + 1 new in apps/memos-local-openclaw/, 5 mirrored in packages/memos-core/. Total 11 files, 876 insertions, 14 deletions. Branch pushed to origin/feature/autodev-1611-20260721033002951; scheduler will create the PR and add reviewers.

Related Issue (Required): Fixes #1611

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (does not change functionality, e.g. code style improvements, linting)
  • Documentation update

How Has This Been Tested?

Automated tests are pending.

  • Unit Test
  • Test Script Or Test Steps (please provide)
  • Pipeline Automated API Test (please provide)

Checklist

  • I have performed a self-review of my own code
  • I have commented my code in hard-to-understand areas
  • I have added tests that prove my fix is effective or that my feature works
  • I have created related documentation issue/PR in MemOS-Docs (if applicable)
  • I have linked the issue to this PR (if applicable)
  • I have mentioned the person who will review this PR

@whipser030, @hijzy please review this PR.

Reviewer Checklist

…MemTensor#1611)

`Summarizer.classifyTopic` and `Summarizer.arbitrateTopicSplit` always
dispatched to the OpenAI implementation regardless of the summarizer's
configured provider. When configured against an Anthropic-only endpoint
(e.g. Kimi Code's `/coding/v1/messages`), every call returned 404 because
the OpenAI helper appended `/chat/completions` to a URL that does not
exist — wasting tokens, polluting logs, adding event-loop latency.

Fix (Option A from the issue):

- Add native classifyTopic{Anthropic,Gemini,Bedrock} and
  arbitrateTopicSplit{Anthropic,Gemini,Bedrock} in the three provider
  files, mirroring the shape of the existing judgeNewTopic* helpers.
- Re-export TOPIC_CLASSIFIER_PROMPT / TOPIC_ARBITRATION_PROMPT from
  openai.ts so all providers share prompt strings and parseTopicClassifyResult
  remains the single JSON parser — no behavioral drift across providers.
- Rewire callTopicClassifier / callTopicArbitration switch statements
  so the anthropic / gemini / bedrock arms route to their native
  implementations instead of falling through to the OpenAI helpers.
- Bedrock helpers require cfg.endpoint (throws
  `Bedrock topic-classifier|arbitration requires 'endpoint'`) matching
  every other Bedrock helper in the file.
- Mirror every edit into packages/memos-core/src/ingest/providers/ so
  the byte-similar sibling tree cannot silently regress the bug after
  the next sync.

Tests: new tests/topic-classifier-dispatch.test.ts (8 cases) stubs
globalThis.fetch and asserts each provider hits the correct URL + body
shape; includes a regression case where an Anthropic 404 surfaces an
`Anthropic topic-classifier failed` error rather than the old
`OpenAI topic-classifier failed` — the exact string from issue MemTensor#1611.
TDD baseline: 7/8 fail against buggy code. Post-fix: 8/8 pass. Existing
tests/topic-judge-minimax-1315.test.ts (5/5) continues to pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Memtensor-AI Memtensor-AI added ai:generated Generated or modified by AI | 由 AI 生成或修改 area:plugin OpenClaw & Hermes status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Jul 21, 2026
@Memtensor-AI
Memtensor-AI requested review from hijzy and whipser030 July 21, 2026 04:16
@Memtensor-AI

Memtensor-AI commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Open Code Review

Target: PR #2133
Task: 75ef940b99925b96
Base: dev-v2.0.25
Head: feature/autodev-1611-20260721033002951
Head SHA: 9ac07bfb5f620ae1f86fecaa22d2e625c6172987

🔍 OpenCodeReview found 11 issue(s) in this PR.

⚠️ 1 warning(s) occurred during review.


1. apps/memos-local-openclaw/src/ingest/providers/index.ts (L721)

Finding #11 (PARTIALLY FIXED): The new DEFAULT_BEDROCK_TOPIC_MODEL constant correctly de-duplicates the model string for classifyTopicBedrock and arbitrateTopicSplitBedrock. However, the pre-existing judgeDedupBedrock (and other helpers in this file) still hardcode "anthropic.claude-3-haiku-20240307-v1:0" independently. A version bump or model rename still requires multiple edits. The constant should be promoted to a file-level default used by all helpers, or judgeDedupBedrock should reference DEFAULT_BEDROCK_TOPIC_MODEL.


2. apps/memos-local-openclaw/src/ingest/providers/gemini.ts (L393-L394)

Finding #5/#12 is NOT FIXED. When the Gemini API returns an unexpected response shape — e.g. an empty candidates array, a missing parts field, or a non-text content block — the optional chain silently collapses to "". In arbitrateTopicSplitGemini, this causes the !answer warn branch to fire and the function silently returns "SAME" without surfacing that the API returned an unusable response. The caller has no way to distinguish a genuine API failure from a normal "SAME" result.

Suggestion: throw an explicit error when the response shape is unexpected, rather than falling back silently:

const text = json.candidates?.[0]?.content?.parts?.[0]?.text?.trim();
if (!text) {
  throw new Error(`Gemini ${errorLabel}: unexpected response shape — ${JSON.stringify(json)}`);
}
return text;

3. apps/memos-local-openclaw/src/ingest/providers/anthropic.ts (L394-L396)

Finding #3 / #7 (partially unresolved — silent empty-string fallback in callAnthropicMessagesTopic):

When the Anthropic API returns an unexpected response shape (e.g. content is missing, empty, or contains only non-text blocks), this optional-chain silently collapses to "". For arbitrateTopicSplitAnthropic this is tolerable (it defaults to "SAME"), but for classifyTopicAnthropic the empty string is passed directly to parseTopicClassifyResult without any warning, masking the API anomaly.

The bedrock and gemini providers added explicit log.warn guards in their callers for the arbitration path. The same protection should be applied here, or callAnthropicMessagesTopic itself should throw (or warn) when the extracted text is empty.

Suggestion — throw in the helper so both callers benefit:

const text = content.find((c) => c.type === "text")?.text?.trim() ?? "";
if (!text) {
  throw new Error(`Anthropic ${errorLabel}: response contained no text content`);
}
return text;

4. apps/memos-local-openclaw/src/ingest/providers/bedrock.ts (L404-L405)

Finding #1 is still NOT FIXED. When the Bedrock Converse API returns an unexpected structure (e.g. an empty content array, a non-text content block, or missing fields), the optional-chain collapses silently to "". For arbitrateTopicSplitBedrock this means the empty string maps to "SAME" without any error thrown or meaningful diagnostic beyond a log.warn. This can mask real API failures and lead to silently incorrect topic decisions.

Suggestion: After the optional-chain, throw an explicit error (or at minimum a structured warning) when the result is "", e.g.:

const text = json.output?.message?.content?.find(b => (b as any).type === 'text' || !('type' in b))?.text?.trim();
if (!text) {
  throw new Error(`Bedrock ${errorLabel}: unexpected response shape: ${JSON.stringify(json)}`);
}
return text;

5. apps/memos-local-openclaw/src/ingest/providers/bedrock.ts (L364)

Finding #11 is still NOT FIXED. DEFAULT_BEDROCK_TOPIC_MODEL is a named constant for the two new helpers, but the pre-existing judgeDedupBedrock (line 456) still contains its own inline "anthropic.claude-3-haiku-20240307-v1:0" literal. A model version bump now requires changes in two places in this file. The fix would be to either widen the scope of DEFAULT_BEDROCK_TOPIC_MODEL to cover all three callers, or introduce a file-level constant that all three Bedrock helpers share.


6. packages/memos-core/src/ingest/providers/gemini.ts (L393-L394)

Finding #5 / #12 is still NOT FIXED. When the Gemini API returns an unexpected response shape (empty candidates array, missing content/parts, non-text block type, etc.), the optional chain silently collapses to "". This empty string flows into arbitrateTopicSplitGemini, which calls "".toUpperCase()"", logs a warn, and silently returns "SAME" — masking a broken API response as a valid classification decision.

Suggested fix: throw an error when the extracted text is empty, so callers surface the failure instead of silently defaulting:

const text = json.candidates?.[0]?.content?.parts?.[0]?.text?.trim();
if (!text) {
  throw new Error(`Gemini ${errorLabel}: empty or unexpected response shape`);
}
return text;

With this, arbitrateTopicSplitGemini can remove the silent-fallback log.warn/default and let the error propagate.

💡 Suggested Change

Before:

  const json = (await resp.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text: string }> } }> };
  return json.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";

After:

  const json = (await resp.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text: string }> } }> };
  const text = json.candidates?.[0]?.content?.parts?.[0]?.text?.trim();
  if (!text) {
    throw new Error(`Gemini ${errorLabel}: empty or unexpected response shape`);
  }
  return text;

7. packages/memos-core/src/ingest/providers/bedrock.ts (L404-L405)

Finding #1/#8/#9 (packages/memos-core bedrock + apps bedrock) — still NOT FIXED.

When the Bedrock Converse API returns an unexpected payload (e.g. an empty content array, a non-text content block, or any missing field along the output.message.content path), the optional-chain silently collapses to "". arbitrateTopicSplitBedrock then logs a warn but silently returns "SAME", masking the underlying contract violation entirely.

The fix should throw explicitly when the expected text is absent:

const text = json.output?.message?.content?.find((b) => "text" in b)?.text?.trim();
if (!text) {
  throw new Error(`Bedrock ${errorLabel}: unexpected response shape – ${JSON.stringify(json)}`);
}
return text;
💡 Suggested Change

Before:

  const json = (await resp.json()) as { output?: { message?: { content?: Array<{ text: string }> } } };
  return json.output?.message?.content?.[0]?.text?.trim() ?? "";

After:

  const json = (await resp.json()) as { output?: { message?: { content?: Array<{ text: string }> } } };
  const text = json.output?.message?.content?.find((b) => "text" in b)?.text?.trim();
  if (!text) {
    throw new Error(`Bedrock ${errorLabel}: unexpected response shape – ${JSON.stringify(json)}`);
  }
  return text;

8. packages/memos-core/src/ingest/providers/bedrock.ts (L364)

Finding #11 — still NOT FIXED (partial fix only).

DEFAULT_BEDROCK_TOPIC_MODEL consolidates the literal for classifyTopicBedrock and arbitrateTopicSplitBedrock, but the pre-existing judgeDedupBedrock (line 456) still independently hardcodes the same string "anthropic.claude-3-haiku-20240307-v1:0". A version bump or model rename still requires updating two separate sites in this file. The constant should be reused by judgeDedupBedrock as well, or renamed to reflect file-wide scope (e.g. DEFAULT_BEDROCK_MODEL).

💡 Suggested Change

Before:

const DEFAULT_BEDROCK_TOPIC_MODEL = "anthropic.claude-3-haiku-20240307-v1:0";

After:

// Rename to signal file-wide scope, then reuse in judgeDedupBedrock
const DEFAULT_BEDROCK_MODEL = "anthropic.claude-3-haiku-20240307-v1:0";

9. packages/memos-core/src/ingest/providers/anthropic.ts (L394-L396)

Finding #6/#7 (packages/memos-core anthropic.ts) is NOT FULLY FIXED. callAnthropicMessagesTopic successfully deduplicates boilerplate between classifyTopicAnthropic and arbitrateTopicSplitAnthropic, but the pre-existing judgeDedupAnthropic (lines 436-474) still contains its own fully independent inline implementation: endpoint resolution, model selection, header construction, fetch, error check, and JSON parsing. Three largely-identical implementations now exist in the file instead of one. The findings explicitly required judgeDedupAnthropic to also be consolidated.

Suggestion: Extend callAnthropicMessagesTopic (or create a more general callAnthropicMessages) to accept maxTokens as it already does, then refactor judgeDedupAnthropic to delegate to it. Note also that judgeDedupAnthropic has a subtle header-order difference (it spreads cfg.headers LAST, allowing user headers to override x-api-key), which the new helper deliberately reverses — this discrepancy between the two implementations should be reconciled.


10. packages/memos-core/src/ingest/providers/anthropic.ts (L433-L436)

Finding #6/#7: arbitrateTopicSplitAnthropic (in the current diff) silently returns "SAME" when callAnthropicMessagesTopic returns an empty string (e.g., if the Anthropic API returns a non-text content block or an empty content array). Unlike the Bedrock and Gemini counterparts in this same PR — which both add log.warn for empty/unexpected responses — arbitrateTopicSplitAnthropic has no such guard. A malformed API response will silently default to "SAME" with no observable signal.

Suggestion: Add the same guard that Bedrock/Gemini now have:

if (!answer) {
  log.warn("Anthropic topic-arbitration returned empty text; defaulting to SAME");
} else if (!answer.startsWith("NEW") && !answer.startsWith("SAME")) {
  log.warn(`Anthropic topic-arbitration returned unexpected value "${answer}"; defaulting to SAME`);
}

11. packages/memos-core/src/ingest/providers/gemini.ts (L431-L436)

Finding #5/#12 (still UNRESOLVED — consumer side): When callGeminiTopic returns "" due to an unexpected API response shape, the empty string is silently promoted to "SAME" here. This is an incorrect behavioral outcome: a malformed API response should surface as an error, not be swallowed as a default classification decision.

Once callGeminiTopic is fixed to throw on empty text (see comment above), this entire guard block becomes unnecessary and can be removed. Alternatively, if the empty-string return is kept, this block should throw instead of log.warn + silent default.

💡 Suggested Change

Before:

  if (!answer) {
    log.warn("Gemini topic-arbitration returned empty text; defaulting to SAME");
  } else if (!answer.startsWith("NEW") && !answer.startsWith("SAME")) {
    log.warn(`Gemini topic-arbitration returned unexpected value "${answer}"; defaulting to SAME`);
  }
  return answer.startsWith("NEW") ? "NEW" : "SAME";

After:

  // After callGeminiTopic is fixed to throw on empty text, the !answer branch
  // is unreachable. The unexpected-value branch should also throw:
  if (!answer.startsWith("NEW") && !answer.startsWith("SAME")) {
    throw new Error(`Gemini topic-arbitration returned unexpected value "${answer}"`);
  }
  return answer.startsWith("NEW") ? "NEW" : "SAME";

🧹 Filtered 6 low-confidence OCR finding(s) before posting/fix-loop (existing_code_mismatch: 5, duplicate: 1).

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator Author

🔧 Open Code Review requested Agent fix

Open Code Review found 14 issue(s). I have resumed the development Agent to fix them.

  • Task: 75ef940b99925b96
  • Fix attempt: 1/2
  • Finding delta: 0 repeated / 14 new / 0 likely resolved

The Agent will push a new commit to this PR branch. OCR will recheck after the commit is pushed.

…eview

Address code-review findings on the topic-classifier + arbitrator dispatch
helpers added in MemTensor#2133 (issue MemTensor#1611). Applied to both
apps/memos-local-openclaw and packages/memos-core (byte-similar mirrors).

Correctness / security fixes:
- anthropic: validate cfg.apiKey up front instead of silently sending "";
  move cfg.headers spread BEFORE x-api-key + anthropic-version so a
  misconfigured header cannot silently overwrite the credential
- anthropic: guard content parsing — treat missing/non-array json.content
  as [] to avoid TypeError on unexpected API payloads
- anthropic: reduce arbitrateTopicSplit max_tokens from 60 to 10 to match
  arbitrateTopicSplitOpenAI (single-word NEW/SAME reply)
- gemini: validate cfg.apiKey up front; construct URL via new URL() +
  URLSearchParams so a caller-supplied endpoint that already contains a
  query string ("?version=v1beta") does not produce malformed
  "…?version=v1beta?key=…"
- bedrock: emit warn log when arbitrateTopicSplit sees empty or
  unexpected text so silent bias toward SAME becomes visible at normal
  log levels (production usually suppresses debug)

Intentionally not applied (out-of-scope refactors that would touch
pre-existing helpers not in this PR): DRY-extract shared low-level
callers for anthropic/gemini/bedrock (findings MemTensor#4/MemTensor#7/MemTensor#10/MemTensor#14) and the
DEFAULT_BEDROCK_MODEL constant (finding MemTensor#11).

Existing tests unchanged: tests/topic-classifier-dispatch.test.ts (8/8)
and tests/topic-judge-minimax-1315.test.ts (5/5) still pass. The new
guards use apiKey values already provided by every test case, so no
test edits were required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Memtensor-AI

Copy link
Copy Markdown
Collaborator Author

✅ Automated Test Results: PASSED

All tests passed (17/17 executed). memos_local_openclaw/unit: 8/8, memos_local_plugin/unit: 9/9. Duration: 6s [advisory, non-gating] AI-generated tests on branch test/auto-gen-75ef940b99925b96-20260721122708: 84/86 passed, 2 failed — these do NOT affect the PR verdict; review the branch manually.

Branch: feature/autodev-1611-20260721033002951

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Jul 21, 2026
@Memtensor-AI

Copy link
Copy Markdown
Collaborator Author

🔧 Open Code Review requested Agent fix

Open Code Review found 12 issue(s). I have resumed the development Agent to fix them.

  • Task: 75ef940b99925b96
  • Fix attempt: 2/2
  • Finding delta: 2 repeated / 10 new / 12 likely resolved

Repeated findings are prioritized because the previous repair did not converge.
The Agent will push a new commit to this PR branch. OCR will recheck after the commit is pushed.

…fier providers

Round 1 (commit 646c28a) did not fully converge OCR review. Round 2 targets
the 2 repeated findings and 10 newly detected ones without expanding scope
beyond the classifier/arbitration functions added by PR MemTensor#2133.

Bedrock (packages/memos-core + apps/memos-local-openclaw):
- Fix TRUE MISS from Round 1: `arbitrateTopicSplitBedrock` was still using
  `maxTokens: 60` while Anthropic's arbitrator was already lowered to 10.
  Aligned to `maxTokens: 10`.
- Introduce `DEFAULT_BEDROCK_TOPIC_MODEL` constant scoped to the two new
  functions only (`classifyTopicBedrock`, `arbitrateTopicSplitBedrock`).
  Pre-existing helpers (`judgeDedupBedrock`, `summarizeBedrock`, etc.) keep
  their inline default to stay out of scope.
- Extract `bedrockConverseTopic()` shared transport used by the two new
  functions to eliminate the DRY duplication OCR flagged.

Gemini (packages/memos-core + apps/memos-local-openclaw):
- Add empty/unexpected-response `log.warn` branches to
  `arbitrateTopicSplitGemini`, mirroring the Bedrock arbitrator's
  defensive handling (Round 1 covered Bedrock only).
- Extract `callGeminiTopic()` shared transport used by the two new
  functions to eliminate the DRY duplication OCR flagged.
- Rejected escalating the empty-response case to `throw`: throwing would
  cascade through `Summarizer.tryChain` and burn tokens on every fallback;
  warn+default-to-SAME is the safer choice for a boundary detector.

Anthropic (packages/memos-core + apps/memos-local-openclaw):
- Extract `callAnthropicMessagesTopic()` shared transport used by the two
  new functions to eliminate the DRY duplication OCR flagged. Header spread
  order is preserved verbatim: `...cfg.headers` comes BEFORE the credential
  and `anthropic-version` so user-supplied headers cannot override them
  (this was the Round 1 security fix — do not undo).

Pre-existing helpers in each provider are intentionally untouched to keep
the diff minimum-scoped to PR MemTensor#2133's classifier/arbitrator additions.

Verification (executed in apps/memos-local-openclaw):
- `./node_modules/.bin/vitest run tests/topic-classifier-dispatch.test.ts \\
  tests/topic-judge-minimax-1315.test.ts` → 13/13 passed
- `./node_modules/.bin/tsc --noEmit` on the 3 modified provider files → 0 errors
- All 6 files (3 apps + 3 packages/memos-core mirrors) are byte-identical
@Memtensor-AI

Copy link
Copy Markdown
Collaborator Author

⚠️ Open Code Review automatic OCR fix limit reached

Open Code Review still found 11 issue(s), but the automatic OCR fix loop has reached 2/2 attempts.

Finding delta: 1 repeated, 10 newly detected, 11 likely resolved since the previous repair request.

I stopped auto-fixing this PR to avoid an infinite loop. This PR now requires manual review by the CodeOwner/maintainer.

Please review the latest OCR comment and either fix the remaining findings or explicitly dismiss false positives.

PR: #2133

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai:generated Generated or modified by AI | 由 AI 生成或修改 area:plugin OpenClaw & Hermes status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants