@askalf/dario 5.5.27 → 5.5.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1672,10 +1672,19 @@ export function buildCCRequest(clientBody, billingTag, cacheControl, identity, o
1672
1672
  // appended its assistant reply locally, dario stripped it from the next
1673
1673
  // request, the model regenerated the same reply, dario stripped that, and
1674
1674
  // the loop never terminated (133 POSTs from a single user prompt).
1675
+ //
1676
+ // Restricted to ASSISTANT turns, which is the only shape this loop was
1677
+ // written for (a thinking-only assistant turn emptied by the strip above).
1678
+ // Popping an empty *user* turn is what the loop must not do: it exposes the
1679
+ // assistant turn behind it and produces the very prefill rejection the loop
1680
+ // exists to prevent (dario#1033). An empty user turn is a malformed client
1681
+ // request either way — leaving it in place lets the upstream name it
1682
+ // accurately ("messages.N: content must contain at least one block")
1683
+ // instead of dario converting it into a misleading prefill error.
1675
1684
  while (messages.length > 0) {
1676
1685
  const last = messages[messages.length - 1];
1677
1686
  const contentEmpty = Array.isArray(last.content) && last.content.length === 0;
1678
- if (contentEmpty) {
1687
+ if (contentEmpty && last.role === 'assistant') {
1679
1688
  messages.pop();
1680
1689
  continue;
1681
1690
  }
package/dist/proxy.js CHANGED
@@ -658,6 +658,12 @@ export function sanitizeMessages(body, preserveTags) {
658
658
  if (!messages)
659
659
  return;
660
660
  const patterns = orchestrationPatternsFor(preserveTags);
661
+ // Snapshot the tail turn before scrubbing. If the scrub empties it and the
662
+ // drop below would expose an assistant turn, we put the original content
663
+ // back rather than ship a prefill (dario#1033) — see the guard after the
664
+ // filter for the reasoning.
665
+ const tail = messages.length > 0 ? messages[messages.length - 1] : undefined;
666
+ const tailContentBeforeScrub = tail ? tail.content : undefined;
661
667
  for (const msg of messages) {
662
668
  if (typeof msg.content === 'string') {
663
669
  msg.content = sanitizeContent(msg.content, patterns);
@@ -691,13 +697,48 @@ export function sanitizeMessages(body, preserveTags) {
691
697
  // The message carried nothing for the model, so removing it is the same
692
698
  // decision the block filter already made, applied one level up. String
693
699
  // content scrubbed to '' is the same case in its other shape.
694
- body.messages = messages.filter((m) => {
700
+ const kept = messages.filter((m) => {
695
701
  if (Array.isArray(m.content))
696
702
  return m.content.length > 0;
697
703
  if (typeof m.content === 'string')
698
704
  return m.content !== '';
699
705
  return true;
700
706
  });
707
+ // Invariant: the scrub must never turn a request that ended on a USER turn
708
+ // into one that ends on an ASSISTANT turn. Anthropic reads a trailing
709
+ // assistant turn as a prefill ("continue from this text") and Opus 4.6 under
710
+ // adaptive thinking + the claude-code beta rejects it outright:
711
+ // 400 "This model does not support assistant message prefill.
712
+ // The conversation must end with a user message."
713
+ //
714
+ // CC emits standalone `<system-reminder>` / `<task_metadata>` user turns —
715
+ // notably right after a Task (sub-agent) result is folded back into the
716
+ // parent transcript. Both tags are in ORCHESTRATION_TAG_NAMES, so that turn
717
+ // scrubs to empty, the filter above drops it, and a valid CC request leaves
718
+ // as a prefill (dario#1033).
719
+ //
720
+ // The fix restores the turn's PRE-SCRUB content instead of dropping it. The
721
+ // orchestration tag survives in this one position only, which is the right
722
+ // trade in both directions: for a CC client the tag is CC's own injection,
723
+ // so keeping it is *more* wire-faithful, not less; for a non-CC client a
724
+ // lone tag as the final turn is the actual prompt, and forwarding it beats
725
+ // a hard 400. Every other position still scrubs exactly as before.
726
+ const tailWasDropped = tail !== undefined && kept[kept.length - 1] !== tail;
727
+ const tailHadContent = Array.isArray(tailContentBeforeScrub)
728
+ ? tailContentBeforeScrub.length > 0
729
+ : typeof tailContentBeforeScrub === 'string'
730
+ ? tailContentBeforeScrub !== ''
731
+ : tailContentBeforeScrub != null;
732
+ if (tail !== undefined &&
733
+ tailWasDropped &&
734
+ tail.role === 'user' &&
735
+ tailHadContent &&
736
+ kept.length > 0 &&
737
+ kept[kept.length - 1].role === 'assistant') {
738
+ tail.content = tailContentBeforeScrub;
739
+ kept.push(tail);
740
+ }
741
+ body.messages = kept;
701
742
  }
702
743
  /**
703
744
  * Scrub non-Claude-Code fields and normalize field ordering.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "5.5.27",
3
+ "version": "5.5.28",
4
4
  "description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
5
5
  "type": "module",
6
6
  "bin": {