qwencode

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package qwencode implements an event-level adapter for Qwen Code.

SURFACE. Qwen Code keeps its OWN append-only usage ledger, one JSON record per API response, at

${QWEN_RUNTIME_DIR:-${QWEN_HOME:-~/.qwen}}/usage/token-usage-<localMonth>.jsonl

That file is the authoritative surface and this adapter reads nothing else. The session transcripts under projects/ are what the third-party parsers read instead, and they carry no request identity, so they force a content-tuple dedup key while the ledger next door hands out a per-record randomUUID. The ledger record carries exactly: schemaVersion, id, timestamp, localDate, localMonth, sessionId, model, authType, source, inputTokens, outputTokens, cachedTokens, thoughtsTokens, totalTokens, apiDurationMs. Every one of those is a counter or an identity; the surface has no content field at all.

ABSENCE IS NOT PROOF OF NON-USE. The write is gated on `privacy.usageStatisticsEnabled` (default true, overridable per run by QWEN_USAGE_STATISTICS_ENABLED), so a missing usage/ directory means EITHER the harness was never run OR the user opted out. Discovery therefore returns no sources and NO error when the directory is absent: an error would report a deliberate privacy choice as a fault, and inventing a fallback surface would collect what the user turned off.

PRESENCE IS NOT PROOF OF EVERYTHING EITHER. The same call site skips the write for an INTERNAL prompt id, so the harness's own utility calls (next speaker checks, summarisation and the like) spend tokens that never reach this ledger. The rows that are here are exact; the ledger is a floor, not a bill, and it will read low against a provider console.

THE BUCKET COMES FROM `timestamp`, NEVER FROM THE FILE NAME. Two fields of this surface are writer-local by the harness's own documentation: the record's `localDate`/`localMonth`, and the FILE NAME, which is built from `record.localMonth`. A ledger copied from a machine in another zone keeps its original buckets, so a reader that trusts either would place events in the writing machine's calendar. `timestamp` is an ISO 8601 instant and is the only field read for time here — which is also the project rule: grouping keys are derived on read, from UTC seconds.

TOKEN SEMANTICS ARE GEMINI'S SHAPE, FILLED BY WHICHEVER WIRE ANSWERED. Qwen Code is a Gemini CLI fork and the record is built straight off `GenerateContentResponseUsageMetadata` (ApiResponseEvent copies promptTokenCount / candidatesTokenCount / cachedContentTokenCount / thoughtsTokenCount / totalTokenCount into the ledger's five counters) — but only authType gemini and vertex-ai fill that struct from a Gemini response. openai and qwen-oauth fill it in `convertOpenAIResponseToGemini`, and anthropic in a converter of its own. The SHAPE is shared; the SEMANTICS are not, and the ledger records no separate hint about which applied.

