コンテンツにスキップ

Strict Chat-Completion Compatibility

このコンテンツはまだ日本語訳がありません。

Some provider APIs require message shapes that others merely tolerate. TomoriBot normalizes three of these behind one shared seam so any provider path (built-in or a custom-endpoint proxy) can front a strict backend (e.g. Claude behind an OpenAI-shaped proxy, or a DeepSeek/Z.ai-style “continue this turn” backend).

The shared helpers live in src/providers/utils/strictChatCompat.ts.

NormalizationToggle?What it does
Role alternationstrict_role_alternationMerge consecutive same-role turns into one, and prepend a synthetic leading user turn when the first dialogue turn is assistant. Tool-bearing turns (top-level tool_calls/tool_call_id) act as merge boundaries so their wiring is never dropped.
Prefix completionsupports_prefix_completionSet prefix: true on the trailing assistant prefill turn so the backend continues it (DeepSeek / Z.ai vendor extension).
Media relocation(always-on, not a toggle)Peel media off assistant turns into a following [System: …] user turn with sender attribution; the assistant role cannot carry media in input history across OpenAI/Anthropic/Gemini-shaped APIs.

Role alternation and prefix completion pull in opposite directions and must stay independent:

  • A Claude-via-proxy backend wants role alternation ON, prefix completion OFF (it does not understand prefix: true and may hard-error on it).
  • A DeepSeek/Z.ai/vLLM-style continue backend wants prefix completion ON, and usually does not need role alternation.

A single bundled “strict mode” boolean would force the wrong combination on one group.

Media relocation runs unconditionally in every provider path, regardless of either toggle; a custom endpoint with both toggles OFF still gets it. It was already unconditional before this seam existed; do not gate it. The canonical wording is shared across all providers:

[System: The following image was sent by {name}.]
[System: The following images were sent by {name}.]

When no sender is available, the fallback is the same canonical sentence without the name:

[System: The following image was sent]
[System: The following images were sent]

The sender comes from StructuredContextItem.sender, populated from each SimplifiedMessageForContext row’s personaName / authorName before provider serialization. Relocation runs on that neutral representation first, so multi-persona histories can attribute each relocated image to the persona that actually sent it, including image-only turns where parsing a leading {Name}: text label would fail.

When relocation clones or splits a structured item, it also preserves hidden participant metadata, including conversationUsers and participantTargetIndex. Provider normalization never reparses participant prompt prose to recover mention, tool-target, or copied-identity candidates.

Anthropic and OpenRouter previously used a single per-request name ([System: This image was sent by {botName}.]) for every relocated image. That could mislabel images sent earlier by other personas. The OpenAI-compatible family previously used the generic [System: The previous assistant message included ...] wording. The current wording intentionally supersedes the plan-07 byte-identical golden-body bar for the media notice only; role alternation and prefix-completion goldens remain unchanged.

Both flags are stored as boolean columns on llms and custom_endpoints (src/types/db/schema.ts, src/db/schema.sql). At request time the active model’s llms row is the source of truth: the adapter reads context.tomoriState.llm.strict_role_alternation / .supports_prefix_completion:

  • Built-in providers are seeded with the required flag in the typed catalog (src/db/seed/catalog/models.ts): anthropic → alternation; deepseek/zai/zaicoding → prefix.
  • Custom endpoints carry the user’s toggle choices on the custom_endpoints row, synced to the endpoint’s synthetic llms row (upsertSyntheticCustomLlm), so the runtime reads them the same way as built-ins.

A small request-time safety net: providerRequiresAlternation / providerRequiresPrefixCompletion in strictChatCompat.ts: OR-combines with the column so a mis-seeded row can never make a built-in emit an invalid body:

const enforceAlternation =
providerRequiresAlternation(provider) || (llm?.strict_role_alternation ?? false);
const enablePrefix =
providerRequiresPrefixCompletion(provider) || (llm?.supports_prefix_completion ?? false);

Enforced by check-seed-catalogs (no UI guard)

Section titled “Enforced by check-seed-catalogs (no UI guard)”

Built-in llms rows have no capability-editing command surface, so there is no write/UI guard. Instead the per-provider required-flag invariant runs at boot and in CI via bun run check-seed-catalogscollectStrictChatFlagViolations in modelSeed.ts. It fails if any anthropic model lacks strictRoleAlternation, or any deepseek/zai/zaicoding model lacks supportsPrefixCompletion. Keep the REQUIRED_*_PROVIDERS sets in modelSeed.ts in lockstep with providerRequires* in strictChatCompat.ts.

The two toggles appear under Chat Completion Compatibilities in the Text model modal opened from the model dropdown on a /providers or /personal providers entry page. They are deliberately separate from Text Capabilities: tool calling, image input, and structured output describe the model, while these two describe the backend’s message parser.

The group is offered only where the request path can act on it, which is the openai-compatible api family minus any flag that family already forces on. Custom endpoints resolve through the custom provider and so keep both. Anthropic forces alternation and never reads prefix completion; DeepSeek, Z.ai, and Z.ai Coding force prefix completion; OpenRouter, Google/Vertex, and NovelAI read neither column. Where a flag is not offered, its stored value is preserved rather than rewritten, so nothing changes for a workspace that set it before. offeredChatCompatFlags derives this from apiFamily plus providerRequires*, and the submit path re-derives it rather than trusting the submission.

  • Strict Role Alternation: enable when your proxy fronts a backend that requires strict user/assistant alternation and a leading user turn (e.g. Claude behind an OpenAI-shaped proxy).
  • Prefix Completion: enable when your proxy fronts a backend that supports continuing a partial assistant turn (e.g. DeepSeek / Z.ai-style prefix: true, or vLLM/SGLang continue modes).

Both default OFF. With both OFF a custom endpoint behaves exactly as before (media relocation still applies; no alternation merge; no prefix: true).

025_strict_chat_compat_flags adds the two columns to both tables (defaulting OFF) and backfills the built-in requirements: strict_role_alternation = true for anthropic, and supports_prefix_completion = true for deepseek/zai/zaicoding. Fresh installs get the same values from the seed catalog + schema.sql.