CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m22s
Development Plan M0.1. Every section reference in code, tests, specs/, the example config, the tuning guide, and the CLAUDE.md and README files now cites docs/Architecture.md (P, L, D, O, X, Q) instead of the old spec's A-H, and docs/Architecture-Build-Spec.md is deleted (git keeps it). The plan's old-to-new table goes with it. Mostly mechanical, with a per-site choice where one old section split: A4 became L2 where it meant stop/abandon/the loop and L3 where it meant approvals; A10 became L6 in the context-budget code and L7 elsewhere; B2 became D4 for outbound Gitea and publishing and D5 for review intake; 'A3 step N' became the matching step of L8's boot sequence. Comments describing behaviour the architecture drops (planning always escalating, thresholds tuned at an old build step) now describe the current code and point at the Development Plan. docs/Architecture.md gains one line the rewrite missed: intake outages delay but never fail work (old B2), now in L5, where the poller and Matrix code cite it. Comments and docs only; no behaviour change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
102 lines
3.4 KiB
Python
102 lines
3.4 KiB
Python
"""Live Anthropic passthrough: the vendor-native route delegations will use.
|
|
|
|
Proves the renderer's body is accepted as-is, that a tool call round-trips
|
|
through the canonical log, and — per L9 — that prompt caching survives the
|
|
passthrough rather than being flattened by the proxy.
|
|
"""
|
|
|
|
import io
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from nisse_dev.canonical import AssistantText, ToolCall, ToolResult, UserMessage
|
|
from nisse_dev.config import AgentConfig
|
|
from nisse_dev.llm import AnthropicPassthroughClient
|
|
from nisse_dev.oplog import OpLog
|
|
from nisse_dev.state import StateStore
|
|
from nisse_dev.tools import EXECUTOR_TOOLS
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
def store_with(tmp_path: Path, *events) -> StateStore:
|
|
store = StateStore(tmp_path / "state.db", oplog=OpLog("nisse-dev", io.StringIO()))
|
|
for event in events:
|
|
store.append_event(event)
|
|
return store
|
|
|
|
|
|
def test_passthrough_completes_a_plain_turn(
|
|
tmp_path: Path, passthrough: AnthropicPassthroughClient, live_model: str
|
|
) -> None:
|
|
store = store_with(
|
|
tmp_path, UserMessage(text="Reply with exactly: pong", source="live")
|
|
)
|
|
result = passthrough.complete(
|
|
store.read_events(), None, live_model, system="Be terse."
|
|
)
|
|
texts = [e.text for e in result if isinstance(e, AssistantText)]
|
|
assert texts and "pong" in texts[0].lower()
|
|
|
|
|
|
def test_passthrough_round_trips_a_tool_call(
|
|
tmp_path: Path, passthrough: AnthropicPassthroughClient, live_model: str
|
|
) -> None:
|
|
store = store_with(
|
|
tmp_path,
|
|
UserMessage(
|
|
text="List the files in the current directory using your tools.",
|
|
source="live",
|
|
),
|
|
)
|
|
client = passthrough
|
|
produced = client.complete(
|
|
store.read_events(),
|
|
EXECUTOR_TOOLS,
|
|
live_model,
|
|
system="Use a tool. Do not answer in prose.",
|
|
)
|
|
calls = [e for e in produced if isinstance(e, ToolCall)]
|
|
assert calls, f"expected a tool call, got {produced!r}"
|
|
|
|
# Feed the result back: the follow-up render must be accepted, which is
|
|
# what proves tool_result placement is right for this vendor.
|
|
for event in produced:
|
|
store.append_event(event)
|
|
store.append_event(
|
|
ToolResult(call_id=calls[0].call_id, result="app.py\ntests/", success=True)
|
|
)
|
|
follow_up = client.complete(store.read_events(), EXECUTOR_TOOLS, live_model)
|
|
assert follow_up, "the vendor rejected or ignored the tool_result turn"
|
|
|
|
|
|
def test_prompt_caching_survives_the_passthrough(
|
|
live_config: AgentConfig, live_model: str
|
|
) -> None:
|
|
# L9 asks for this to be verified rather than assumed: a proxy that
|
|
# flattened the request would silently drop cache control and turn a
|
|
# cost optimisation into a per-call expense.
|
|
base = live_config.llm_base_url.rstrip("/")
|
|
token = live_config.llm_token
|
|
response = httpx.post(
|
|
f"{base}/v1/messages",
|
|
headers={
|
|
"Authorization": f"Bearer {token}",
|
|
"x-api-key": token,
|
|
"anthropic-version": "2023-06-01",
|
|
},
|
|
json={
|
|
"model": live_model,
|
|
"max_tokens": 16,
|
|
"messages": [{"role": "user", "content": "Say pong."}],
|
|
},
|
|
timeout=120,
|
|
)
|
|
response.raise_for_status()
|
|
usage = response.json().get("usage", {})
|
|
assert "cache_read_input_tokens" in usage, (
|
|
f"cache accounting absent from passthrough usage: {usage}"
|
|
)
|