casebench 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- casebench/__init__.py +5 -0
- casebench/capture/__init__.py +1 -0
- casebench/capture/assemble.py +92 -0
- casebench/capture/backend.py +47 -0
- casebench/capture/cli.py +36 -0
- casebench/capture/config.py +28 -0
- casebench/capture/conformance.py +33 -0
- casebench/capture/context.py +53 -0
- casebench/capture/corpus_routes.py +50 -0
- casebench/capture/functions.py +115 -0
- casebench/capture/interface.py +23 -0
- casebench/capture/otlp/__init__.py +1 -0
- casebench/capture/otlp/decode.py +82 -0
- casebench/capture/otlp/mapping.py +105 -0
- casebench/capture/parsers/__init__.py +1 -0
- casebench/capture/parsers/anthropic/__init__.py +86 -0
- casebench/capture/parsers/anthropic/casebench-plugin.json +5 -0
- casebench/capture/parsers/anthropic/samples/plain-with-tool-result.json +76 -0
- casebench/capture/parsers/anthropic/samples/streamed-tool-call.json +97 -0
- casebench/capture/parsers/gemini/__init__.py +77 -0
- casebench/capture/parsers/gemini/casebench-plugin.json +5 -0
- casebench/capture/parsers/gemini/samples/plain-with-function-response.json +86 -0
- casebench/capture/parsers/gemini/samples/streamed-function-call.json +86 -0
- casebench/capture/parsers/openai/__init__.py +106 -0
- casebench/capture/parsers/openai/casebench-plugin.json +5 -0
- casebench/capture/parsers/openai/samples/plain-with-tool-result.json +72 -0
- casebench/capture/parsers/openai/samples/streamed-tool-call.json +104 -0
- casebench/capture/proxy/__init__.py +1 -0
- casebench/capture/proxy/forward.py +143 -0
- casebench/capture/proxy/redact.py +18 -0
- casebench/capture/proxy/routes.py +45 -0
- casebench/capture/proxy/server.py +38 -0
- casebench/capture/proxy/sse.py +23 -0
- casebench/capture/read.py +17 -0
- casebench/capture/recorder.py +82 -0
- casebench/capture/redirect.py +83 -0
- casebench/capture/routing.py +16 -0
- casebench/capture/session.py +165 -0
- casebench/capture/sizes.py +11 -0
- casebench/capture/step.py +75 -0
- casebench/cases/__init__.py +1 -0
- casebench/cases/build.py +98 -0
- casebench/cases/cli.py +214 -0
- casebench/cases/draft.py +42 -0
- casebench/cases/resolve.py +39 -0
- casebench/cases/suite.py +31 -0
- casebench/cli.py +60 -0
- casebench/config.py +128 -0
- casebench/core/__init__.py +1 -0
- casebench/core/canonical.py +20 -0
- casebench/core/case.py +140 -0
- casebench/core/jurisdiction.py +57 -0
- casebench/core/model.py +15 -0
- casebench/core/score.py +95 -0
- casebench/corpus/__init__.py +1 -0
- casebench/corpus/add.py +115 -0
- casebench/corpus/add_cli.py +607 -0
- casebench/corpus/binding.py +122 -0
- casebench/corpus/bundled.py +75 -0
- casebench/corpus/cache.py +118 -0
- casebench/corpus/check.py +77 -0
- casebench/corpus/cli.py +105 -0
- casebench/corpus/conformance.py +43 -0
- casebench/corpus/connectors/__init__.py +1 -0
- casebench/corpus/connectors/folder/__init__.py +68 -0
- casebench/corpus/connectors/folder/casebench-plugin.json +5 -0
- casebench/corpus/connectors/folder/samples/README.md +4 -0
- casebench/corpus/connectors/folder/samples/config.json +1 -0
- casebench/corpus/connectors/folder/samples/corpus/sample-1.json +1 -0
- casebench/corpus/connectors/folder/samples/corpus/sample-2.json +1 -0
- casebench/corpus/connectors/http/__init__.py +86 -0
- casebench/corpus/connectors/mcp/__init__.py +170 -0
- casebench/corpus/corpora.py +76 -0
- casebench/corpus/from_profile.py +135 -0
- casebench/corpus/guard.py +39 -0
- casebench/corpus/ids.py +21 -0
- casebench/corpus/interface.py +56 -0
- casebench/corpus/mapping.py +24 -0
- casebench/corpus/passage.py +55 -0
- casebench/corpus/probe_cli.py +77 -0
- casebench/corpus/profile.py +55 -0
- casebench/corpus/profiles/README.md +13 -0
- casebench/corpus/record.py +27 -0
- casebench/corpus/results.py +56 -0
- casebench/corpus/routing.py +69 -0
- casebench/corpus/suggest.py +80 -0
- casebench/envfiles.py +94 -0
- casebench/exporters/__init__.py +1 -0
- casebench/exporters/interface.py +11 -0
- casebench/extract/__init__.py +1 -0
- casebench/extract/agreement.py +52 -0
- casebench/extract/cli.py +150 -0
- casebench/extract/compare.py +58 -0
- casebench/extract/config.py +19 -0
- casebench/extract/estimate.py +93 -0
- casebench/extract/prompt.py +30 -0
- casebench/extract/reading.py +54 -0
- casebench/extract/run.py +162 -0
- casebench/formats/__init__.py +1 -0
- casebench/formats/extensions.py +20 -0
- casebench/formats/files.py +28 -0
- casebench/formats/read.py +39 -0
- casebench/formats/registry.py +47 -0
- casebench/formats/schema.py +29 -0
- casebench/formats/schema_cli.py +38 -0
- casebench/formats/schemas/case/1.0.json +320 -0
- casebench/formats/schemas/document/1.0.json +57 -0
- casebench/formats/schemas/index.json +3 -0
- casebench/formats/schemas/profile/1.0.json +142 -0
- casebench/formats/schemas/report/1.0.json +819 -0
- casebench/formats/schemas/suite/1.0.json +84 -0
- casebench/formats/validate_cli.py +96 -0
- casebench/formats/version.py +62 -0
- casebench/judge/__init__.py +1 -0
- casebench/judge/budget.py +33 -0
- casebench/judge/client.py +85 -0
- casebench/judge/config.py +33 -0
- casebench/judge/judges.py +21 -0
- casebench/judge/memo.py +37 -0
- casebench/judge/prompts.py +81 -0
- casebench/judge/resolve.py +135 -0
- casebench/key_record.py +37 -0
- casebench/keys.py +48 -0
- casebench/loading.py +70 -0
- casebench/metrics/__init__.py +1 -0
- casebench/metrics/authorities.py +102 -0
- casebench/metrics/base.py +73 -0
- casebench/metrics/builtin/__init__.py +1 -0
- casebench/metrics/builtin/citation_integrity/__init__.py +102 -0
- casebench/metrics/builtin/criteria/__init__.py +53 -0
- casebench/metrics/builtin/outcome/__init__.py +45 -0
- casebench/metrics/builtin/precedent/__init__.py +8 -0
- casebench/metrics/builtin/statute/__init__.py +8 -0
- casebench/metrics/criteria.py +94 -0
- casebench/metrics/loading.py +31 -0
- casebench/plugins/__init__.py +1 -0
- casebench/plugins/cli.py +66 -0
- casebench/plugins/conformance.py +90 -0
- casebench/plugins/kinds.py +67 -0
- casebench/plugins/manifest.py +59 -0
- casebench/plugins/registry.py +67 -0
- casebench/plugins/samples.py +43 -0
- casebench/plugins/scaffold.py +111 -0
- casebench/plugins/templates.py +130 -0
- casebench/providers/__init__.py +1 -0
- casebench/providers/builtin/__init__.py +1 -0
- casebench/providers/builtin/anthropic/__init__.py +72 -0
- casebench/providers/builtin/anthropic/casebench-plugin.json +5 -0
- casebench/providers/builtin/anthropic/samples/config.json +1 -0
- casebench/providers/builtin/anthropic/samples/list.json +38 -0
- casebench/providers/builtin/azure/__init__.py +60 -0
- casebench/providers/builtin/bedrock/__init__.py +66 -0
- casebench/providers/builtin/google/__init__.py +65 -0
- casebench/providers/builtin/openai/__init__.py +64 -0
- casebench/providers/builtin/openai/casebench-plugin.json +5 -0
- casebench/providers/builtin/openai/samples/list.json +35 -0
- casebench/providers/call.py +44 -0
- casebench/providers/conformance.py +38 -0
- casebench/providers/interface.py +57 -0
- casebench/providers/send.py +86 -0
- casebench/providers/settings.py +31 -0
- casebench/py.typed +0 -0
- casebench/references/__init__.py +1 -0
- casebench/references/court.py +43 -0
- casebench/references/interface.py +12 -0
- casebench/report/__init__.py +1 -0
- casebench/report/agreement.py +32 -0
- casebench/report/baselines.py +24 -0
- casebench/report/citations.py +24 -0
- casebench/report/cli.py +89 -0
- casebench/report/comparable.py +71 -0
- casebench/report/compare.py +45 -0
- casebench/report/funnel.py +156 -0
- casebench/report/record.py +49 -0
- casebench/report/render.py +142 -0
- casebench/report/statistics.py +60 -0
- casebench/report/tables.py +99 -0
- casebench/runs/__init__.py +1 -0
- casebench/runs/cli.py +175 -0
- casebench/runs/dry_run.py +128 -0
- casebench/runs/log.py +136 -0
- casebench/runs/producer.py +74 -0
- casebench/runs/progress.py +32 -0
- casebench/runs/rescore.py +69 -0
- casebench/runs/runner.py +89 -0
- casebench/runs/scoring.py +152 -0
- casebench/setup/__init__.py +1 -0
- casebench/setup/advice.py +30 -0
- casebench/setup/catalog.py +112 -0
- casebench/setup/check_cli.py +74 -0
- casebench/setup/checks.py +264 -0
- casebench/setup/corpus_question.py +216 -0
- casebench/setup/guide.md +142 -0
- casebench/setup/guide_cli.py +38 -0
- casebench/setup/init_cli.py +437 -0
- casebench/setup/judge_question.py +89 -0
- casebench/setup/key_question.py +136 -0
- casebench/setup/keys_cli.py +58 -0
- casebench/setup/models.py +68 -0
- casebench/setup/models_cli.py +56 -0
- casebench/setup/picker.py +112 -0
- casebench/setup/routes.py +106 -0
- casebench/setup/scan.py +276 -0
- casebench/setup/screen.py +109 -0
- casebench/setup/sdks.py +76 -0
- casebench/setup/summary.py +79 -0
- casebench/setup/update.py +80 -0
- casebench/suites/__init__.py +1 -0
- casebench/suites/cli.py +133 -0
- casebench/suites/court.py +143 -0
- casebench/suites/freeze.py +147 -0
- casebench/suites/generate.py +41 -0
- casebench/suites/qualify.py +36 -0
- casebench/suites/resolve.py +52 -0
- casebench/targets/__init__.py +1 -0
- casebench/targets/answer.py +32 -0
- casebench/targets/builtin/__init__.py +1 -0
- casebench/targets/builtin/command/__init__.py +52 -0
- casebench/targets/builtin/command/casebench-plugin.json +5 -0
- casebench/targets/builtin/command/samples/config.json +1 -0
- casebench/targets/builtin/http/__init__.py +105 -0
- casebench/targets/builtin/http/casebench-plugin.json +5 -0
- casebench/targets/builtin/http/samples/config.json +1 -0
- casebench/targets/builtin/python/__init__.py +144 -0
- casebench/targets/builtin/python/casebench-plugin.json +5 -0
- casebench/targets/builtin/python/entry.py +30 -0
- casebench/targets/builtin/python/framing.py +31 -0
- casebench/targets/builtin/python/samples/config.json +1 -0
- casebench/targets/builtin/python/trace_context.py +21 -0
- casebench/targets/builtin/python/worker.py +57 -0
- casebench/targets/cli.py +53 -0
- casebench/targets/interface.py +22 -0
- casebench/targets/load.py +19 -0
- casebench/targets/placeholders.py +63 -0
- casebench/targets/sample.py +34 -0
- casebench/tasks/__init__.py +1 -0
- casebench/tasks/interface.py +16 -0
- casebench-0.1.0.dist-info/METADATA +82 -0
- casebench-0.1.0.dist-info/RECORD +242 -0
- casebench-0.1.0.dist-info/WHEEL +4 -0
- casebench-0.1.0.dist-info/entry_points.txt +32 -0
- casebench-0.1.0.dist-info/licenses/LICENSE +211 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Turn spans in the GenAI or OpenInference conventions into steps (decision 0023)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from casebench.capture.otlp.decode import RawSpan
|
|
8
|
+
from casebench.capture.step import NO_DIAGNOSTIC, Step, StepKind
|
|
9
|
+
|
|
10
|
+
LLM_OPERATIONS = {"chat", "text_completion", "generate_content"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _indexed_ids(attributes: dict[str, Any], prefix: str) -> list[str]:
|
|
14
|
+
"""OpenInference flattens lists: `<prefix>.0.document.id`, `<prefix>.1.document.id`."""
|
|
15
|
+
pattern = re.compile(rf"^{re.escape(prefix)}\.(\d+)\.document\.id$")
|
|
16
|
+
found = sorted(
|
|
17
|
+
(int(m.group(1)), str(v)) for k, v in attributes.items() if (m := pattern.match(k))
|
|
18
|
+
)
|
|
19
|
+
return [value for _, value in found]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _documents(value: Any) -> list[str]:
|
|
23
|
+
"""`gen_ai.retrieval.documents`: a list of objects with `id`, possibly sent as JSON text."""
|
|
24
|
+
if isinstance(value, str):
|
|
25
|
+
try:
|
|
26
|
+
value = json.loads(value)
|
|
27
|
+
except json.JSONDecodeError:
|
|
28
|
+
return []
|
|
29
|
+
return [str(item["id"]) for item in value or [] if isinstance(item, dict) and "id" in item]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _kind(attributes: dict[str, Any]) -> StepKind | None:
|
|
33
|
+
operation = attributes.get("gen_ai.operation.name")
|
|
34
|
+
span_kind = str(attributes.get("openinference.span.kind", "")).upper()
|
|
35
|
+
if operation in LLM_OPERATIONS or span_kind == "LLM":
|
|
36
|
+
return StepKind.LLM
|
|
37
|
+
if operation == "execute_tool" or span_kind == "TOOL":
|
|
38
|
+
return StepKind.TOOL
|
|
39
|
+
if operation == "retrieval" or span_kind == "RETRIEVER":
|
|
40
|
+
return StepKind.RETRIEVAL
|
|
41
|
+
if (
|
|
42
|
+
operation == "rerank"
|
|
43
|
+
or span_kind == "RERANKER"
|
|
44
|
+
or "casebench.rerank.output_ids" in attributes
|
|
45
|
+
):
|
|
46
|
+
return StepKind.RERANK
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _ids(kind: StepKind, a: dict[str, Any]) -> tuple[list[str], list[str]]:
|
|
51
|
+
if kind is StepKind.RETRIEVAL:
|
|
52
|
+
return [], _documents(a.get("gen_ai.retrieval.documents")) or _indexed_ids(
|
|
53
|
+
a, "retrieval.documents"
|
|
54
|
+
)
|
|
55
|
+
if kind is StepKind.RERANK:
|
|
56
|
+
ids_in = [str(i) for i in a.get("casebench.rerank.input_ids") or []]
|
|
57
|
+
ids_out = [str(i) for i in a.get("casebench.rerank.output_ids") or []]
|
|
58
|
+
return (
|
|
59
|
+
ids_in or _indexed_ids(a, "reranker.input_documents"),
|
|
60
|
+
ids_out or _indexed_ids(a, "reranker.output_documents"),
|
|
61
|
+
)
|
|
62
|
+
return [], []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _int(value: Any) -> int | None:
|
|
66
|
+
return int(value) if value is not None else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def to_step(span: RawSpan) -> Step | None:
|
|
70
|
+
a = span.attributes
|
|
71
|
+
kind = _kind(a)
|
|
72
|
+
if kind is None:
|
|
73
|
+
return None
|
|
74
|
+
ids_in, ids_out = _ids(kind, a)
|
|
75
|
+
searching = kind in (StepKind.RETRIEVAL, StepKind.RERANK)
|
|
76
|
+
return Step(
|
|
77
|
+
kind=kind,
|
|
78
|
+
name=a.get("gen_ai.tool.name") or a.get("tool.name") or span.name,
|
|
79
|
+
provider=a.get("gen_ai.provider.name") or a.get("gen_ai.system") or a.get("llm.provider"),
|
|
80
|
+
model=a.get("gen_ai.response.model")
|
|
81
|
+
or a.get("gen_ai.request.model")
|
|
82
|
+
or a.get("llm.model_name"),
|
|
83
|
+
input_tokens=_int(a.get("gen_ai.usage.input_tokens", a.get("llm.token_count.prompt"))),
|
|
84
|
+
output_tokens=_int(
|
|
85
|
+
a.get("gen_ai.usage.output_tokens", a.get("llm.token_count.completion"))
|
|
86
|
+
),
|
|
87
|
+
doc_ids_in=ids_in,
|
|
88
|
+
doc_ids_out=ids_out,
|
|
89
|
+
started=span.started,
|
|
90
|
+
ended=span.ended,
|
|
91
|
+
duration_ms=(span.ended - span.started) * 1000,
|
|
92
|
+
note=NO_DIAGNOSTIC if searching and not ids_out else None,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def without_duplicates(span_steps: list[Step], proxied: list[Step]) -> list[Step]:
|
|
97
|
+
"""A model call the proxy saw is kept from the proxy; the span describing it is dropped."""
|
|
98
|
+
calls = [s for s in proxied if s.kind is StepKind.LLM and s.started is not None]
|
|
99
|
+
|
|
100
|
+
def seen_by_proxy(step: Step) -> bool:
|
|
101
|
+
if step.kind is not StepKind.LLM or step.started is None or step.ended is None:
|
|
102
|
+
return False
|
|
103
|
+
return any(step.started <= c.started <= step.ended for c in calls if c.started is not None)
|
|
104
|
+
|
|
105
|
+
return [step for step in span_steps if not seen_by_proxy(step)]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Built-in capture parsers, one per upstream API."""
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Read Anthropic Messages API calls, streamed or not, into model-call steps."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, ClassVar
|
|
5
|
+
|
|
6
|
+
from casebench.capture.sizes import content_size
|
|
7
|
+
from casebench.capture.step import HttpExchange, Step, StepKind, ToolCall, ToolResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _tool_results(request: Any) -> list[ToolResult]:
|
|
11
|
+
results: list[ToolResult] = []
|
|
12
|
+
for message in (request or {}).get("messages", []):
|
|
13
|
+
if message.get("role") != "user" or not isinstance(message.get("content"), list):
|
|
14
|
+
continue
|
|
15
|
+
for block in message["content"]:
|
|
16
|
+
if isinstance(block, dict) and block.get("type") == "tool_result":
|
|
17
|
+
size = content_size(block.get("content", ""))
|
|
18
|
+
results.append(ToolResult(id=block["tool_use_id"], size=size))
|
|
19
|
+
return results
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _from_message(message: dict[str, Any]) -> tuple[str | None, int | None, int | None, list]:
|
|
23
|
+
usage = message.get("usage") or {}
|
|
24
|
+
tools = [
|
|
25
|
+
ToolCall(id=block["id"], name=block["name"], arguments=block.get("input"))
|
|
26
|
+
for block in message.get("content") or []
|
|
27
|
+
if block.get("type") == "tool_use"
|
|
28
|
+
]
|
|
29
|
+
return message.get("model"), usage.get("input_tokens"), usage.get("output_tokens"), tools
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _from_events(events: list[Any]) -> tuple[str | None, int | None, int | None, list]:
|
|
33
|
+
model = input_tokens = output_tokens = None
|
|
34
|
+
blocks: dict[int, dict[str, Any]] = {}
|
|
35
|
+
for event in events:
|
|
36
|
+
kind = event.get("type")
|
|
37
|
+
if kind == "message_start":
|
|
38
|
+
model = event["message"].get("model")
|
|
39
|
+
input_tokens = (event["message"].get("usage") or {}).get("input_tokens")
|
|
40
|
+
elif kind == "content_block_start" and event["content_block"].get("type") == "tool_use":
|
|
41
|
+
block = event["content_block"]
|
|
42
|
+
blocks[event["index"]] = {"id": block["id"], "name": block["name"], "json": ""}
|
|
43
|
+
elif kind == "content_block_delta" and event["delta"].get("type") == "input_json_delta":
|
|
44
|
+
if event["index"] in blocks:
|
|
45
|
+
blocks[event["index"]]["json"] += event["delta"].get("partial_json", "")
|
|
46
|
+
elif kind == "message_delta":
|
|
47
|
+
output_tokens = (event.get("usage") or {}).get("output_tokens", output_tokens)
|
|
48
|
+
tools = [
|
|
49
|
+
ToolCall(id=b["id"], name=b["name"], arguments=json.loads(b["json"]) if b["json"] else {})
|
|
50
|
+
for _, b in sorted(blocks.items())
|
|
51
|
+
]
|
|
52
|
+
return model, input_tokens, output_tokens, tools
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Parser:
|
|
56
|
+
hosts: ClassVar[list[str]] = ["api.anthropic.com"]
|
|
57
|
+
packages: ClassVar[list[str]] = ["anthropic", "@anthropic-ai/sdk"]
|
|
58
|
+
# The client classes whose calls this parser reads, for `casebench init` (decision 0040).
|
|
59
|
+
clients: ClassVar[list[str]] = ["anthropic.Anthropic", "anthropic.AsyncAnthropic"]
|
|
60
|
+
default_route: ClassVar[dict[str, str]] = {
|
|
61
|
+
"name": "anthropic",
|
|
62
|
+
"upstream": "https://api.anthropic.com",
|
|
63
|
+
"env": "ANTHROPIC_BASE_URL",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def parse(self, exchange: HttpExchange) -> list[Step]:
|
|
67
|
+
if not exchange.url.rstrip("/").endswith("/v1/messages"):
|
|
68
|
+
return []
|
|
69
|
+
if exchange.response_events is not None:
|
|
70
|
+
model, tokens_in, tokens_out, tools = _from_events(exchange.response_events)
|
|
71
|
+
else:
|
|
72
|
+
model, tokens_in, tokens_out, tools = _from_message(exchange.response_body or {})
|
|
73
|
+
return [
|
|
74
|
+
Step(
|
|
75
|
+
kind=StepKind.LLM,
|
|
76
|
+
provider="anthropic",
|
|
77
|
+
model=model or (exchange.request_body or {}).get("model"),
|
|
78
|
+
input_tokens=tokens_in,
|
|
79
|
+
output_tokens=tokens_out,
|
|
80
|
+
requested_tools=tools,
|
|
81
|
+
tool_results=_tool_results(exchange.request_body),
|
|
82
|
+
started=exchange.started,
|
|
83
|
+
ended=exchange.ended,
|
|
84
|
+
duration_ms=exchange.duration_ms,
|
|
85
|
+
)
|
|
86
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"exchange": {
|
|
3
|
+
"method": "POST",
|
|
4
|
+
"url": "https://api.anthropic.com/v1/messages",
|
|
5
|
+
"status": 200,
|
|
6
|
+
"request_body": {
|
|
7
|
+
"model": "claude-sonnet-5-5",
|
|
8
|
+
"max_tokens": 1024,
|
|
9
|
+
"messages": [
|
|
10
|
+
{
|
|
11
|
+
"role": "user",
|
|
12
|
+
"content": "Is the dismissal valid?"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"role": "assistant",
|
|
16
|
+
"content": [
|
|
17
|
+
{
|
|
18
|
+
"type": "tool_use",
|
|
19
|
+
"id": "toolu_1",
|
|
20
|
+
"name": "search",
|
|
21
|
+
"input": {
|
|
22
|
+
"query": "dismissal"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"role": "user",
|
|
29
|
+
"content": [
|
|
30
|
+
{
|
|
31
|
+
"type": "tool_result",
|
|
32
|
+
"tool_use_id": "toolu_1",
|
|
33
|
+
"content": "3 decisions found"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"response_body": {
|
|
40
|
+
"type": "message",
|
|
41
|
+
"model": "claude-sonnet-5-5",
|
|
42
|
+
"content": [
|
|
43
|
+
{
|
|
44
|
+
"type": "text",
|
|
45
|
+
"text": "No."
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"usage": {
|
|
49
|
+
"input_tokens": 200,
|
|
50
|
+
"output_tokens": 40
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"route": "anthropic",
|
|
54
|
+
"started": 1760000002.5,
|
|
55
|
+
"ended": 1760000003.5,
|
|
56
|
+
"duration_ms": 1000.0
|
|
57
|
+
},
|
|
58
|
+
"expected": [
|
|
59
|
+
{
|
|
60
|
+
"kind": "llm",
|
|
61
|
+
"provider": "anthropic",
|
|
62
|
+
"model": "claude-sonnet-5-5",
|
|
63
|
+
"input_tokens": 200,
|
|
64
|
+
"output_tokens": 40,
|
|
65
|
+
"tool_results": [
|
|
66
|
+
{
|
|
67
|
+
"id": "toolu_1",
|
|
68
|
+
"size": 17
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"started": 1760000002.5,
|
|
72
|
+
"ended": 1760000003.5,
|
|
73
|
+
"duration_ms": 1000.0
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"exchange": {
|
|
3
|
+
"method": "POST",
|
|
4
|
+
"url": "https://api.anthropic.com/v1/messages",
|
|
5
|
+
"status": 200,
|
|
6
|
+
"request_body": {
|
|
7
|
+
"model": "claude-sonnet-5-5",
|
|
8
|
+
"max_tokens": 1024,
|
|
9
|
+
"stream": true,
|
|
10
|
+
"messages": [
|
|
11
|
+
{
|
|
12
|
+
"role": "user",
|
|
13
|
+
"content": "Is the dismissal valid?"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"response_events": [
|
|
18
|
+
{
|
|
19
|
+
"type": "message_start",
|
|
20
|
+
"message": {
|
|
21
|
+
"model": "claude-sonnet-5-5",
|
|
22
|
+
"usage": {
|
|
23
|
+
"input_tokens": 120,
|
|
24
|
+
"output_tokens": 1
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"type": "content_block_start",
|
|
30
|
+
"index": 0,
|
|
31
|
+
"content_block": {
|
|
32
|
+
"type": "tool_use",
|
|
33
|
+
"id": "toolu_1",
|
|
34
|
+
"name": "search",
|
|
35
|
+
"input": {}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"type": "content_block_delta",
|
|
40
|
+
"index": 0,
|
|
41
|
+
"delta": {
|
|
42
|
+
"type": "input_json_delta",
|
|
43
|
+
"partial_json": "{\"query\": "
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"type": "content_block_delta",
|
|
48
|
+
"index": 0,
|
|
49
|
+
"delta": {
|
|
50
|
+
"type": "input_json_delta",
|
|
51
|
+
"partial_json": "\"dismissal\"}"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"type": "content_block_stop",
|
|
56
|
+
"index": 0
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"type": "message_delta",
|
|
60
|
+
"delta": {
|
|
61
|
+
"stop_reason": "tool_use"
|
|
62
|
+
},
|
|
63
|
+
"usage": {
|
|
64
|
+
"output_tokens": 30
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"type": "message_stop"
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"route": "anthropic",
|
|
72
|
+
"started": 1760000000.0,
|
|
73
|
+
"ended": 1760000001.5,
|
|
74
|
+
"duration_ms": 1500.0
|
|
75
|
+
},
|
|
76
|
+
"expected": [
|
|
77
|
+
{
|
|
78
|
+
"kind": "llm",
|
|
79
|
+
"provider": "anthropic",
|
|
80
|
+
"model": "claude-sonnet-5-5",
|
|
81
|
+
"input_tokens": 120,
|
|
82
|
+
"output_tokens": 30,
|
|
83
|
+
"requested_tools": [
|
|
84
|
+
{
|
|
85
|
+
"id": "toolu_1",
|
|
86
|
+
"name": "search",
|
|
87
|
+
"arguments": {
|
|
88
|
+
"query": "dismissal"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"started": 1760000000.0,
|
|
93
|
+
"ended": 1760000001.5,
|
|
94
|
+
"duration_ms": 1500.0
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Read Gemini API generateContent calls, streamed or not, into model-call steps.
|
|
2
|
+
|
|
3
|
+
Gemini may omit function-call ids; a call without one is paired with its result by the
|
|
4
|
+
function's name.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, ClassVar
|
|
9
|
+
|
|
10
|
+
from casebench.capture.sizes import content_size
|
|
11
|
+
from casebench.capture.step import HttpExchange, Step, StepKind, ToolCall, ToolResult
|
|
12
|
+
|
|
13
|
+
METHOD = re.compile(r"/models/([^/:]+):(generateContent|streamGenerateContent)")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _tool_results(request: Any) -> list[ToolResult]:
|
|
17
|
+
results: list[ToolResult] = []
|
|
18
|
+
for content in (request or {}).get("contents", []):
|
|
19
|
+
for part in content.get("parts") or []:
|
|
20
|
+
answer = part.get("functionResponse")
|
|
21
|
+
if answer:
|
|
22
|
+
key = answer.get("id") or answer["name"]
|
|
23
|
+
results.append(ToolResult(id=key, size=content_size(answer.get("response", {}))))
|
|
24
|
+
return results
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _read(chunks: list[Any]) -> tuple[str | None, int | None, int | None, list[ToolCall]]:
|
|
28
|
+
model = prompt = completion = None
|
|
29
|
+
tools: list[ToolCall] = []
|
|
30
|
+
for chunk in chunks:
|
|
31
|
+
model = chunk.get("modelVersion") or model
|
|
32
|
+
usage = chunk.get("usageMetadata") or {}
|
|
33
|
+
prompt = usage.get("promptTokenCount", prompt)
|
|
34
|
+
completion = usage.get("candidatesTokenCount", completion)
|
|
35
|
+
for candidate in chunk.get("candidates") or []:
|
|
36
|
+
for part in (candidate.get("content") or {}).get("parts") or []:
|
|
37
|
+
call = part.get("functionCall")
|
|
38
|
+
if call:
|
|
39
|
+
key = call.get("id") or call["name"]
|
|
40
|
+
tools.append(ToolCall(id=key, name=call["name"], arguments=call.get("args")))
|
|
41
|
+
return model, prompt, completion, tools
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Parser:
|
|
45
|
+
hosts: ClassVar[list[str]] = ["generativelanguage.googleapis.com"]
|
|
46
|
+
packages: ClassVar[list[str]] = ["google-genai", "google-generativeai", "@google/genai"]
|
|
47
|
+
# The client classes whose calls this parser reads, for `casebench init` (decision 0040).
|
|
48
|
+
clients: ClassVar[list[str]] = ["google.genai.Client"]
|
|
49
|
+
default_route: ClassVar[dict[str, str]] = {
|
|
50
|
+
"name": "gemini",
|
|
51
|
+
"upstream": "https://generativelanguage.googleapis.com",
|
|
52
|
+
"env": "GOOGLE_GEMINI_BASE_URL",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def parse(self, exchange: HttpExchange) -> list[Step]:
|
|
56
|
+
method = METHOD.search(exchange.url)
|
|
57
|
+
if method is None:
|
|
58
|
+
return []
|
|
59
|
+
if exchange.response_events is not None:
|
|
60
|
+
chunks = exchange.response_events
|
|
61
|
+
else:
|
|
62
|
+
chunks = [exchange.response_body or {}]
|
|
63
|
+
model, prompt, completion, tools = _read(chunks)
|
|
64
|
+
return [
|
|
65
|
+
Step(
|
|
66
|
+
kind=StepKind.LLM,
|
|
67
|
+
provider="gemini",
|
|
68
|
+
model=model or method.group(1),
|
|
69
|
+
input_tokens=prompt,
|
|
70
|
+
output_tokens=completion,
|
|
71
|
+
requested_tools=tools,
|
|
72
|
+
tool_results=_tool_results(exchange.request_body),
|
|
73
|
+
started=exchange.started,
|
|
74
|
+
ended=exchange.ended,
|
|
75
|
+
duration_ms=exchange.duration_ms,
|
|
76
|
+
)
|
|
77
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"exchange": {
|
|
3
|
+
"method": "POST",
|
|
4
|
+
"url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro:generateContent",
|
|
5
|
+
"status": 200,
|
|
6
|
+
"request_body": {
|
|
7
|
+
"contents": [
|
|
8
|
+
{
|
|
9
|
+
"role": "user",
|
|
10
|
+
"parts": [
|
|
11
|
+
{
|
|
12
|
+
"text": "Is the dismissal valid?"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"role": "model",
|
|
18
|
+
"parts": [
|
|
19
|
+
{
|
|
20
|
+
"functionCall": {
|
|
21
|
+
"name": "search",
|
|
22
|
+
"args": {
|
|
23
|
+
"query": "dismissal"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"role": "user",
|
|
31
|
+
"parts": [
|
|
32
|
+
{
|
|
33
|
+
"functionResponse": {
|
|
34
|
+
"name": "search",
|
|
35
|
+
"response": {
|
|
36
|
+
"found": 3
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"response_body": {
|
|
45
|
+
"candidates": [
|
|
46
|
+
{
|
|
47
|
+
"content": {
|
|
48
|
+
"role": "model",
|
|
49
|
+
"parts": [
|
|
50
|
+
{
|
|
51
|
+
"text": "No."
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"usageMetadata": {
|
|
58
|
+
"promptTokenCount": 120,
|
|
59
|
+
"candidatesTokenCount": 4
|
|
60
|
+
},
|
|
61
|
+
"modelVersion": "gemini-3-pro"
|
|
62
|
+
},
|
|
63
|
+
"route": "gemini",
|
|
64
|
+
"started": 1760000002.0,
|
|
65
|
+
"ended": 1760000003.0,
|
|
66
|
+
"duration_ms": 1000.0
|
|
67
|
+
},
|
|
68
|
+
"expected": [
|
|
69
|
+
{
|
|
70
|
+
"kind": "llm",
|
|
71
|
+
"provider": "gemini",
|
|
72
|
+
"model": "gemini-3-pro",
|
|
73
|
+
"input_tokens": 120,
|
|
74
|
+
"output_tokens": 4,
|
|
75
|
+
"tool_results": [
|
|
76
|
+
{
|
|
77
|
+
"id": "search",
|
|
78
|
+
"size": 12
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"started": 1760000002.0,
|
|
82
|
+
"ended": 1760000003.0,
|
|
83
|
+
"duration_ms": 1000.0
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"exchange": {
|
|
3
|
+
"method": "POST",
|
|
4
|
+
"url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro:streamGenerateContent?alt=sse",
|
|
5
|
+
"status": 200,
|
|
6
|
+
"request_body": {
|
|
7
|
+
"contents": [
|
|
8
|
+
{
|
|
9
|
+
"role": "user",
|
|
10
|
+
"parts": [
|
|
11
|
+
{
|
|
12
|
+
"text": "Is the dismissal valid?"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"response_events": [
|
|
19
|
+
{
|
|
20
|
+
"candidates": [
|
|
21
|
+
{
|
|
22
|
+
"content": {
|
|
23
|
+
"role": "model",
|
|
24
|
+
"parts": [
|
|
25
|
+
{
|
|
26
|
+
"functionCall": {
|
|
27
|
+
"name": "search",
|
|
28
|
+
"args": {
|
|
29
|
+
"query": "dismissal"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"modelVersion": "gemini-3-pro"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"candidates": [
|
|
41
|
+
{
|
|
42
|
+
"content": {
|
|
43
|
+
"role": "model",
|
|
44
|
+
"parts": [
|
|
45
|
+
{
|
|
46
|
+
"text": ""
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"finishReason": "STOP"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"usageMetadata": {
|
|
54
|
+
"promptTokenCount": 80,
|
|
55
|
+
"candidatesTokenCount": 9
|
|
56
|
+
},
|
|
57
|
+
"modelVersion": "gemini-3-pro"
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"route": "gemini",
|
|
61
|
+
"started": 1760000000.0,
|
|
62
|
+
"ended": 1760000001.0,
|
|
63
|
+
"duration_ms": 1000.0
|
|
64
|
+
},
|
|
65
|
+
"expected": [
|
|
66
|
+
{
|
|
67
|
+
"kind": "llm",
|
|
68
|
+
"provider": "gemini",
|
|
69
|
+
"model": "gemini-3-pro",
|
|
70
|
+
"input_tokens": 80,
|
|
71
|
+
"output_tokens": 9,
|
|
72
|
+
"requested_tools": [
|
|
73
|
+
{
|
|
74
|
+
"id": "search",
|
|
75
|
+
"name": "search",
|
|
76
|
+
"arguments": {
|
|
77
|
+
"query": "dismissal"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"started": 1760000000.0,
|
|
82
|
+
"ended": 1760000001.0,
|
|
83
|
+
"duration_ms": 1000.0
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|