`cachedTokens` is a SUBSET of the prompt count on every one of those wires (`cachedContentTokenCount` inside `promptTokenCount`; `prompt_tokens_details.cached_tokens` inside `prompt_tokens`; `cache_read_input_tokens` folded into the anthropic converter's own prompt total), so it is mapped to CacheRead and SUBTRACTED from Input rather than added beside it. `calculateInputTokens` also falls back to the cached count when the prompt count is absent, so `inputTokens == cachedTokens` is a real record shape and Input is then legitimately zero.

`thoughtsTokens` is NOT additive here, which is the one place this surface contradicts its Gemini ancestry. On the OpenAI-compatible wires — authType openai, and qwen-oauth whose QwenContentGenerator EXTENDS OpenAIContentGenerator — the converter writes `candidatesTokenCount = usage.completion_tokens` beside `thoughtsTokenCount = usage.completion_tokens_details.reasoning_tokens`, and reasoning_tokens is a COMPONENT of completion_tokens (the converter's own estimation fallback clamps the count with `Math.min(estimated, completionTokens)`, which only holds if it sits inside). The anthropic converter writes no thoughts count at all. Only the native Gemini wire reports thoughts BESIDE candidates.

A reasoning mode is a property of the TOOL in this project (model.ReasoningModeFor), not of a row, so one rule has to cover a ledger that mixes wires: SUBSET. It is exact for openai, qwen-oauth and anthropic, and on the Gemini wire it can only UNDER-bill, which is the direction this project takes when it cannot know. See ReasoningMode below.

Reasoning is therefore not part of the total floor either. The provider's own `totalTokens` stays authoritative and is raised only when it falls below `input + cached + output` (issue #49). Adding reasoning to that floor would raise the stored total by the reasoning count of every OpenAI-wire record — an OVERSTATEMENT appended to an immutable ledger, on the wire most Qwen Code installs use. Cache read stays outside the floor for the reason it always does: it is already inside the prompt count.

The ledger reports no cache-creation count and no service tier, and it names no project or working directory, so those fields stay empty rather than carrying a guess.

PROVIDER IS LEFT UNKNOWN ON PURPOSE. `authType` is one of openai, qwen-oauth, gemini, vertex-ai, anthropic — the credential/wire kind, not the biller. Every one of them can be pointed at a third-party or local endpoint through a base URL, and since the free Qwen OAuth tier was discontinued the common configuration is exactly that: this machine's own records read authType=openai against a `gemma4:31b` served from localhost. Stamping ProviderOpenAI there would attribute local inference to OpenAI's bill. model.UsageEvent treats an empty provider as unknown and renders it as such, which is the true answer; the value is kept verbatim in the audit payload. Pricing is unaffected either way: the provider only adds namespaced lookup keys that are tried BEFORE the bare model id, never instead of it.

TURN CONTEXT. `source` is `subagent_name || "main"`, i.e. the name of the subagent that issued the request, so a record whose source is not the "main" sentinel is a subagent turn and produces one TurnContext on the agent dimension. The sentinel produces none: "main" means no subagent ran, and storing it would both invent an agent that does not exist and be indistinguishable from a real subagent named "main".

NO ACTIVITY, AND THE SECOND SURFACE IS REFUSED. This ledger records no tool call, skill invocation or hook, so the Activity stream stays empty. Qwen does write tool and skill counters — `tools.byName` / `skills.byName` inside `${QWEN_HOME:-~/.qwen}/usage_record.jsonl` — and they are deliberately not read: they are per-session COUNTS with no per-invocation identity, no timestamp and nothing to deduplicate on, so they cannot become append-only invocation rows; the same file also re-reports the session's whole token total, which the ledger already carries event by event; and the harness writes a session's summary from several independent paths (session end, transcript deletion salvage, history rebuild) whose own reader resolves the collision last-wins. A last-wins summary is not a source for an append-only ledger.

THE THREE NAMED BUG CLASSES, checked:

  • Split-identity records: does not occur. One API response is one record with its own randomUUID; nothing is streamed across records and no identity spans two lines. There is consequently no divisor to get wrong.
  • Cumulative-vs-event counting: does not occur on this surface. Each record holds THAT response's counts, never a running total, so a tail read needs no baseline and re-reading a file cannot re-add a total.
  • Assigned-not-accumulated columns: does not occur. `totalTokens` is the provider's figure for the one response the record describes.

CRITICAL: strictly observational. The ledger is opened O_RDONLY and never written, locked or rotated.

Index

Constants

View Source
const (
	RuntimeDirEnv = "QWEN_RUNTIME_DIR"
	HomeEnv       = "QWEN_HOME"
)

RuntimeDirEnv and HomeEnv name the environment variables that move the Qwen ledger, and with it everything this adapter reads. They are exported because a caller that copies this process's discovery into another one — the systemd units internal/service writes — has to know that the environment, not the defaults, decided what gets collected.

The precedence is the harness's own: QWEN_RUNTIME_DIR wins, then QWEN_HOME, then ~/.qwen. QWEN_DATA_DIR is deliberately absent — no such variable exists in the harness; a third-party parser invented it.

The harness has one rung BETWEEN those two that no environment variable exposes: `advanced.runtimeOutputDir` in settings.json (Storage's own order is QWEN_RUNTIME_DIR > runtimeOutputDir > QWEN_HOME > ~/.qwen). It is not read here, and it cannot be read reliably: settings merge across scopes and a relative value resolves against the WORKSPACE the qwen process was started in, which a polling daemon has no way to enumerate. An install that sets it therefore collects nothing rather than something wrong — the same silence as an opted-out install, and the reason absence is never reported as a fault.

View Source
const ReasoningMode = model.ReasoningSubset

ReasoningMode is the reasoning-billing rule this surface reports, so the fact travels with the adapter that measured it: SUBSET. thoughtsTokens is INSIDE outputTokens on the OpenAI-compatible wires (authType openai and qwen-oauth — the wire this machine's own records took), and is never written at all on the anthropic one; only the native Gemini wire reports it beside output. One tool id cannot carry two rules, so the mode is the one that is exact on three wires and can only under-bill on the fourth. Billing this ADDITIVE would charge the reasoning tokens of every OpenAI-wire turn twice, since the output count it sits inside is already being charged.

It is the same value model.ReasoningModeFor returns for a tool it does not know, so nothing is mis-billed while the adapter is unregistered. Registering it in model.reasoningModes is still part of wiring the adapter up: the fallback is a default, and this is a measurement.

Variables

This section is empty.

Functions

func New

func New() adapter.Adapter

New returns a Qwen Code adapter.

Types

type Adapter

type Adapter struct{}

Adapter reads the Qwen Code usage ledger. Read-only.

func (Adapter) Capabilities

func (Adapter) Capabilities() model.ToolCapability

Capabilities declares what this project can say about Qwen Code.

Cost is COMPUTED: nothing here calls SetCost. There is NO activity at all — this adapter references model.ActivityEvent nowhere; the harness's own usage ledger records turns, not calls.

func (Adapter) Collect

func (a Adapter) Collect(ctx context.Context, src adapter.Source) (adapter.Observation, error)

Collect reads one ledger file in full and returns its usage events.

func (Adapter) CollectIncremental

func (a Adapter) CollectIncremental(ctx context.Context, src adapter.Source, cp *model.SourceCheckpoint) (adapter.Observation, error)

CollectIncremental reads only what is new since cp: an unchanged size+mtime skips the file entirely; growth tail-reads from the stored offset; any shrink or same-size rewrite re-reads from zero. A nil cp is a full read.

The tail read needs NO carried state, and that is a property of the surface rather than an omission: every record holds its own response's counts and its own UUID, so a record read from byte 40,000 means exactly what it would have meant read from byte 0. Re-reading is therefore always safe — the re-derived dedup keys collapse in the store — which is why every uncertain case above restarts from zero instead of guessing.

func (Adapter) Discover

func (a Adapter) Discover(ctx context.Context, cfg adapter.DiscoverConfig) ([]adapter.Source, error)

Discover lists the monthly ledger files under <root>/usage.

A missing usage/ directory yields no sources and no error: the harness only writes it while privacy.usageStatisticsEnabled is on, so its absence means "never ran OR opted out" and neither is a fault to report. Files are listed non-recursively and matched on the harness's own name shape (token-usage-*.jsonl) — the month in that name is writer-local metadata and is never parsed, here or anywhere else in this package.

func (Adapter) DisplayName

func (Adapter) DisplayName() string

DisplayName returns the human-friendly name.

func (Adapter) ID

func (Adapter) ID() string

ID returns the stable tool identifier.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL