title: "help: background maintenance tasks fail on non-OpenAI-native chat providers (minimax M3 returns thinking-prefixed text, not JSON)"
labels: "help wanted, documentation"
Context
I'm running memsearch plugin for Claude Code with a Chinese LLM provider called <provider-A> (let's call it this for neutrality). The provider exposes an OpenAI-compatible endpoint at https://<provider>.com/v1/chat/completions, supports function calls, and returns standard OpenAI-format responses.
The summarize and index paths work fine with this provider. The plugin config:
[llm.providers.[provider-A]]
type = "openai"
model = "<provider-A>-chat"
base_url = "https://<provider>.com/v1"
api_key = "env:PROVIDER_API_KEY"
[plugins.claude-code.summarize]
provider = "[provider-A]"
model = "<provider-A>-chat"
→ Stop hook summaries land in .memsearch/memory/YYYY-MM-DD.md as expected.
Problem
Background maintenance tasks (project_review, user_profile, memory_to_skill) fail at runtime with:
RuntimeError: Maintenance LLM did not return valid JSON: Expecting value: line 1 column 1 (char 0)
The state file ~/.memsearch/.maintenance-state.json records:
{
"claude-code.user_profile": {
"last_error": "RuntimeError: Maintenance LLM did not return valid JSON: Expecting value: line 1 column 1 (char 0)",
"last_failed_at": "2026-08-05T07:36:10.405714Z"
}
}
Root Cause Analysis
Direct curl to the provider's chat completions endpoint (mirror of what memsearch's maintenance.py calls):
curl -X POST "https://<provider>.com/v1/chat/completions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "<provider-A>-chat",
"messages": [{"role": "user", "content": "Return only JSON: {\"action\":\"none\"}"}],
"max_tokens": 1000,
"temperature": 0
}'
The response:
{
"choices": [{
"message": {
"content": "{\"action\":\"none\"}",
"role": "assistant"
}
}],
"usage": {
"completion_tokens_details": {"reasoning_tokens": 0}
}
}
But with smaller max_tokens (default 200), the response gets truncated mid-JSON:
{
"message": {
"content": "{\"action\":\"none\"}\"" ← trailing " breaks json.loads
}
}
And with the default memsearch prompt (which builds a long ## Current Direction / Active Threads / Recent Progress ... template), the model adds a ## Reasoning block in front of the JSON:
## Reasoning
The user is asking me to update PROJECT.md based on recent journals...
## Result
{"action":"none"}
In either case, json.loads(content) in maintenance.py:578 fails because the first non-whitespace character is # (from ## Reasoning) or " (from truncated JSON).
What I Tried (all failed)
| Attempt |
Result |
Set max_tokens=1000 |
Some prompts still hit ceiling (long journals + big prompt) |
Custom prompt via [prompts].project_review and [prompts].user_profile forcing JSON output |
Model still adds Chinese-language leading prose |
System prompt "Never output think tags, reasoning, or analysis. Output ONLY what the user requests." |
Worked for trivial prompts (10 chars), but ignored for real 18k-char prompts |
Patch maintenance.py to strip ```json ... ``` fences |
Already supported; my provider doesn't wrap in fences |
Patch maintenance.py to strip ## Reasoning blocks |
Worked for some prompts, but the user-managed codebase (/home/jade/.local/share/uv/tools/memsearch/...) gets overwritten on every uv tool install --force |
What I'm Asking For
I'd like some combination of:
-
Pre-validation / tolerant parsing in maintenance.py:
- Strip leading
## Reasoning / non-JSON prose before json.loads
- On parse failure, retry the LLM call with a stricter prompt (e.g., "failed: only return JSON, no markdown")
- Or fall back to
action: "none" with the raw text logged to .maintenance-state.json
-
Configurable request params for the maintenance calls:
max_tokens (currently hardcoded or per-provider default)
response_format: {type: "json_object"} (OpenAI native JSON mode) — opt-in, since not all providers support it
extra_body passthrough for provider-specific knobs (e.g. chat_template_kwargs for Groq, thinking for some others)
-
Documentation / examples:
- A worked example using a non-OpenAI-native provider (DeepSeek, Zhipu, MiniMax, etc.) with the exact recommended config in
~/.memsearch/config.toml
- A note that
summarize is more permissive (plain text) but maintenance is strict JSON, so the failure modes differ
-
Provider-aware defaults:
- Skip
maintenance automatically when the provider name doesn't match a known JSON-strict list (or always run it in strict_json mode with a fallback)
My preferred fix would be (1) lenient parsing + (2) max_tokens + response_format config — the smallest scope that unblocks non-OpenAI-native providers.
Environment
- memsearch 0.4.17
- Plugin installed via marketplace
- Provider:
<provider-A> (OpenAI-compatible REST API)
- OS: Linux
Workaround (current)
I disabled all project_review / user_profile / memory_to_skill maintenance tasks via memsearch config set plugins.claude-code.<task>.enabled false. This means I lose the auto-generated PROJECT.md / USER.md / skill-candidates/. I write PROJECT.md by hand when needed.
Happy to send a PR if you can point me to a maintainer preference for the approach (lenient parsing vs. config knob vs. docs).
title: "help: background maintenance tasks fail on non-OpenAI-native chat providers (minimax M3 returns thinking-prefixed text, not JSON)"
labels: "help wanted, documentation"
Context
I'm running memsearch plugin for Claude Code with a Chinese LLM provider called
<provider-A>(let's call it this for neutrality). The provider exposes an OpenAI-compatible endpoint athttps://<provider>.com/v1/chat/completions, supports function calls, and returns standard OpenAI-format responses.The summarize and index paths work fine with this provider. The plugin config:
→ Stop hook summaries land in
.memsearch/memory/YYYY-MM-DD.mdas expected.Problem
Background maintenance tasks (
project_review,user_profile,memory_to_skill) fail at runtime with:The state file
~/.memsearch/.maintenance-state.jsonrecords:{ "claude-code.user_profile": { "last_error": "RuntimeError: Maintenance LLM did not return valid JSON: Expecting value: line 1 column 1 (char 0)", "last_failed_at": "2026-08-05T07:36:10.405714Z" } }Root Cause Analysis
Direct
curlto the provider's chat completions endpoint (mirror of what memsearch'smaintenance.pycalls):The response:
{ "choices": [{ "message": { "content": "{\"action\":\"none\"}", "role": "assistant" } }], "usage": { "completion_tokens_details": {"reasoning_tokens": 0} } }But with smaller
max_tokens(default 200), the response gets truncated mid-JSON:{ "message": { "content": "{\"action\":\"none\"}\"" ← trailing " breaks json.loads } }And with the default memsearch prompt (which builds a long
## Current Direction / Active Threads / Recent Progress ...template), the model adds a## Reasoningblock in front of the JSON:In either case,
json.loads(content)inmaintenance.py:578fails because the first non-whitespace character is#(from## Reasoning) or"(from truncated JSON).What I Tried (all failed)
max_tokens=1000[prompts].project_reviewand[prompts].user_profileforcing JSON output"Never output think tags, reasoning, or analysis. Output ONLY what the user requests."maintenance.pyto strip```json ... ```fencesmaintenance.pyto strip## Reasoningblocks/home/jade/.local/share/uv/tools/memsearch/...) gets overwritten on everyuv tool install --forceWhat I'm Asking For
I'd like some combination of:
Pre-validation / tolerant parsing in
maintenance.py:## Reasoning/ non-JSON prose beforejson.loadsaction: "none"with the raw text logged to.maintenance-state.jsonConfigurable request params for the maintenance calls:
max_tokens(currently hardcoded or per-provider default)response_format: {type: "json_object"}(OpenAI native JSON mode) — opt-in, since not all providers support itextra_bodypassthrough for provider-specific knobs (e.g.chat_template_kwargsfor Groq,thinkingfor some others)Documentation / examples:
~/.memsearch/config.tomlsummarizeis more permissive (plain text) butmaintenanceis strict JSON, so the failure modes differProvider-aware defaults:
maintenanceautomatically when the provider name doesn't match a known JSON-strict list (or always run it instrict_jsonmode with a fallback)My preferred fix would be (1) lenient parsing + (2)
max_tokens+response_formatconfig — the smallest scope that unblocks non-OpenAI-native providers.Environment
<provider-A>(OpenAI-compatible REST API)Workaround (current)
I disabled all
project_review/user_profile/memory_to_skillmaintenance tasks viamemsearch config set plugins.claude-code.<task>.enabled false. This means I lose the auto-generatedPROJECT.md/USER.md/skill-candidates/. I writePROJECT.mdby hand when needed.Happy to send a PR if you can point me to a maintainer preference for the approach (lenient parsing vs. config knob vs. docs).