Skip to content

feat: qwen code support#502

Open
suculent wants to merge 8 commits into
wesm:mainfrom
suculent:feat/qwen-code-support
Open

feat: qwen code support#502
suculent wants to merge 8 commits into
wesm:mainfrom
suculent:feat/qwen-code-support

Conversation

@suculent
Copy link
Copy Markdown

adds results from Qwen Code

suculent and others added 2 commits May 15, 2026 20:22
Qwen emits one type=assistant JSONL line per model output, including
every tool-call iteration in a multi-step turn. Each iteration's parts
typically contain only a thought + functionCall, with the final
iteration carrying the user-facing text. The previous parser counted
each iteration as a distinct message, inflating MessageCount well
beyond what other agents report for the same turn (one real session
reported 142 messages over 2.5 minutes — 138 were tool-call-only
iterations of a single turn).

Coalesce consecutive tool-call-only assistant entries into the next
text-bearing assistant entry, aggregating their thinking text and
token usage. A trailing run with no text follow-up flushes as a
single coalesced assistant message so data isn't lost. Token usage
sums output and tracks peak (input + cache_read) for context, keeping
session-level TotalOutputTokens / PeakContextTokens arithmetically
consistent.

Adds fixture tests for the coalesce, trailing-tool-call, aborted
(no-response), and short-clean session shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@roborev-ci
Copy link
Copy Markdown

roborev-ci Bot commented May 15, 2026

roborev: Combined Review (7bb8ced)

Summary verdict: Qwen support is close, but the parser currently loses tool-use data and double-counts cached input tokens.

Medium

  • Location: internal/parser/qwen.go:201
    Problem: Qwen functionCall parts and corresponding functionResponse user entries are ignored, so Qwen sessions with tool use are stored with no ToolCalls, ToolResults, or HasToolUse; tool-call-only assistant entries without thought text are skipped entirely.
    Fix: Parse functionCall/functionResponse parts into ParsedToolCall/ParsedToolResult, aggregate them in qwenAssistantBuffer, set HasToolUse, and keep tool-only entries pending even when they have no thought text.

  • Location: internal/parser/qwen.go:355
    Problem: promptTokenCount is treated as uncached input and then cachedContentTokenCount is added again, but the provided Qwen payload shows totalTokenCount = promptTokenCount + candidatesTokenCount, meaning cached tokens are already included in the prompt count. This inflates ContextTokens and can double-count cached input in normalized usage/cost queries.
    Fix: Normalize like Codex: compute uncached input as max(promptTokenCount - cachedContentTokenCount, 0), store that as input_tokens, store cached separately, and use the original prompt count as the context total.


Synthesized from 3 reviews (agents: codex, gemini | types: default, security)

Qwen functionCall parts on assistant entries and functionResponse
parts on user entries were discarded, so tool-use sessions stored no
ToolCalls/ToolResults and HasToolUse stayed false; tool-call-only
assistant entries with no thought text were dropped entirely. Parse
both into ParsedToolCall/ParsedToolResult, aggregate them on
qwenAssistantBuffer (folding tool-result-only user entries into the
pending turn), and keep tool-only entries pending so the coalesced
turn always carries its tool-use evidence.

Qwen promptTokenCount already includes cachedContentTokenCount
(totalTokenCount = prompt + candidates), so adding the cached count
on top of the prompt double-counted cached input and inflated
ContextTokens. Normalize like Codex: store
max(promptTokenCount - cachedContentTokenCount, 0) as input_tokens,
keep cached separately, and use the prompt count as the context total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@roborev-ci
Copy link
Copy Markdown

roborev-ci Bot commented May 15, 2026

roborev: Combined Review (8c86b27)

New Qwen support looks mostly sound, but live syncing for Qwen sessions is broken until watcher path handling is fixed.

Medium

  • internal/parser/types.go:233: Qwen live syncing is effectively disabled. WatchSubdirs: []string{"chats"} points the watcher at <projectsDir>/chats, but Qwen stores sessions under <projectsDir>/<encoded-project>/chats. Also, classifyPath has no Qwen-specific branch to classify fsnotify events as AgentQwen files.

    Fix: Watch the Qwen projects root recursively or add per-project chats watch roots, and classify paths matching <qwenProjectsDir>/<encoded-project>/chats/<session>.jsonl as Qwen session files.


Synthesized from 3 reviews (agents: codex, gemini | types: default, security)

wesm and others added 2 commits May 16, 2026 12:50
Qwen sessions live at <projectsDir>/<encoded-project>/chats/<session>.jsonl,
but WatchSubdirs pointed the watcher at <projectsDir>/chats and classifyPath
had no Qwen branch, so fsnotify events for new chats never reached the sync
engine. Drop WatchSubdirs so the projects root is watched recursively
(matching Claude and iFlow), and add a Qwen case to classifyOnePath.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
qwen stored functionResponse.response (e.g. {"output": "..."}) directly in
ContentRaw, but the shared toolResultContentLength/DecodeContent helpers only
recognise strings, arrays, and the iFlow deep-nested object shape, so paired
qwen tool calls reported a zero ContentLength and an empty decoded result.
Unwrap response.output before storing so the string branch can handle it,
matching the gemini parser. The previous test only checked ContentRaw for a
substring, which passed spuriously; tighten it to DecodeContent + ContentLength
so the decode contract is exercised.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@roborev-ci
Copy link
Copy Markdown

roborev-ci Bot commented May 17, 2026

roborev: Combined Review (3b1b03f)

Summary verdict: one Medium issue remains; no High or Critical findings were reported.

Medium

  • internal/parser/qwen.go:223 - The parser flushes the assistant buffer as soon as any non-thought text is present, even when that same assistant entry also contains a functionCall. If Qwen emits text plus a tool call in one model entry, the following functionResponse will create a new pending blank assistant message via absorbToolResults, inflating MessageCount and leaving an empty assistant turn in the UI.
    • Suggested fix: Keep the buffer open for text-bearing entries that also have tool calls until the matching tool-result/final assistant response arrives, or attach tool results to the previous assistant message instead of starting a new pending assistant buffer.

Synthesized from 3 reviews (agents: codex, gemini | types: default, security)

A qwen assistant entry that carries both text and a functionCall flushed
the buffer immediately, so the next functionResponse landed on a fresh
pending turn and EOF (or the next text-only entry) emitted a phantom
empty assistant message holding the orphaned tool result. The tool_use
and tool_result for the same id ended up on different displayed messages
and MessageCount was inflated by one per occurrence.

Defer the flush until a text-only "closing" entry arrives so intermediate
text + tool_call iterations stay attached to their results, and append
content across entries in a turn so intermediate text isn't dropped when
a later iteration's text overwrites it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@roborev-ci
Copy link
Copy Markdown

roborev-ci Bot commented May 17, 2026

roborev: Combined Review (5e2ef7d)

No Medium, High, or Critical issues found.

All reviewers agree the code is clean.


Synthesized from 3 reviews (agents: codex, gemini | types: default, security)

suculent and others added 2 commits May 18, 2026 11:47
Qwen sessions live at <projectsDir>/<encoded-project>/chats/<id>.jsonl,
but the AgentDef pinned WatchSubdirs to "chats" — that pointed the
recursive watcher at <projectsDir>/chats, which does not exist, so no
fsnotify events fired for any Qwen session. Drop the subdir override
so the projects root is watched recursively and new project chats
directories are picked up as they appear.

The path classifier had no Qwen branch either, so even synthetic
SyncPaths calls fell through to no-match. Add a branch that maps
paths shaped like <qwenProjectsDir>/<encoded-project>/chats/<id>.jsonl
to AgentQwen, decoding the project name from the encoded directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both sides independently fixed the WatchSubdirs/classifier bug
(2b32cef vs 70f8842) and converged on the same shape. Resolved by
keeping the local explanatory comment on the AgentDef and the
stricter classifier (validates session ID, not just .jsonl suffix),
while folding in the broader negative-path test cases from the
remote.

Pulled in remote-only improvements that did not conflict:
- 3b1b03f: unwrap functionResponse.response.output so the shared
  toolResultContentLength/DecodeContent helpers can surface the
  result text.
- 5e2ef7d: defer the buffer flush on assistant entries that carry
  both text and a functionCall, and append content across entries
  within a turn, so intermediate text + tool_call iterations stay
  attached to their results.
@roborev-ci
Copy link
Copy Markdown

roborev-ci Bot commented May 18, 2026

roborev: Combined Review (0a2dd3e)

Summary verdict: One medium correctness issue should be fixed before merge; no high or critical findings were reported.

Medium

  • internal/parser/qwen.go:459: Coalesced Qwen assistant turns sum candidatesTokenCount across multiple model calls but only keep the peak promptTokenCount / cachedContentTokenCount for normalized input usage. This underreports total input_tokens and cache_read_input_tokens for turns with multiple tool-call iterations because earlier API calls are dropped from input/cache usage.
    • Fix: Track summed uncached input and summed cache-read tokens per absorbed assistant entry, while keeping peak prompt separately for ContextTokens.

Synthesized from 3 reviews (agents: codex, gemini | types: default, security)

@wesm
Copy link
Copy Markdown
Owner

wesm commented May 18, 2026

@suculent I will get this branch merged today (I think it was good to go at 5e2ef7d I just hadn't clicked the merge button)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants