Skip to content

feat(metering): wrapper 完整版 invoke 聚合+hash 链(W2-C3 .github#216,ADR-0062) - #54

Merged
randypanding merged 1 commit into
mainfrom
w2c3-metering-wrapper
Aug 21, 2026
Merged

feat(metering): wrapper 完整版 invoke 聚合+hash 链(W2-C3 .github#216,ADR-0062)#54
randypanding merged 1 commit into
mainfrom
w2c3-metering-wrapper

Conversation

@randypanding

Copy link
Copy Markdown
Contributor

动机

宪法 §4A 要求一切 LLM 调用可计量、可归账、可审计。spike T8 暴露:流式响应按分片打点导致一次 invoke 被计成 N 条(成本虚增、去重失败);cost-check 的 LLM 预算通道数据源一直是 pending(ADR-0040 决策 6)。本 PR 把计量 wrapper 从第一期形态(scripts/llm-call.sh,ADR-0048)升完整版:invoke 聚合 + 产物 hash 链 + INV-06 静态扫描 + 账本同步管道(metering-ledger 分支,cost-check LLM 预算通道数据源——配套 .github 仓 PR 接消费端)。

决策契约:ADR-0062(已合 archive main)。

变更清单

  • pipeline/metering/metering-wrapper.sh——bash 入口,一切 LLM 调用唯一入口(直连 provider API,AR-3 第一期);流式/非流式、离线回放(--replay-file,自测/重放审计);GATE_METERING_DIR 指定 JSONL 账本目录
  • pipeline/metering/metering.py——核心单点:mkreq(请求体组装)、emit(一次 invoke 恰一条记录:SSE 分片聚合在唯一实现点、schema 断言、hash 链追加、invoke_id 去重执法、llm-usage/v1 兼容件)、verify(验链)、aggregate(按角色档归账)、relink(账本续接)。零 jq/三方依赖(内置最小 JSON Schema 校验器,fail-closed)
  • pipeline/metering/record.schema.json——metering-record/v1:BEH-09 字段齐全(model/prompt 版本/seed/采样参数 temperature·top_p·max_tokens·thinking/输入输出 token/耗时/http+exit 状态/chunks 聚合证据/产物 hash)+ 链字段(record_index/prev_record_sha256/record_sha256)
  • pipeline/metering/metering-verify.sh——验链工具(--dir 全部周片 / --file 断点定位)
  • pipeline/metering/scan-direct-sdk.sh + scan-patterns.yaml——INV-06 直连静态扫描(5 类模式:裸 openai/anthropic import、client 实例化、curl/requests 直打 provider 端点),模式表版本化可配置,豁免面显式(wrapper 本体 + 一期入口 + 自测样本),行内豁免留痕(# metering-allow: <id>
  • pipeline/metering/ledger-sync.sh——账本同步管道:本地与远端各自验链 → relink 续接 → 合并片整链复验 → contents API 写回 metering-ledger 分支(--dry-run 预演通道)
  • pipeline/metering/selftest/——自测(零真实 LLM 调用)+ fixtures;入 ci.yml 新 job metering-selftest(并挂 gate needs)
  • 接线:llm-connectivity.yml 调用改经 wrapper(--role probe)+ 新增验链/同步步(job 级 contents:write,推本仓 metering-ledger);spec-author.yml 调用改经 wrapper(--role spec-author,--usage-compat-dir 保既有下游 usage-path.txt/spec-pr.py 零改动)。钉版说明:spec-author 由 conductor 钉 ciw_ref 调用,workflow 与被 checkout 代码同源同钉点——本变更仅在钉版推进到含本 PR 的 SHA 后生效,无中间态破坏(未动任何钉版指针)

AC 映射(.github#216)

  • AC-1(含流式调用→恰一条聚合记录,字段过 schema):Given 一次 5 分片流式 invoke(fixture stream-response.sse 离线回放),When wrapper 记录,Then 账本恰 1 条、chunks=5、正文="Hello world!"、usage=终块聚合值(120/45/165);同 invoke_id 二次落账被拒(exit 3,聚合键=invoke_id 机器执法)。证据:自测 T1/T1b/T1c 全 PASS
  • AC-2(BEH-09 字段齐全 + hash 链可验):Given 落盘记录,Then model/prompt_version(sha256)/seed/采样参数/用量/耗时/exit_status/产物 hash(request+response artifacts)/链字段逐项断言 PASS(T2);未篡改验链绿 → 改中间记录 usage.completion_tokens 45→999 → 验链红(record_sha256 重算不符定位)→ 复原复绿;schema 负控制(多余字段)红(T3)
  • AC-3(无绕过 wrapper 的直连 SDK 调用):Given fixture 代码双形态,When scan-direct-sdk.sh,Then 含直连样本命中全部 4 类模式 exit 1、合规样本(经 wrapper)绿、行内豁免放行留痕、模式表不可读 fail-closed exit 2(T4);仓级默认扫描本 PR 后绿(wrapper 本体/一期入口/自测样本为显式豁免面)
  • AC-4(cost-check 读归账)数据源侧:aggregate 按角色档归账 probe=165/spec-author=42/合计=207,窗口过滤生效,坏账本拒归账(T5);ledger-sync 干跑(gh 桩)全链预演通过(T7)。消费端在 .github 仓配套 PR(cost-check LLM 通道)

测试方法

  • 本地:METERING_PYTHON=python bash pipeline/metering/selftest/run-selftest.sh → 37 行 PASS + SELFTEST PASS:全部断言绿(零真实 LLM 调用)
  • CI:ci.yml 新增 metering-selftest job(自测 + 仓级 INV-06 扫描)挂入 gate
PASS  T1 5 分片聚合成恰 1 条 invoke 记录(spike T8 教训不复发)
PASS  T1c 同 invoke_id 二次落账被拒(exit 3)(rc=3)
PASS  T2 BEH-09 字段齐全 + 自哈希重算一致 + 链前驱正确
PASS  T3 篡改中间字段 → 验链红(负控制)(rc=3)
PASS  T4 含直连 fixture → 命中红(rc=1)   [4 类模式全检出]
PASS  T5 角色档归账正确(probe=165 / spec-author=42 / 合计=207)
PASS  T6 兼容件字段可被既有下游消费(model/tokens/response_sha256)
PASS  T7 ledger-sync 干跑:建分支/创世续接/合并片验链/写回预演通过
SELFTEST PASS:全部断言绿(零真实 LLM 调用)

风险与回滚

  • wrapper 是热路径单点(ADR-0062 后果节已声明);llm-call.sh 保留为回滚入口(豁免面内),spec-author/llm-connectivity 回滚=改回一行调用
  • metering-ledger 分支推送仅 llm-connectivity(每 6h 1 条记录,体量可忽略);账本 JSONL 落 archive 策略随飞轮卡落位
  • 计量缺失按 §5 语义:schema 不过/链断一律 exit 3 红,不静默降级;扫描模式表漏报由红队回流增行(版本化 PR)
  • 合并顺序:本 PR 与 .github 仓 cost-check LLM 通道 PR 互不阻塞(数据源分支不存在时消费端按"零用量"声明处理)

Card: Cloudbird-Software/.github#216

…ithub#216,ADR-0062)

- pipeline/metering/:metering-wrapper.sh(bash 入口,直连 provider API AR-3 一期;
  流式 SSE 分片在 emit 单点聚合——spike T8 分片重复计数教训)、metering.py
  (emit/verify/aggregate/relink,零 jq 依赖)、record.schema.json(BEH-09 字段,
  内置最小 schema 校验器)、metering-verify.sh(验链)、scan-direct-sdk.sh +
  scan-patterns.yaml(INV-06 直连模式表,版本化)、ledger-sync.sh(metering-ledger
  分支数据源管道:relink 续接+写回前整链复验)
- 自测 selftest/(零真实 LLM 调用,--replay-file 离线回放):T1 聚合恰一条+invoke_id
  去重执法、T2 BEH-09 字段齐全、T3 篡改负控制(改中间字段→验链红)、T4 扫描双形态、
  T5 按角色档归账、T6 llm-usage/v1 兼容件、T7 ledger-sync 干跑——入 ci.yml 新 job
- 接线:llm-connectivity/spec-author 调用改经 wrapper(--role probe/spec-author;
  兼容件保既有下游零改动);llm-connectivity 增账本验链+同步步(job 级 contents:write);
  spec-author 账本不外推(App token 目标仓单仓作用域 AG-2,随 conductor 波次接入)
Copilot AI lite review requested due to automatic review settings August 21, 2026 17:59
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@randypanding, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

Limit details: You’ve used all 10 included reviews currently available.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9aeed9b1-6381-4525-a873-d70591b17d58

📥 Commits

Reviewing files that changed from the base of the PR and between 2520ea1 and ad6b968.

📒 Files selected for processing (18)
  • .github/workflows/ci.yml
  • .github/workflows/llm-connectivity.yml
  • .github/workflows/spec-author.yml
  • pipeline/metering/ledger-sync.sh
  • pipeline/metering/metering-verify.sh
  • pipeline/metering/metering-wrapper.sh
  • pipeline/metering/metering.py
  • pipeline/metering/record.schema.json
  • pipeline/metering/scan-direct-sdk.sh
  • pipeline/metering/scan-patterns.yaml
  • pipeline/metering/selftest/fixtures/bad-direct.py
  • pipeline/metering/selftest/fixtures/bad-with-allow.py
  • pipeline/metering/selftest/fixtures/ok-via-wrapper.sh
  • pipeline/metering/selftest/fixtures/plain-response.json
  • pipeline/metering/selftest/fixtures/prompt.txt
  • pipeline/metering/selftest/fixtures/stream-expected.txt
  • pipeline/metering/selftest/fixtures/stream-response.sse
  • pipeline/metering/selftest/run-selftest.sh

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

feat(metering): invoke 聚合 + hash 链账本 + 直连扫描 + ledger 同步

✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• 引入计量 wrapper:一次 invoke 恰一条记录,支持流式聚合与 invoke_id 去重
• 增加 hash 链账本与验链/续接/归账能力,作为 cost-check 预算数据源
• 接线 CI/workflow 并加入自测与 INV-06 直连 SDK 静态扫描,默认 fail-closed
Diagram

graph TD
  A["GitHub Workflows\n(ci / llm-connectivity / spec-author)"] --> B["metering-wrapper.sh"] --> C["metering.py\n(mkreq/emit)"] --> D[".metering\nrecords-*.jsonl"]
  D --> E["metering-verify.sh\n(metering.py verify)"]
  D --> F["ledger-sync.sh\n(relink + verify)"] --> G["metering-ledger branch\n(contents API)"]
  A --> H["scan-direct-sdk.sh\n(INV-06)" ]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use jsonschema library for validation
  • ➕ More complete JSON Schema coverage and better error reporting
  • ➕ Less custom code to maintain as schema evolves
  • ➖ Adds dependency management to runner/local env (contrary to “zero third-party deps” goal)
  • ➖ Potential supply-chain and version drift concerns for a security/audit critical path
2. Emit telemetry to OTel/metrics backend instead of git-branch ledger
  • ➕ Avoids contents API conflicts and branch storage growth
  • ➕ Better suited for near-real-time aggregation/dashboards
  • ➖ Requires standing infrastructure + credentials + retention policy
  • ➖ Harder to treat as an immutable audit artifact without additional controls
3. Store ledger as GitHub Artifact/Release asset per run
  • ➕ No write permission to repository contents needed
  • ➕ Natural run-scoped immutability and easier provenance per workflow run
  • ➖ Harder to build a single continuous hash chain across runs
  • ➖ Consumption by cost-check becomes more complex (artifact enumeration/download)

Recommendation: The PR’s approach (hash-chained JSONL + fail-closed verification + optional sync to a dedicated ledger branch) is appropriate for auditability and for making cost-check’s budget channel deterministic. Given the explicit requirement for “zero third-party deps” and repository-native storage, the minimal in-repo schema validator and contents-based sync are reasonable; the main tradeoff to watch is branch growth and occasional 409 conflicts, which the design already treats as “retry and relink” rather than risking overwrite.

Files changed (18) +1299 / -8

Enhancement (6) +971 / -0
ledger-sync.shAdd ledger sync pipeline to metering-ledger branch +97/-0

Add ledger sync pipeline to metering-ledger branch

• Implements a sync tool that fetches remote weekly shards, relinks local records onto the remote base chain, re-verifies integrity, then writes back via GitHub contents API. Supports '--dry-run' and refuses to overwrite a corrupted remote chain (fail-closed).

pipeline/metering/ledger-sync.sh

metering-verify.shAdd metering ledger verification CLI +22/-0

Add metering ledger verification CLI

• Adds a small bash wrapper to run 'metering.py verify' against a directory or a single shard file. Standardizes exit codes for workflow gating and local auditing.

pipeline/metering/metering-verify.sh

metering-wrapper.shAdd the full metering wrapper entrypoint for LLM calls +144/-0

Add the full metering wrapper entrypoint for LLM calls

• Creates the canonical bash entrypoint for all LLM calls, supporting streaming/non-streaming, offline replay, and usage-compat output. Delegates request construction and one-record-per-invoke emission (including stream aggregation) to 'metering.py', and fails closed if metering cannot be proven.

pipeline/metering/metering-wrapper.sh

metering.pyImplement core metering engine: emit/verify/aggregate/relink +496/-0

Implement core metering engine: emit/verify/aggregate/relink

• Introduces the Python single source of truth for request building, response parsing (including SSE chunk aggregation), schema validation, hash-chain append, invoke_id uniqueness enforcement, and chain verification. Adds aggregation by role for budget accounting and relink logic for safe merges during ledger sync, while keeping dependencies to stdlib only.

pipeline/metering/metering.py

record.schema.jsonDefine metering-record/v1 schema for audited records +121/-0

Define metering-record/v1 schema for audited records

• Adds a strict JSON Schema for metering records including BEH-09 fields, artifact hashes, stream chunk evidence, and hash-chain fields. Sets 'additionalProperties: false' to enforce a closed schema and enable fail-closed validation.

pipeline/metering/record.schema.json

scan-direct-sdk.shAdd INV-06 static scan for direct SDK/provider calls +91/-0

Add INV-06 static scan for direct SDK/provider calls

• Adds a repository scanner driven by a versioned YAML pattern table to detect direct OpenAI/Anthropic SDK usage and direct HTTP calls to provider endpoints. Supports explicit exempt paths and inline allow markers while remaining fail-closed on configuration/parse errors.

pipeline/metering/scan-direct-sdk.sh

Tests (8) +238 / -0
bad-direct.pyAdd failing fixtures for direct-call scan +8/-0

Add failing fixtures for direct-call scan

• Adds a deliberately non-compliant Python sample that should trigger multiple scan patterns (SDK import, client init, direct HTTP, curl endpoint). Used to assert scan coverage in selftests.

pipeline/metering/selftest/fixtures/bad-direct.py

bad-with-allow.pyAdd fixture for inline scan exemption behavior +3/-0

Add fixture for inline scan exemption behavior

• Adds a fixture that matches a scan pattern but includes an inline 'metering-allow' marker. Used to validate that exemptions are explicit and leave an audit trail in output.

pipeline/metering/selftest/fixtures/bad-with-allow.py

ok-via-wrapper.shAdd compliant fixture showing wrapper usage +5/-0

Add compliant fixture showing wrapper usage

• Adds a small shell fixture demonstrating LLM invocation via the metering wrapper without embedding provider endpoints or SDK usage. Used to validate scan non-matches.

pipeline/metering/selftest/fixtures/ok-via-wrapper.sh

plain-response.jsonAdd non-stream replay response fixture +1/-0

Add non-stream replay response fixture

• Adds a minimal JSON response fixture with choices/content and usage tokens. Used for offline replay tests to avoid real provider calls.

pipeline/metering/selftest/fixtures/plain-response.json

prompt.txtAdd prompt fixture for offline replay +1/-0

Add prompt fixture for offline replay

• Adds a fixed prompt text used across selftests to generate stable prompt_version hashes and validate schema fields. Keeps tests deterministic and network-free.

pipeline/metering/selftest/fixtures/prompt.txt

stream-expected.txtAdd expected aggregated stream output +1/-0

Add expected aggregated stream output

• Adds the expected final content after aggregating the streamed SSE deltas. Used to assert that chunk aggregation produces the correct single output string.

pipeline/metering/selftest/fixtures/stream-expected.txt

stream-response.sseAdd streamed SSE replay fixture with final usage block +11/-0

Add streamed SSE replay fixture with final usage block

• Adds a multi-line SSE fixture simulating a 5-chunk stream plus a terminal usage block. Used to validate the one-record-per-invoke aggregation behavior and usage extraction logic.

pipeline/metering/selftest/fixtures/stream-response.sse

run-selftest.shAdd deterministic metering selftest suite (T1–T7) +208/-0

Add deterministic metering selftest suite (T1–T7)

• Introduces a full selftest harness validating stream aggregation, schema completeness, tamper detection, scan enforcement, role-based aggregation, usage-compat output, and ledger-sync dry run. Runs entirely offline via '--replay-file' fixtures and provides explicit rc assertions for gating.

pipeline/metering/selftest/run-selftest.sh

Other (4) +90 / -8
ci.ymlAdd metering selftest job and gate it +21/-1

Add metering selftest job and gate it

• Adds a new CI job 'metering-selftest' that runs T1–T7 without real LLM calls and runs the default INV-06 scan. Updates the 'gate' job to depend on this new job.

.github/workflows/ci.yml

llm-connectivity.ymlRoute connectivity probe through metering + sync ledger +22/-4

Route connectivity probe through metering + sync ledger

• Switches the probe call from 'scripts/llm-call.sh' to 'pipeline/metering/metering-wrapper.sh' with '--role probe' and usage compat output. Adds concurrency constraints and a final step to verify and sync '.metering' to the 'metering-ledger' branch with job-level 'contents: write'.

.github/workflows/llm-connectivity.yml

spec-author.ymlSwitch spec-author workflow to the full metering wrapper +10/-3

Switch spec-author workflow to the full metering wrapper

• Replaces 'scripts/llm-call.sh' with 'pipeline/metering/metering-wrapper.sh' and passes role-based accounting parameters. Adds '--usage-compat-dir' to preserve existing downstream consumption and runs ledger verification after the call.

.github/workflows/spec-author.yml

scan-patterns.yamlIntroduce versioned pattern table and exemptions for INV-06 scan +37/-0

Introduce versioned pattern table and exemptions for INV-06 scan

• Defines regex patterns for direct SDK imports/initialization and direct HTTP endpoint hits, along with scan globs and exempt paths for the wrapper itself and legacy entrypoints. Documents the inline exemption mechanism 'metering-allow: <id>'.

pipeline/metering/scan-patterns.yaml

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a “full” LLM metering wrapper pipeline for CI-Workflows, aiming to ensure each LLM invoke is measurable/auditable (single aggregated record per invoke), tamper-evident (hash chain), and enforceable (INV-06 direct-SDK static scan), with CI selftests and workflow wiring to produce/verify/sync ledgers.

Changes:

  • Added a new metering core (metering.py) + JSON Schema (record.schema.json) implementing invoke aggregation, schema assertions, hash chaining, verification, and role-based aggregation (incl. llm-usage/v1 compatibility output).
  • Added ledger operational tooling: verification wrapper, direct-SDK scanner (configurable patterns), ledger sync to metering-ledger, plus an offline replay selftest suite wired into CI.
  • Updated workflows to route LLM calls through the new wrapper and (for connectivity) verify + sync the ledger.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pipeline/metering/selftest/run-selftest.sh Adds offline replay + negative-control selftests T1–T7 for wrapper/metering pipeline.
pipeline/metering/selftest/fixtures/stream-response.sse Adds SSE stream fixture used to validate chunk aggregation and usage extraction.
pipeline/metering/selftest/fixtures/stream-expected.txt Adds expected aggregated stream output fixture.
pipeline/metering/selftest/fixtures/prompt.txt Adds prompt fixture for offline replay tests.
pipeline/metering/selftest/fixtures/plain-response.json Adds non-stream response fixture for emit/compat output tests.
pipeline/metering/selftest/fixtures/ok-via-wrapper.sh Adds “compliant” fixture for direct-SDK scan selftest.
pipeline/metering/selftest/fixtures/bad-with-allow.py Adds scan fixture demonstrating inline allow marker handling.
pipeline/metering/selftest/fixtures/bad-direct.py Adds intentionally non-compliant fixture to ensure scanner detects violations.
pipeline/metering/scan-patterns.yaml Adds versioned, configurable direct-SDK detection patterns + exemptions.
pipeline/metering/scan-direct-sdk.sh Adds scanning entrypoint implementing pattern-based detection and inline exemptions.
pipeline/metering/record.schema.json Adds metering-record/v1 JSON Schema for fail-closed record validation.
pipeline/metering/metering.py Adds core logic: request building, response aggregation, emit/verify/aggregate/relink.
pipeline/metering/metering-wrapper.sh Adds unified metering wrapper for online calls and offline replay; emits ledger records.
pipeline/metering/metering-verify.sh Adds CLI wrapper for verifying ledger chain validity.
pipeline/metering/ledger-sync.sh Adds contents-API based sync pipeline to a ledger branch with relink + re-verify gates.
.github/workflows/spec-author.yml Switches spec-author workflow to use the new wrapper + verify + usage-compat output.
.github/workflows/llm-connectivity.yml Switches connectivity probe to wrapper; adds ledger verify + sync; adds concurrency + job-level permissions.
.github/workflows/ci.yml Adds metering-selftest job and wires it into the gate.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +149 to +153
if not stream:
try:
body = json.loads(raw)
except json.JSONDecodeError as e:
return "", 0, None, f"响应非 JSON:{e}"
Comment on lines +471 to +473
p = sub.add_parser("verify")
p.add_argument("--dir", default=None, help="账本目录(验全部周片);与 --file 互斥省略")
p.add_argument("--file", help="单个账本片(断点定位)")
Comment on lines +56 to +71
hits, allows = 0, 0
for root in paths:
base = root if os.path.isdir(root) else (os.path.dirname(root) or ".")
if os.path.isfile(root):
walk = [(os.path.dirname(root) or ".", [], [os.path.basename(root)])]
else:
walk = os.walk(root)
for dirpath, dirnames, filenames in walk:
dirnames[:] = [d for d in dirnames if d not in (".git", "__pycache__", "node_modules")]
for fn in filenames:
if not any(fnmatch.fnmatch(fn, g) for g in globs):
continue
full = os.path.join(dirpath, fn)
rel = os.path.relpath(full, base).replace("\\", "/")
if any(e.search(rel) for e in exempt):
continue
Comment on lines +8 to +28
"required": [
"schema",
"invoke_id",
"role",
"model",
"ts_start",
"ts_end",
"prompt_version",
"prompt_bytes",
"sampling",
"usage",
"latency_ms",
"http_status",
"exit_status",
"stream",
"chunks",
"artifacts",
"record_index",
"prev_record_sha256",
"record_sha256"
],
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (8) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Remote records silently skipped 🐞 Bug ≡ Correctness
Description
ledger-sync.sh incorrectly treats a remote shard with at least as many lines as the current local
shard as already synchronized, without verifying invoke_id or record/content identity, so new runs
are silently skipped. Because each llm-connectivity workflow run typically produces a fresh
one-record local .metering shard while the remote shard is cumulative, every run after the first
fails to upload and downstream cost-check ledger data is continuously missing.
Code

pipeline/metering/ledger-sync.sh[R73-77]

+    REMOTE_N=$(wc -l <"$BASE" | tr -d ' ')
+    LOCAL_M=$(wc -l <"$LOCAL" | tr -d ' ')
+    if [[ "$REMOTE_N" -ge "$LOCAL_M" ]]; then
+      echo "OK $NAME:远端 $REMOTE_N 条 ≥ 本地 $LOCAL_M 条——本片无需同步(幂等跳过)"
+      continue
Relevance

●●● Strong

Clear correctness bug: cumulative remote line count skips distinct local records before relink.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The cited logic in ledger-sync.sh decides to skip syncing purely from a line-count comparison
(REMOTE_N >= LOCAL_M) and does so before running relink, even though relink is the step that
actually merges/attaches local records onto the remote chain. Since the connectivity workflow
invokes the script with a newly generated, ephemeral .metering directory each run (often
containing only the current run’s single invoke record), the local and remote can both be one line
(or remote larger) while still representing different invoke_ids and different content; after the
first successful upload, the remote’s cumulative count will typically be >= the local count,
triggering the premature skip and dropping subsequent records.

pipeline/metering/ledger-sync.sh[70-81]
pipeline/metering/metering.py[416-428]
.github/workflows/llm-connectivity.yml[54-73]
pipeline/metering/ledger-sync.sh[47-51]
.github/workflows/llm-connectivity.yml[48-57]
.github/workflows/llm-connectivity.yml[68-73]
pipeline/metering/metering.py[286-291]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Remote line count is not evidence that the current run’s local invoke records are already present on the remote ledger; using `REMOTE_N >= LOCAL_M` as an idempotency check causes new, different `invoke_id` records (often the only record in an ephemeral local `.metering`) to be silently skipped and never uploaded.

## Issue Context
Each workflow run generally produces a small independent local shard (commonly a single new record) while the remote shard is cumulative across runs, so length comparisons between two independent ledgers do not prove content equality. The script currently skips before `relink`, even though `relink` is what can connect the local shard onto the remote chain tail; the sync decision should instead verify whether the remote actually already contains the local record(s) (e.g., by `invoke_id` or record/hash identity and chain contents). Preserve existing concurrency PUT protections (SHA/conflict protection).

## Fix Focus Areas
- pipeline/metering/ledger-sync.sh[70-81]
- pipeline/metering/metering.py[397-428]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Concurrent appends break chain 🐞 Bug ☼ Reliability
Description
emit reads the shard tail, derives the next record_index and predecessor hash, and appends the
JSONL record without mutual exclusion, so concurrent wrapper invocations can both use the same
observed tail and index/prev values. Both calls can return success while leaving a forked/invalid
JSONL hash-chain sequence that later fails verification and downstream accounting.
Code

pipeline/metering/metering.py[R281-285]

+    prev, index = None, 0
+    if os.path.isfile(shard_path):
+        records, _ = read_records(shard_path)
+        if records:
+            prev, index = records[-1]["record_sha256"], len(records)
Relevance

●● Moderate

Concurrency failure is plausible and material, but no closely matching repository precedent was
found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In pipeline/metering/metering.py around lines 281–285, emit reads the current shard tail and
computes the next chain position (index and previous-hash) before writing; duplicate detection is
also performed as an unlocked scan, and the final record is appended using plain append mode without
a lock. Because the verifier requires each record’s physical order to be strictly continuous—each
sequential record must have exactly the prior record’s hash and the expected incremented index—two
processes that observe the same tail can each construct a record with identical
record_index/prev and append in sequence, causing the second appended record to immediately
violate index/prev validation and break subsequent chain verification/settlement.

pipeline/metering/metering.py[281-291]
pipeline/metering/metering.py[316-317]
pipeline/metering/metering.py[220-236]
pipeline/metering/metering.py[281-308]
pipeline/metering/metering.py[221-229]
pipeline/metering/metering-wrapper.sh[113-135]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Concurrent `emit` calls can read the same shard tail, compute identical chain fields (`record_index` and predecessor hash), and append two “successful” records that permanently fork/break the per-week JSONL hash chain and later fail verification/accounting.

## Issue Context
Tail reading, cross-file `invoke_id` de-duplication, chain field computation (`index`/`prev`/`hash`), and the actual append-to-disk must be treated as a single mutually exclusive transaction per ledger directory/shard, and the locking must work across separate wrapper processes. The lock must cover more than just the final `write()` call; otherwise two processes can still derive chain fields from the same old tail and produce conflicting records.

## Fix Focus Areas
- pipeline/metering/metering.py[281-317]
- pipeline/metering/metering.py[212-238]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Invalid ledger accepts appends 🐞 Bug ☼ Reliability
Description
emit ignores parse errors and never verifies the existing shard before deriving its tail, so a
malformed, tampered, or already broken ledger still accepts another record and the wrapper reports
success. This violates the wrapper's fail-closed contract and extends an untrustworthy audit chain
until a separate optional verification is run.
Code

pipeline/metering/metering.py[R282-285]

+    if os.path.isfile(shard_path):
+        records, _ = read_records(shard_path)
+        if records:
+            prev, index = records[-1]["record_sha256"], len(records)
Relevance

●● Moderate

Fail-closed ledger validation is semantically important, but no closely matching repository
precedent was found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
read_records returns parse errors separately, but emit discards them and trusts the final parsed
record. Full schema/hash/chain validation exists only in verify_ledger; emit does not call it
before appending.

pipeline/metering/metering.py[197-209]
pipeline/metering/metering.py[212-238]
pipeline/metering/metering.py[281-291]
pipeline/metering/metering.py[316-317]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Emit appends to an existing ledger without validating its JSON, schema, hashes, indexes, or invoke uniqueness first.

## Issue Context
Validation must occur inside the same locked append transaction so the checked state cannot change before writing.

## Fix Focus Areas
- pipeline/metering/metering.py[281-291]
- pipeline/metering/metering.py[212-238]
- pipeline/metering/metering.py[316-317]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (2)
4. Multiline calls bypass scanner 🐞 Bug ≡ Correctness
Description
The scanner applies every regex to one physical line, while the curl and HTTP-library patterns
require the client name and provider hostname on that same line. A direct call split with shell
continuation or a multiline Python call passes INV-06 scanning despite directly invoking a provider.
Code

pipeline/metering/scan-direct-sdk.sh[R76-78]

+            for lineno, line in enumerate(text.splitlines(), 1):
+                for pid, desc, rx in compiled:
+                    if rx.search(line):
Relevance

●● Moderate

Scanner bypass is plausible, but historical evidence supports scanner coverage generally rather than
multiline matching specifically.

PR-#8

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The implementation calls each compiled regex on an individual line. Both endpoint patterns
explicitly require curl/HTTP library text followed by the provider domain in the same regex match,
so separating those tokens across lines yields no match.

pipeline/metering/scan-direct-sdk.sh[72-85]
pipeline/metering/scan-patterns.yaml[28-33]
pipeline/metering/selftest/run-selftest.sh[142-151]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Line-by-line matching misses direct provider calls whose command and endpoint appear on different lines.

## Issue Context
At minimum, normalize shell continuations and scan multiline text; preferably parse supported languages or add robust multiline patterns and negative fixtures.

## Fix Focus Areas
- pipeline/metering/scan-direct-sdk.sh[72-85]
- pipeline/metering/scan-patterns.yaml[28-33]
- pipeline/metering/selftest/run-selftest.sh[142-151]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. 扫描豁免覆盖整个目录 🐞 Bug ⛨ Security
Description
pipeline/metering/ 被整体排除而非仅排除 wrapper 实现,因此任何后来加入该目录的 Python、Shell 或 JS 文件都可直连 provider/SDK 而 CI
扫描仍会通过。该目录级豁免直接绕过了仓级 INV-06 门禁。
Code

pipeline/metering/scan-patterns.yaml[R14-16]

+exempt_paths:
+  - '^pipeline/metering/'
+  - '^scripts/llm-call\.sh$'
Relevance

●● Moderate

Broad security exemption weakens future enforcement, but historical security precedents are about
adjacent scope and permissions.

PR-#5
PR-#8

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
默认 CI 从仓根运行扫描;扫描器会在逐行匹配前跳过任何匹配 exempt_paths 的相对路径。当前正则匹配整个新增目录。

pipeline/metering/scan-patterns.yaml[7-16]
pipeline/metering/scan-direct-sdk.sh[57-85]
.github/workflows/ci.yml[110-113]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
扫描配置将整个 `pipeline/metering/` 子树排除。扫描器在应用任意 SDK/provider 正则前即跳过匹配路径,故该目录中新建的直连调用不会被发现。

## Issue Context
wrapper 自身确实需要调用 provider,但豁免应是最小路径集合。自测 fixture 也应按精确路径处理,避免扩大可绕过范围。

## Fix Focus Areas
- pipeline/metering/scan-patterns.yaml[14-16]
- pipeline/metering/scan-direct-sdk.sh[63-85]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Explicit scans ignore exemptions 🐞 Bug ≡ Correctness
Description
For explicitly supplied paths, exemption matching uses paths relative to the scanned directory or
file parent rather than the repository root. Scanning pipeline/metering/ directly therefore sees
metering-wrapper.sh, not pipeline/metering/metering-wrapper.sh, and falsely flags the wrapper's
authorized provider curl.
Code

pipeline/metering/scan-direct-sdk.sh[R58-60]

+    base = root if os.path.isdir(root) else (os.path.dirname(root) or ".")
+    if os.path.isfile(root):
+        walk = [(os.path.dirname(root) or ".", [], [os.path.basename(root)])]
Relevance

●●● Strong

Deterministic path-base bug prevents configured repository-root exemptions during explicit directory
scans.

PR-#8

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
For a directory, base is set to that directory; for a file, it is set to its parent. Exemptions
are configured as repository-root-relative regexes, and the wrapper contains the provider curl that
those exemptions are intended to authorize.

pipeline/metering/scan-direct-sdk.sh[57-70]
pipeline/metering/scan-patterns.yaml[14-16]
pipeline/metering/metering-wrapper.sh[90-98]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Explicit-path scans compute relative paths from inconsistent bases, preventing configured repository-relative exemptions from matching.

## Issue Context
Use the configured repository root as the stable base for all files, including explicitly supplied files/directories, and add tests for both forms.

## Fix Focus Areas
- pipeline/metering/scan-direct-sdk.sh[57-70]
- pipeline/metering/scan-patterns.yaml[14-16]
- pipeline/metering/selftest/run-selftest.sh[142-151]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Tag argument is discarded 🐞 Bug ≡ Correctness
Description
The wrapper parses and documents --tag but never passes it to emit; compatibility records
hard-code tag to the accounting role instead. The changed connectivity workflow explicitly
supplies --role probe --tag connectivity, yet its llm-usage/v1 output now says probe, breaking
the prior wrapper's stage-tag semantics.
Code

pipeline/metering/metering-wrapper.sh[R49-50]

+    --role)            ROLE="${2:?}"; shift 2 ;;
+    --tag)             TAG="${2:?}"; shift 2 ;;
Relevance

●●● Strong

Clear argument propagation bug: parsed tag is never forwarded, producing observably incorrect
compatibility metadata.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The old wrapper defines --tag as the stage tag and emits that exact value. The new wrapper parses
TAG but does not include it in EMIT_ARGS, while compatibility generation uses a.role; the
connectivity change provides distinct role and tag values, proving the observable mismatch.

scripts/llm-call.sh[9-17]
scripts/llm-call.sh[79-100]
pipeline/metering/metering-wrapper.sh[37-61]
pipeline/metering/metering-wrapper.sh[116-130]
pipeline/metering/metering.py[321-334]
.github/workflows/llm-connectivity.yml[54-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The accepted `--tag` value is discarded, so compatibility output changes caller-visible metadata.

## Issue Context
Keep role as the ledger accounting key, but pass tag separately through emit and use it for the `llm-usage/v1` compatibility record and filename.

## Fix Focus Areas
- pipeline/metering/metering-wrapper.sh[37-61]
- pipeline/metering/metering-wrapper.sh[116-130]
- pipeline/metering/metering.py[321-334]
- .github/workflows/llm-connectivity.yml[54-56]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. 异常响应触发未处理解包 🐞 Bug ☼ Reliability
Description
非流式 2xx 响应在返回体不是 JSON(如 HTML/畸形)时,parse_response() 在 JSON 解析失败分支返回四个值,但 cmd_emit()
始终按三个值解包,导致先抛出 ValueError/traceback 而不是走预期的 error:metering 与 invalid-sidecar 旁证写入流程。结果是该次调用仅以
exit 3 退出且未生成对应的 invalid-*.json 审计记录,违背“总是尝试记录 invoke”的预期行为。
Code

pipeline/metering/metering.py[R151-153]

+            body = json.loads(raw)
+        except json.JSONDecodeError as e:
+            return "", 0, None, f"响应非 JSON:{e}"
Relevance

●●● Strong

Deterministic tuple-contract bug causes immediate unpacking failure and bypasses documented
invalid-sidecar handling.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
证据表明 parse_response() 的正常/其他分支遵循统一契约返回三元组 (content, chunks, usage),但在非流式响应发生 JSONDecodeError
时却返回四元组,从而破坏了调用约定;与此同时,cmd_emit() 在调用 parse_response() 后无条件用三个变量接收返回值,在构建记录与执行
invalid-sidecar/计量失败旁证逻辑之前就会因返回值数量不匹配而触发解包异常。由于异常发生点早于审计记录与失败旁证的生成,任何 HTML/畸形 provider 响应都会绕过既有
fail-closed 处理,只留下 Python traceback。

pipeline/metering/metering.py[149-177]
pipeline/metering/metering.py[260-272]
pipeline/metering/metering.py[309-317]
pipeline/metering/metering-wrapper.sh[132-135]
pipeline/metering/metering.py[142-177]
pipeline/metering/metering.py[292-317]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
非流式响应的 JSON 解析失败路径在 `parse_response()` 中返回了与其他路径不同长度的元组(四元组 vs 三元组),而 `cmd_emit()` 固定按三值解包,导致在进入既有的 `error:metering`、invalid-sidecar 与 HTTP/transport 审计记录逻辑之前就抛出 `ValueError` 并输出 traceback。

## Issue Context
解析失败应以与正常路径一致的返回形状(或一致的结果类型/契约)交给 `cmd_emit()`,让 `cmd_emit` 继续执行现有的 fail-closed 逻辑来写入预期的计量失败旁证与 `invalid-*.json`(或对应的 HTTP/transport 错误记录),从而避免异常响应绕过“总是尝试记录 invoke”的行为。

## Fix Focus Areas
- pipeline/metering/metering.py[142-177]
- pipeline/metering/metering.py[260-315]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: 该 PR 引入跨 wrapper、计量核心、schema、静态扫描、账本同步及多个 CI workflow 的大量新逻辑,包含权限/审计/成本归账等高影响路径,存在多个独立且易漏检的缺陷面。

Grey Divider

Tip of the day
💡 Did you know, you can tweak Display preferences with a live preview to see your comment before it ships

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +73 to +77
REMOTE_N=$(wc -l <"$BASE" | tr -d ' ')
LOCAL_M=$(wc -l <"$LOCAL" | tr -d ' ')
if [[ "$REMOTE_N" -ge "$LOCAL_M" ]]; then
echo "OK $NAME:远端 $REMOTE_N 条 ≥ 本地 $LOCAL_M 条——本片无需同步(幂等跳过)"
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Remote records silently skipped 🐞 Bug ≡ Correctness

ledger-sync.sh incorrectly treats a remote shard with at least as many lines as the current local
shard as already synchronized, without verifying invoke_id or record/content identity, so new runs
are silently skipped. Because each llm-connectivity workflow run typically produces a fresh
one-record local .metering shard while the remote shard is cumulative, every run after the first
fails to upload and downstream cost-check ledger data is continuously missing.
Agent Prompt
## Issue description
Remote line count is not evidence that the current run’s local invoke records are already present on the remote ledger; using `REMOTE_N >= LOCAL_M` as an idempotency check causes new, different `invoke_id` records (often the only record in an ephemeral local `.metering`) to be silently skipped and never uploaded.

## Issue Context
Each workflow run generally produces a small independent local shard (commonly a single new record) while the remote shard is cumulative across runs, so length comparisons between two independent ledgers do not prove content equality. The script currently skips before `relink`, even though `relink` is what can connect the local shard onto the remote chain tail; the sync decision should instead verify whether the remote actually already contains the local record(s) (e.g., by `invoke_id` or record/hash identity and chain contents). Preserve existing concurrency PUT protections (SHA/conflict protection).

## Fix Focus Areas
- pipeline/metering/ledger-sync.sh[70-81]
- pipeline/metering/metering.py[397-428]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +281 to +285
prev, index = None, 0
if os.path.isfile(shard_path):
records, _ = read_records(shard_path)
if records:
prev, index = records[-1]["record_sha256"], len(records)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Concurrent appends break chain 🐞 Bug ☼ Reliability

emit reads the shard tail, derives the next record_index and predecessor hash, and appends the
JSONL record without mutual exclusion, so concurrent wrapper invocations can both use the same
observed tail and index/prev values. Both calls can return success while leaving a forked/invalid
JSONL hash-chain sequence that later fails verification and downstream accounting.
Agent Prompt
## Issue description
Concurrent `emit` calls can read the same shard tail, compute identical chain fields (`record_index` and predecessor hash), and append two “successful” records that permanently fork/break the per-week JSONL hash chain and later fail verification/accounting.

## Issue Context
Tail reading, cross-file `invoke_id` de-duplication, chain field computation (`index`/`prev`/`hash`), and the actual append-to-disk must be treated as a single mutually exclusive transaction per ledger directory/shard, and the locking must work across separate wrapper processes. The lock must cover more than just the final `write()` call; otherwise two processes can still derive chain fields from the same old tail and produce conflicting records.

## Fix Focus Areas
- pipeline/metering/metering.py[281-317]
- pipeline/metering/metering.py[212-238]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +282 to +285
if os.path.isfile(shard_path):
records, _ = read_records(shard_path)
if records:
prev, index = records[-1]["record_sha256"], len(records)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Invalid ledger accepts appends 🐞 Bug ☼ Reliability

emit ignores parse errors and never verifies the existing shard before deriving its tail, so a
malformed, tampered, or already broken ledger still accepts another record and the wrapper reports
success. This violates the wrapper's fail-closed contract and extends an untrustworthy audit chain
until a separate optional verification is run.
Agent Prompt
## Issue description
Emit appends to an existing ledger without validating its JSON, schema, hashes, indexes, or invoke uniqueness first.

## Issue Context
Validation must occur inside the same locked append transaction so the checked state cannot change before writing.

## Fix Focus Areas
- pipeline/metering/metering.py[281-291]
- pipeline/metering/metering.py[212-238]
- pipeline/metering/metering.py[316-317]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +76 to +78
for lineno, line in enumerate(text.splitlines(), 1):
for pid, desc, rx in compiled:
if rx.search(line):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Multiline calls bypass scanner 🐞 Bug ≡ Correctness

The scanner applies every regex to one physical line, while the curl and HTTP-library patterns
require the client name and provider hostname on that same line. A direct call split with shell
continuation or a multiline Python call passes INV-06 scanning despite directly invoking a provider.
Agent Prompt
## Issue description
Line-by-line matching misses direct provider calls whose command and endpoint appear on different lines.

## Issue Context
At minimum, normalize shell continuations and scan multiline text; preferably parse supported languages or add robust multiline patterns and negative fixtures.

## Fix Focus Areas
- pipeline/metering/scan-direct-sdk.sh[72-85]
- pipeline/metering/scan-patterns.yaml[28-33]
- pipeline/metering/selftest/run-selftest.sh[142-151]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +58 to +60
base = root if os.path.isdir(root) else (os.path.dirname(root) or ".")
if os.path.isfile(root):
walk = [(os.path.dirname(root) or ".", [], [os.path.basename(root)])]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Explicit scans ignore exemptions 🐞 Bug ≡ Correctness

For explicitly supplied paths, exemption matching uses paths relative to the scanned directory or
file parent rather than the repository root. Scanning pipeline/metering/ directly therefore sees
metering-wrapper.sh, not pipeline/metering/metering-wrapper.sh, and falsely flags the wrapper's
authorized provider curl.
Agent Prompt
## Issue description
Explicit-path scans compute relative paths from inconsistent bases, preventing configured repository-relative exemptions from matching.

## Issue Context
Use the configured repository root as the stable base for all files, including explicitly supplied files/directories, and add tests for both forms.

## Fix Focus Areas
- pipeline/metering/scan-direct-sdk.sh[57-70]
- pipeline/metering/scan-patterns.yaml[14-16]
- pipeline/metering/selftest/run-selftest.sh[142-151]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +49 to +50
--role) ROLE="${2:?}"; shift 2 ;;
--tag) TAG="${2:?}"; shift 2 ;;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

6. Tag argument is discarded 🐞 Bug ≡ Correctness

The wrapper parses and documents --tag but never passes it to emit; compatibility records
hard-code tag to the accounting role instead. The changed connectivity workflow explicitly
supplies --role probe --tag connectivity, yet its llm-usage/v1 output now says probe, breaking
the prior wrapper's stage-tag semantics.
Agent Prompt
## Issue description
The accepted `--tag` value is discarded, so compatibility output changes caller-visible metadata.

## Issue Context
Keep role as the ledger accounting key, but pass tag separately through emit and use it for the `llm-usage/v1` compatibility record and filename.

## Fix Focus Areas
- pipeline/metering/metering-wrapper.sh[37-61]
- pipeline/metering/metering-wrapper.sh[116-130]
- pipeline/metering/metering.py[321-334]
- .github/workflows/llm-connectivity.yml[54-56]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +14 to +16
exempt_paths:
- '^pipeline/metering/'
- '^scripts/llm-call\.sh$'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

7. 扫描豁免覆盖整个目录 🐞 Bug ⛨ Security

pipeline/metering/ 被整体排除而非仅排除 wrapper 实现,因此任何后来加入该目录的 Python、Shell 或 JS 文件都可直连 provider/SDK 而 CI
扫描仍会通过。该目录级豁免直接绕过了仓级 INV-06 门禁。
Agent Prompt
## Issue description
扫描配置将整个 `pipeline/metering/` 子树排除。扫描器在应用任意 SDK/provider 正则前即跳过匹配路径,故该目录中新建的直连调用不会被发现。

## Issue Context
wrapper 自身确实需要调用 provider,但豁免应是最小路径集合。自测 fixture 也应按精确路径处理,避免扩大可绕过范围。

## Fix Focus Areas
- pipeline/metering/scan-patterns.yaml[14-16]
- pipeline/metering/scan-direct-sdk.sh[63-85]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +151 to +153
body = json.loads(raw)
except json.JSONDecodeError as e:
return "", 0, None, f"响应非 JSON:{e}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

8. 异常响应触发未处理解包 🐞 Bug ☼ Reliability

非流式 2xx 响应在返回体不是 JSON(如 HTML/畸形)时,parse_response() 在 JSON 解析失败分支返回四个值,但 cmd_emit()
始终按三个值解包,导致先抛出 ValueError/traceback 而不是走预期的 error:metering 与 invalid-sidecar 旁证写入流程。结果是该次调用仅以
exit 3 退出且未生成对应的 invalid-*.json 审计记录,违背“总是尝试记录 invoke”的预期行为。
Agent Prompt
## Issue description
非流式响应的 JSON 解析失败路径在 `parse_response()` 中返回了与其他路径不同长度的元组(四元组 vs 三元组),而 `cmd_emit()` 固定按三值解包,导致在进入既有的 `error:metering`、invalid-sidecar 与 HTTP/transport 审计记录逻辑之前就抛出 `ValueError` 并输出 traceback。

## Issue Context
解析失败应以与正常路径一致的返回形状(或一致的结果类型/契约)交给 `cmd_emit()`,让 `cmd_emit` 继续执行现有的 fail-closed 逻辑来写入预期的计量失败旁证与 `invalid-*.json`(或对应的 HTTP/transport 错误记录),从而避免异常响应绕过“总是尝试记录 invoke”的行为。

## Fix Focus Areas
- pipeline/metering/metering.py[142-177]
- pipeline/metering/metering.py[260-315]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@randypanding
randypanding merged commit 61191f8 into main Aug 21, 2026
14 checks passed
@randypanding
randypanding deleted the w2c3-metering-wrapper branch August 21, 2026 18:16
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