Skip to content

help: background maintenance tasks fail on non-OpenAI-native chat providers (minimax M3 returns thinking-prefixed text, not JSON) #668

Description

@hasibagen

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:

  1. 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
  2. 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)
  3. 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
  4. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions