From 3f995607cecca04a995c5d4feecf8a1a4ce77d67 Mon Sep 17 00:00:00 2001 From: Phillip Date: Wed, 10 Jun 2026 23:53:32 +0800 Subject: [PATCH 1/2] fix(demo): default temporary-run model to claimed_model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The verifier UI's temporary flow posts only `claimed_model` and no `model` field, but live_report required `model` via require_text and raised "Missing required field: model". Every temporary verification against a real gateway therefore failed with HTTP 400 — only the hardcoded gateway.example.test demo path worked, because it substitutes claimed_model for model. Make `model` optional and default it to `claimed_model`, matching both the demo path and the frontend payload contract. Add a regression test that runs a respx-mocked live temporary verification with no `model`. Signed-off-by: Phillip Co-Authored-By: Claude Opus 4.8 --- src/model_verifier/demo.py | 4 +++- tests/test_demo_server.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/model_verifier/demo.py b/src/model_verifier/demo.py index f84bb45..4db4dd1 100644 --- a/src/model_verifier/demo.py +++ b/src/model_verifier/demo.py @@ -300,8 +300,10 @@ def live_report(body: bytes) -> dict: data = parse_json_body(body) base_url = validate_public_base_url(require_text(data, "base_url")) api_key = require_text(data, "api_key") - model = require_text(data, "model") claimed_model = require_text(data, "claimed_model") + # The verifier UI's temporary flow sends only `claimed_model`; the upstream + # request model defaults to it when an explicit `model` is not provided. + model = str(data.get("model") or "").strip() or claimed_model provider_mode = str(data.get("provider_mode") or "auto").strip() if provider_mode not in {"auto", "openai", "anthropic"}: raise ValueError("provider_mode must be auto, openai, or anthropic") diff --git a/tests/test_demo_server.py b/tests/test_demo_server.py index 6073244..ae31c6b 100644 --- a/tests/test_demo_server.py +++ b/tests/test_demo_server.py @@ -1,5 +1,8 @@ import json +import respx +from httpx import Response + from model_verifier import demo from model_verifier.demo import demo_report, dispatch_demo_request @@ -104,6 +107,36 @@ def test_model_verification_runs_endpoint_accepts_temporary_payload(): assert data["verdict"] == "verified" +def test_temporary_live_run_defaults_model_to_claimed_model(monkeypatch): + # The temporary flow posts only `claimed_model` (no `model`); a live gateway + # run must default the upstream request model to it instead of failing 400. + monkeypatch.setattr(demo, "validate_public_base_url", lambda url: url) + with respx.mock: + respx.post("https://live.example.test/v1/chat/completions").mock(return_value=Response(200, json={ + "model": "gpt-4.1", + "choices": [{"message": {"content": "ok"}}], + "usage": {"prompt_tokens": 5, "completion_tokens": 2, "total_tokens": 7}, + })) + response = dispatch_demo_request( + "POST", + "/api/model-verification/runs", + json.dumps( + { + "source": "temporary", + "base_url": "https://live.example.test", + "api_key": "sk-test-secret", + "claimed_model": "gpt-4.1", + "provider_mode": "openai", + } + ).encode("utf-8"), + ) + data = decode_json(response.body) + + assert response.status == 200 + assert data["claimed_model"] == "gpt-4.1" + assert data["actual_model"] == "gpt-4.1" + + def test_sites_endpoint_is_empty_for_open_source_demo(): response = dispatch_demo_request("GET", "/api/sites", b"") data = decode_json(response.body) From 3696d496978eda5ba43b7fda15eb900225f9b29f Mon Sep 17 00:00:00 2001 From: Phillip Date: Wed, 10 Jun 2026 23:53:32 +0800 Subject: [PATCH 2/2] fix(client): send x-api-key for Anthropic protocol requests Anthropic-mode requests to /v1/messages only sent `Authorization: Bearer` plus `anthropic-version`, never the `x-api-key` header the native Anthropic Messages API requires. Verifying api.anthropic.com (or any native Anthropic endpoint) always failed with 401, all probes errored, and an honest Claude deployment was reported as unreachable/inconclusive. Add an `anthropic_auth_headers` helper that sends `x-api-key` while keeping `Authorization: Bearer` so Anthropic-compatible gateways that expect a bearer token keep working. Add a test asserting both headers. Signed-off-by: Phillip Co-Authored-By: Claude Opus 4.8 --- src/model_verifier/client.py | 15 +++++++++++++-- tests/test_client.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/model_verifier/client.py b/src/model_verifier/client.py index e7d3c51..330580d 100644 --- a/src/model_verifier/client.py +++ b/src/model_verifier/client.py @@ -36,6 +36,17 @@ def target_auth_headers(target: VerificationTarget) -> dict[str, str]: return auth_headers(target.api_key) +def anthropic_auth_headers(target: VerificationTarget) -> dict[str, str]: + # Native Anthropic Messages API authenticates via `x-api-key`. Keep the + # `Authorization: Bearer` header too so Anthropic-compatible gateways that + # expect a bearer token continue to work. + return { + **target_auth_headers(target), + "x-api-key": target.api_key, + "anthropic-version": "2023-06-01", + } + + def build_chat_payload(model_name: str, prompt: str, stream: bool = False) -> dict: payload = { "model": model_name, @@ -197,7 +208,7 @@ def call_model(target: VerificationTarget, prompt: str, options: VerificationOpt try: with httpx.Client(timeout=options.timeout_seconds) as client: if effective_mode == "anthropic": - headers = {**target_auth_headers(target), "anthropic-version": "2023-06-01"} + headers = anthropic_auth_headers(target) res = client.post(target.base_url + "/v1/messages", headers=headers, json=build_anthropic_payload(target.model, prompt)) res.raise_for_status() answer, error, upstream_model = parse_anthropic_message_completion(res) @@ -225,7 +236,7 @@ def run_protocol_verification(target: VerificationTarget, options: VerificationO try: with httpx.Client(timeout=options.timeout_seconds) as client: if effective_mode == "anthropic": - headers = {**target_auth_headers(target), "anthropic-version": "2023-06-01"} + headers = anthropic_auth_headers(target) non_stream = client.post(target.base_url + "/v1/messages", headers=headers, json=build_anthropic_payload(target.model, prompt)) checks.append(parse_anthropic_message_response(non_stream, target.model)) stream = client.post(target.base_url + "/v1/messages", headers=headers, json=build_anthropic_payload(target.model, prompt, stream=True)) diff --git a/tests/test_client.py b/tests/test_client.py index 7c49236..3b1b3f7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -97,3 +97,32 @@ def test_run_protocol_verification_openai_reports_scores(): assert report["protocol_score"] >= 90 assert report["usage_score"] == 75 assert report["observed_model"] == "gpt-4.1" + + +def test_call_model_anthropic_sends_x_api_key_header(): + anthropic_target = VerificationTarget( + source="temporary", + base_url="https://gateway.example.test", + model="claude-3-5-sonnet", + api_key="sk-ant-secret", + api_key_masked="sk-ant...cret", + site_name="临时接入", + ) + with respx.mock: + route = respx.post("https://gateway.example.test/v1/messages").mock(return_value=Response(200, json={ + "model": "claude-3-5-sonnet", + "content": [{"type": "text", "text": "ok"}], + })) + + result = call_model( + anthropic_target, + "hello", + VerificationOptions(claimed_model="claude-3-5-sonnet", provider_mode="anthropic"), + ) + + request = route.calls.last.request + assert request.headers["x-api-key"] == "sk-ant-secret" + assert request.headers["authorization"] == "Bearer sk-ant-secret" + assert request.headers["anthropic-version"] == "2023-06-01" + assert result.answer == "ok" + assert result.upstream_model == "claude-3-5-sonnet"