Skip to content

Commit 66bf9ba

Browse files
OriNachumclaude
andauthored
feat: per-lobe benchmark suite — speed/parallel/prefill/generation + logprobs cat probe (v0.31.0) (#66)
* spec+plan: per-lobe benchmark suite (devague /think + /spec-to-plan) Converged spec + buildable plan for a per-lobe benchmark suite: speed, parallel throughput (auto-ramp to plateau), prefill/TTFT, generation latency for the minor (Qwen3.5-4B) and primary (27B) lobes through the gateway, plus a logprobs-scored "Where is the cat?" temporal-reasoning probe (echo-softmax headline + first-token-mass cross-check, two modes). Plan: 7 file-disjoint TDD-gated tasks across 3 dependency waves; extends lobes benchmark (--all-lobes --concurrency auto) and lobes eval (cat --score logprobs) plus net-new logprobs client plumbing. Risk r1: verify /v1/completions echo is gateway-routed, else fall back to first-token mass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqHUSNHDF5v15ztvz7zbbS * feat(bench): add lobes.bench package skeleton for the benchmark suite Pre-creates the shared package __init__ so parallel Wave 0 tasks (cat_probe, report) add disjoint module files without contending on __init__.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqHUSNHDF5v15ztvz7zbbS * feat(bench): combined per-lobe markdown report renderer with deltas (t4) Implement render_report() to emit a GitHub-flavored markdown table comparing minor vs primary lobes with one metric per row and explicit signed deltas. Includes cat soft-score alongside perf metrics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqHUSNHDF5v15ztvz7zbbS * feat(bench): timestamped cat-probe generator, open+closed modes (t2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(minor): logprobs + completions-echo + gateway-echo probe (t1) Extend chat_completion with logprobs/top_logprobs forwarding; add completions_echo (POST /v1/completions echo=true for full-sequence scoring) and gateway_supports_echo (capability probe that never raises). All three stdlib-only; 9 hermetic tests in tests/test_minor_logprobs.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(assess): per-lobe perf engine — ttft, concurrent driver, auto-ramp knee (t3) Add four new functions to lobes/assess.py (purely additive, no existing code changed): - measure_prefill_ttft: time-to-first-token via max_tokens=1 round-trip - run_concurrent: ThreadPoolExecutor batch driver; returns req/s, p50/p95, ms/token - _find_knee: pure plateau detector on pre-collected rows (no network) - auto_ramp_concurrency: ramps schedule=(1,2,4,8,16,32), early-stops at knee New test file tests/test_assess_perf.py (20 tests, all hermetic): - ThreadingHTTPServer fixture for ttft/concurrent network tests - Pure unit tests for _find_knee (plateau, climbing, single-row, edge cases) - Injected fake _measure for auto_ramp_concurrency (no real sockets) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bench): logprobs cat scorer — echo softmax headline + first-token cross-check + fallback (t5) Adds lobes/bench/cat_score.py with three pure helpers (_softmax, _sequence_logprob, _first_token_mass) and the score_case orchestrator. 19 hermetic tests in tests/test_cat_score.py cover AC1 (headline ≈ 1.0), AC2 (first_token_mass always float in [0,1]), and AC3 (renormalisation + fallback to first-token mass when echo is unavailable). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): lobes eval cat --score logprobs, open|closed modes (t6) Wire generate_case + score_case (logprobs) into a read-only `lobes eval cat` sub-verb beside `eval minor`. Accepts a JSONL suite of {seed, mode?, n_characters?} lines; emits per-case soft_score, headline, first_token_mass, and the correct answer; aggregates mean_soft_score. --mode {open,closed} selects the probe mode globally; suite lines may override per-case. Covers spec target c16. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): lobes benchmark --all-lobes --concurrency auto, combined per-lobe report (t7) Add --all-lobes branch to `lobes benchmark` that benchmarks both the primary and minor lobes through the gateway in a single invocation, rendering perf metrics (decode tok/s, prefill TTFT, peak req/s, p50/p95 latency, ms/token) + cat soft-score per lobe via render_report with signed per-metric deltas (AC1-AC4). New flags: --all-lobes (store_true), --concurrency (auto or int), --minor-model. New helpers: _bench_one_lobe (per-lobe perf+score), _bench_all_lobes (orchestrator). Existing single-model cmd_benchmark path is preserved unchanged. All 18 new tests pass hermetically (monkeypatched network); 64 CLI regression tests unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(bench): silence two bandit false positives (B311 seeded RNG, B105 label) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqHUSNHDF5v15ztvz7zbbS * chore: bump version 0.30.0 -> 0.31.0 (per-lobe benchmark suite) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqHUSNHDF5v15ztvz7zbbS * fix(bench): avoid float == in cat scorer denom guard (SonarCloud S1244) Replace `denom == 0.0` with `denom <= 0.0` — semantically identical (denom is a sum of non-negative probabilities so it can never be negative), but avoids the exact float-equality that SonarCloud S1244 flags as a reliability issue. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(sonar): mark seeded-RNG hotspots (python:S2245) reviewed-safe in cat_probe The 'Where is the cat?' benchmark generator uses a seeded random.Random for deterministic, reproducible test-data — no crypto, no secrets, no trust boundary. Mirrors the existing e1 reviewed-safe suppression and the bandit # nosec B311 already on these calls. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqHUSNHDF5v15ztvz7zbbS * fix(minor): drop redundant HTTPError from except tuple (SonarCloud S5713) urllib.error.HTTPError is a subclass of OSError, making it redundant in the except tuple of gateway_supports_echo(). Remove it and drop the now- unused `import urllib.error` — behavior is unchanged (HTTPError is still caught via OSError). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(eval): validate cat suite entries + dedupe json-help literal (Qodo + SonarCloud S1192) - _load_cat_suite() now validates seed is int-parseable, mode is open/closed (if present), and n_characters is a positive int (if present); raises ModelGearError(EXIT_USER_ERROR) with line number for each violation instead of letting raw ValueError/TypeError propagate from cmd_eval_cat() - Add module-level _JSON_HELP constant to deduplicate the "Emit structured JSON." literal (S1192 — appeared 3× across eval/minor/cat parsers) - Add 3 new tests: non-integer seed, invalid mode, non-int n_characters Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(report): lift render_report closures to module level (SonarCloud S3776) Move the three nested helper functions (format_metric_name, format_value, format_delta) from inside render_report to module-level private functions (_format_metric_name, _format_value, _format_delta). Reduces cognitive complexity of render_report from 16 to ≤15 (SonarCloud python:S3776 CRITICAL). Logic and rendered output are byte-identical. Add characterization test to lock exact output across future refactors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(benchmark): validate --concurrency + non-zero exit on no lobes (Qodo + SonarCloud S3516) Finding A (Qodo reliability): --concurrency is now validated early in cmd_benchmark; non-positive integers and non-numeric strings raise ModelGearError(EXIT_USER_ERROR) before any network call, instead of crashing with an uncaught ValueError inside ThreadPoolExecutor. The parsed int is forwarded downstream so int() is called exactly once. Finding B (S3516 BLOCKER): --all-lobes with both served names unset now raises ModelGearError(EXIT_ENV_ERROR) instead of silently rendering an empty report and returning 0, giving the dispatcher a genuine error path. New tests: test_concurrency_invalid_string_raises_user_error, test_concurrency_invalid_does_not_call_run_concurrent, test_all_lobes_no_served_names_raises_env_error (3 tests added; 21 total green). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqHUSNHDF5v15ztvz7zbbS * fix(cat_score): guard softmax NaN on all -inf + skip empty tokens (Qodo) Finding A: _softmax() now returns a uniform distribution [1/n]*n when max_lp is not finite (all-inf input) or when total is zero/non-finite, instead of propagating NaN. score_case() detects when every candidate's echo sequence logprob is non-finite and forces the fallback path (echo_available=False, headline="unavailable", soft_score=first_token_mass) so no NaN/Inf value can reach emit_result() or JSON serialisation. Finding B: _first_token_mass() now skips whitespace-only tokens (stripped to "") when building token_probs. Previously, "" matched every candidate via first_word.startswith(""), inflating all candidate masses and distorting the renormalised distribution. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqHUSNHDF5v15ztvz7zbbS * fix(sonar): clear second-round Sonar findings (S1244/S3776/S5799/S3516) - cat_score: denom guard uses <= not == (S1244 reliability bug; was the gate-breaker) - benchmark: extract _parse_concurrency + _run_all_lobes to cut cmd_benchmark cognitive complexity (S3776) and avoid the invariant-return shape on the handler; merge same-line implicit string concat (S5799) - eval: extract _validate_cat_entry to cut _load_cat_suite complexity (S3776); merge same-line implicit string concat (S5799) - sonar-project.properties: e3 reviewed-safe suppression of S3516 on benchmark.py (CLI handlers uniformly return 0-on-success / raise ModelGearError on failure) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqHUSNHDF5v15ztvz7zbbS --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 37740ec commit 66bf9ba

26 files changed

Lines changed: 4759 additions & 13 deletions

.devague/current

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
lobes-ships-a-minor-lobe-a-cheap-warm-co-resident
1+
lobes-ships-a-per-lobe-benchmark-suite-speed-paral

.devague/current_plan

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
lobes-ships-a-minor-lobe-a-cheap-warm-co-resident
1+
lobes-ships-a-per-lobe-benchmark-suite-speed-paral
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
{
2+
"slug": "lobes-ships-a-per-lobe-benchmark-suite-speed-paral",
3+
"title": "lobes ships a per-lobe benchmark suite: speed, parallel throughput, prefill (read) and generation latency for the minor (Qwen3.5-4B) and primary (27B) lobes through the gateway, plus a logprobs-scored 'Where is the cat?' temporal-reasoning probe that grades soft-correctness over candidate locations",
4+
"schema_version": 1,
5+
"status": "exported",
6+
"created": "2026-06-26T14:40:14Z",
7+
"updated": "2026-06-26T14:53:38Z",
8+
"claims": [
9+
{
10+
"id": "c1",
11+
"kind": "announcement",
12+
"text": "lobes ships a per-lobe benchmark suite: speed, parallel throughput, prefill (read) and generation latency for the minor (Qwen3.5-4B) and primary (27B) lobes through the gateway, plus a logprobs-scored 'Where is the cat?' temporal-reasoning probe that grades soft-correctness over candidate locations",
13+
"origin": "user",
14+
"status": "confirmed",
15+
"honesty_conditions": [
16+
{
17+
"id": "h7",
18+
"text": "the suite runs end-to-end against a live lobes deployment and produces both the four perf metrics and the cat-probe soft-score for each lobe in one invocation",
19+
"status": "confirmed"
20+
}
21+
],
22+
"hard_questions": [],
23+
"links": []
24+
},
25+
{
26+
"id": "c2",
27+
"kind": "requirement",
28+
"text": "The suite runs four per-lobe performance measurements: (a) speed = single-stream decode tokens/sec; (b) parallel = throughput and latency under N concurrent requests; (c) read time = prefill / time-to-first-token; (d) generation time = end-to-end and per-token decode latency",
29+
"origin": "user",
30+
"status": "confirmed",
31+
"honesty_conditions": [
32+
{
33+
"id": "h1",
34+
"text": "each of the four metrics has a defined method and unit (decode tok/s; requests/s + p50/p95 latency; ms TTFT for read; ms/token + total s for generation) and is reproducible across runs",
35+
"status": "confirmed"
36+
}
37+
],
38+
"hard_questions": [],
39+
"links": []
40+
},
41+
{
42+
"id": "c3",
43+
"kind": "requirement",
44+
"text": "Every measurement runs against each lobe \u2014 minor (Qwen3.5-4B) and primary (27B) \u2014 routed through the gateway, and results are reported per-lobe side by side",
45+
"origin": "user",
46+
"status": "confirmed",
47+
"honesty_conditions": [
48+
{
49+
"id": "h2",
50+
"text": "the suite can address each lobe distinctly through the gateway (minor vs primary) and every reported number is labelled with the lobe it came from",
51+
"status": "confirmed"
52+
}
53+
],
54+
"hard_questions": [],
55+
"links": []
56+
},
57+
{
58+
"id": "c4",
59+
"kind": "requirement",
60+
"text": "The suite includes a 'Where is the cat?' probe: a generated narrative where several characters each saw/played-with the cat at different timestamps; an investigating-kid framing collects the clues; the prompt ends with 'And the cat is at:' and the model must infer the cat's CURRENT location from the temporal clues",
61+
"origin": "user",
62+
"status": "confirmed",
63+
"honesty_conditions": [
64+
{
65+
"id": "h3",
66+
"text": "the generated narrative has a single unambiguous correct CURRENT location given the timestamps, and the intended answer is machine-checkable",
67+
"status": "confirmed"
68+
}
69+
],
70+
"hard_questions": [],
71+
"links": []
72+
},
73+
{
74+
"id": "c5",
75+
"kind": "requirement",
76+
"text": "The cat probe is graded with logprobs: the score is the model's probability mass over the candidate-location tokens (soft-correctness), not just the greedy/argmax token",
77+
"origin": "user",
78+
"status": "confirmed",
79+
"honesty_conditions": [
80+
{
81+
"id": "h4",
82+
"text": "the soft-score is computed from returned logprobs over the candidate set and equals 1.0 only when all probability mass sits on the correct location",
83+
"status": "confirmed"
84+
}
85+
],
86+
"hard_questions": [],
87+
"links": []
88+
},
89+
{
90+
"id": "c6",
91+
"kind": "success_signal",
92+
"text": "The report shows the minor lobe is materially faster/cheaper on the four perf metrics while the cat probe's logprobs soft-score quantifies how much temporal-reasoning quality it trades vs the primary",
93+
"origin": "user",
94+
"status": "confirmed",
95+
"honesty_conditions": [
96+
{
97+
"id": "h8",
98+
"text": "the report makes the speed-vs-reasoning tradeoff legible at a glance \u2014 the perf deltas and the soft-score delta are both shown per lobe",
99+
"status": "confirmed"
100+
}
101+
],
102+
"hard_questions": [],
103+
"links": []
104+
},
105+
{
106+
"id": "c7",
107+
"kind": "why_it_matters",
108+
"text": "Deciding whether the minor lobe earns its co-residency needs evidence on both axes \u2014 speed AND reasoning \u2014 and a logprobs soft-score discriminates the two lobes more finely than a pass/fail probe",
109+
"origin": "user",
110+
"status": "confirmed",
111+
"honesty_conditions": [
112+
{
113+
"id": "h9",
114+
"text": "the soft-score separates the two lobes by more than run-to-run noise on the cat probe (otherwise it is not discriminating)",
115+
"status": "confirmed"
116+
}
117+
],
118+
"hard_questions": [],
119+
"links": []
120+
},
121+
{
122+
"id": "c8",
123+
"kind": "audience",
124+
"text": "the lobes maintainer / mesh operator evaluating whether to keep the minor lobe warm and co-resident",
125+
"origin": "llm",
126+
"status": "confirmed",
127+
"honesty_conditions": [
128+
{
129+
"id": "h12",
130+
"text": "the report answers 'is the minor lobe fast enough and good-enough to keep co-resident?' for that operator without extra tooling",
131+
"status": "confirmed"
132+
}
133+
],
134+
"hard_questions": [],
135+
"links": []
136+
},
137+
{
138+
"id": "c9",
139+
"kind": "boundary",
140+
"text": "not a training/fine-tuning eval and not a general LLM leaderboard; it benchmarks only the lobes that lobes serves, through the gateway, read-only \u2014 and the cat probe is one hand-built temporal-reasoning probe, not a broad dataset",
141+
"origin": "llm",
142+
"status": "confirmed",
143+
"honesty_conditions": [
144+
{
145+
"id": "h13",
146+
"text": "the suite only reads from a running lobes deployment (no training, no external datasets, no writes) and the cat probe is fully self-contained",
147+
"status": "confirmed"
148+
}
149+
],
150+
"hard_questions": [],
151+
"links": []
152+
},
153+
{
154+
"id": "c10",
155+
"kind": "non_goal",
156+
"text": "does not replace 'lobes assess' correctness probes or the existing 'lobes eval' substring/regex suites; it adds perf+logprobs scoring rather than changing them",
157+
"origin": "llm",
158+
"status": "confirmed",
159+
"honesty_conditions": [],
160+
"hard_questions": [],
161+
"links": []
162+
},
163+
{
164+
"id": "c11",
165+
"kind": "decision",
166+
"text": "exposed as an extension of the existing read-only benchmark/eval surface (per-lobe + concurrency + a logprobs scorer), reusing lobes.assess.run_benchmark for prefill/decode and lobes.minor._client for requests",
167+
"origin": "llm",
168+
"status": "confirmed",
169+
"honesty_conditions": [],
170+
"hard_questions": [],
171+
"links": []
172+
},
173+
{
174+
"id": "c12",
175+
"kind": "requirement",
176+
"text": "the request client gains logprobs support (logprobs + top_logprobs) so the cat probe can read per-token candidate probabilities from the OpenAI-compatible endpoint",
177+
"origin": "llm",
178+
"status": "confirmed",
179+
"honesty_conditions": [
180+
{
181+
"id": "h5",
182+
"text": "a request returns per-token logprobs/top_logprobs that include the candidate tokens, or the scorer degrades gracefully (records 'unavailable') when they are absent",
183+
"status": "confirmed"
184+
}
185+
],
186+
"hard_questions": [],
187+
"links": []
188+
},
189+
{
190+
"id": "c13",
191+
"kind": "requirement",
192+
"text": "the cat probe constrains the answer to a fixed small candidate-location set and the soft-score is the renormalised probability mass over those candidates, so scores are comparable across lobes",
193+
"origin": "llm",
194+
"status": "rejected",
195+
"honesty_conditions": [
196+
{
197+
"id": "h6",
198+
"text": "the candidate-location set is fixed and known to the scorer, and the score renormalises over only those candidates so it lands in [0,1] and is comparable across lobes",
199+
"status": "proposed"
200+
}
201+
],
202+
"hard_questions": [],
203+
"links": []
204+
},
205+
{
206+
"id": "c14",
207+
"kind": "assumption",
208+
"text": "the vLLM OpenAI-compatible endpoints the lobes serve expose logprobs/top_logprobs (chat completions and/or /v1/completions with echo)",
209+
"origin": "llm",
210+
"status": "confirmed",
211+
"honesty_conditions": [],
212+
"hard_questions": [],
213+
"links": []
214+
},
215+
{
216+
"id": "c15",
217+
"kind": "after_state",
218+
"text": "one read-only command runs the same workload against each lobe through the gateway and emits a per-lobe comparison: decode tok/s, parallel throughput + p50/p95 latency, prefill/TTFT, generation time, and the cat probe's logprobs soft-score",
219+
"origin": "user",
220+
"status": "confirmed",
221+
"honesty_conditions": [
222+
{
223+
"id": "h14",
224+
"text": "a single invocation per the chosen surface produces both the per-lobe perf numbers and the cat soft-score, rendered together for comparison",
225+
"status": "confirmed"
226+
}
227+
],
228+
"hard_questions": [],
229+
"links": []
230+
},
231+
{
232+
"id": "c16",
233+
"kind": "requirement",
234+
"text": "The cat probe ships in two modes: (a) open-ended \u2014 the model is shown no options and the scorer discovers the candidate locations the narrative introduced; (b) closed forced-choice \u2014 the prompt lists the candidate locations. In both, the soft-score is the renormalised probability mass over the candidate-location set, so it lands in [0,1] and is comparable across lobes",
235+
"origin": "user",
236+
"status": "confirmed",
237+
"honesty_conditions": [
238+
{
239+
"id": "h10",
240+
"text": "in open-ended mode the scorer reliably recovers the set of locations the story introduced; in closed mode that set is given; in both, the renormalisation denominator is exactly that set",
241+
"status": "confirmed"
242+
}
243+
],
244+
"hard_questions": [],
245+
"links": []
246+
},
247+
{
248+
"id": "c17",
249+
"kind": "requirement",
250+
"text": "The headline soft-score is the softmax over candidates' full-sequence logprobs from /v1/completions with echo=true + logprobs (each candidate string appended after 'And the cat is at:'); the first-token probability mass from a chat logprobs+top_logprobs call is reported alongside as a cheaper cross-check",
251+
"origin": "user",
252+
"status": "confirmed",
253+
"honesty_conditions": [
254+
{
255+
"id": "h11",
256+
"text": "the /v1/completions echo path returns per-token logprobs for the appended candidate tokens through the gateway; if echo or completions is unavailable, the suite records the sequence score as 'unavailable' and falls back to the first-token-mass score rather than failing",
257+
"status": "confirmed"
258+
}
259+
],
260+
"hard_questions": [],
261+
"links": []
262+
},
263+
{
264+
"id": "c18",
265+
"kind": "decision",
266+
"text": "Surface: perf metrics via 'lobes benchmark --all-lobes --concurrency auto' (extends the read-only benchmark verb with per-lobe routing + a concurrency sweep); the cat probe via 'lobes eval cat --suite <jsonl> --score logprobs' (extends the read-only eval verb with a logprobs scorer). No new top-level verb",
267+
"origin": "user",
268+
"status": "confirmed",
269+
"honesty_conditions": [],
270+
"hard_questions": [],
271+
"links": []
272+
},
273+
{
274+
"id": "c19",
275+
"kind": "decision",
276+
"text": "The parallel-calls measurement auto-ramps concurrency (1->2->4->8->16...) until the requests/sec gain between steps falls below a threshold, then reports the knee (peak-throughput concurrency) plus per-step throughput and p50/p95 latency",
277+
"origin": "user",
278+
"status": "confirmed",
279+
"honesty_conditions": [],
280+
"hard_questions": [],
281+
"links": []
282+
}
283+
],
284+
"open_vagueness": [
285+
{
286+
"id": "v1",
287+
"text": "RESOLVED (user): scoring = softmax over candidates' full-sequence logprobs via /v1/completions echo (headline) + chat first-token mass (cross-check); see c17. Open follow-up: verify echo is gateway-routed, else fall back to first-token mass.",
288+
"kind": "follow_up",
289+
"claim_id": null
290+
},
291+
{
292+
"id": "v2",
293+
"text": "RESOLVED (user): parallel measurement auto-ramps concurrency to the throughput knee; see c19. Open follow-up: tune the plateau threshold.",
294+
"kind": "follow_up",
295+
"claim_id": null
296+
}
297+
]
298+
}

0 commit comments

Comments
 (0)