SPB Git forge

spb/doc-api

Public
2commits 1branches 0releases
15.7 MBsize
maindefault branch
14 days agolast push
Python 88.3% TypeScript 7.6% Shell 4.1%
14.4 KB · 207 lines python
Raw Blame History
1#!/usr/bin/env python32"""Live probe of the Anthropic Messages API core surface (run once on 2026-09-18).34STATUS: LIVE_VERIFIED — every call below ran against api.anthropic.com with the key in .env.5Sanitized raw responses are written to tmp-live/anthropic-core/<name>.json and every call is6logged to reports/live-requests.jsonl by scripts/live.py. Budget: ~40 tiny calls on7claude-haiku-4-5-20251001 (one on claude-sonnet-5), well under $0.30 total.89Run:  .venv/bin/python examples/anthropic/messages/probe_core_live.py10"""11from __future__ import annotations1213import base6414import json15import sys16import zlib17from pathlib import Path1819ROOT = Path(__file__).resolve().parents[3]20sys.path.insert(0, str(ROOT))21from scripts.live import anthropic_request, interesting_headers, save_sanitized  # noqa: E4022223OUT = ROOT / "tmp-live" / "anthropic-core"24MODEL = "claude-haiku-4-5-20251001"25PRICE = {"claude-haiku-4-5-20251001": (1.0, 5.0), "claude-sonnet-5": (3.0, 15.0)}  # USD per 1M in/out (Haiku documented; Sonnet 5 assumed)26SUMMARY: list[dict] = []27TOTAL_COST = 0.0282930def cost(model: str, body) -> float:31    if not isinstance(body, dict) or "usage" not in body:32        return 0.033    u = body["usage"]34    pin, pout = PRICE.get(model, (3.0, 15.0))35    return (u.get("input_tokens", 0) + (u.get("cache_creation_input_tokens") or 0)) * pin / 1e6 + u.get("output_tokens", 0) * pout / 1e6363738def call(name: str, method: str, path: str, body=None, *, beta=None, headers=None, note="", model=MODEL):39    global TOTAL_COST40    st, out, hdrs = anthropic_request(method, path, body, beta=beta, extra_headers=headers, note=f"core-probe {name}: {note}")41    c = cost(model, out)42    TOTAL_COST += c43    rec = {"name": name, "method": method, "path": path, "status": st, "request": body, "beta": beta,44           "extra_headers": {k: ("***" if k.lower() == "x-api-key" else v) for k, v in (headers or {}).items()},45           "response_headers": interesting_headers(hdrs), "response": out if not isinstance(out, bytes) else out.decode("utf-8", "replace"),46           "est_cost_usd": round(c, 6)}47    save_sanitized(rec, OUT / f"{name}.json")48    SUMMARY.append({"name": name, "status": st, "est_cost_usd": round(c, 6),49                    "stop_reason": out.get("stop_reason") if isinstance(out, dict) else None,50                    "error": out.get("error") if isinstance(out, dict) and out.get("type") == "error" else None})51    print(f"[{st}] {name}")52    return st, out, hdrs535455def msg(text="Reply with OK.", **kw):56    b = {"model": MODEL, "max_tokens": 16, "messages": [{"role": "user", "content": text}]}57    b.update(kw)58    return b596061def stream_probe(name: str, body: dict, *, beta=None):62    """Capture the exact ordered SSE event list."""63    global TOTAL_COST64    st, gen, hdrs = anthropic_request("POST", "/v1/messages", body, beta=beta, stream=True, note=f"core-probe {name}: stream")65    events, raw_lines = [], []66    if st == 200:67        cur = {}68        for line in gen:69            raw_lines.append(line)70            if line.startswith("event:"):71                cur = {"event": line[6:].strip()}72            elif line.startswith("data:"):73                try:74                    cur["data"] = json.loads(line[5:].strip())75                except Exception:  # noqa: BLE00176                    cur["data"] = line[5:].strip()77                events.append(cur)78                cur = {}79        final_usage = next((e["data"].get("usage") for e in events if e["event"] == "message_delta"), None)80        start_msg = next((e["data"]["message"] for e in events if e["event"] == "message_start"), {})81        c = 0.082        if final_usage:83            pin, pout = PRICE[MODEL]84            c = start_msg.get("usage", {}).get("input_tokens", 0) * pin / 1e6 + final_usage.get("output_tokens", 0) * pout / 1e685        TOTAL_COST += c86    else:87        c = 0.088        events = [{"event": "http_error", "data": gen if not isinstance(gen, bytes) else gen.decode()}]89    rec = {"name": name, "status": st, "request": body, "response_headers": interesting_headers(hdrs),90           "event_sequence": [e["event"] for e in events], "events": events, "raw_lines": raw_lines, "est_cost_usd": round(c, 6)}91    save_sanitized(rec, OUT / f"{name}.json")92    SUMMARY.append({"name": name, "status": st, "est_cost_usd": round(c, 6), "event_sequence": [e["event"] for e in events]})93    print(f"[{st}] {name}: {[e['event'] for e in events]}")94    return events959697def one_px_png() -> str:98    def chunk(t, d):99        return len(d).to_bytes(4, "big") + t + d + zlib.crc32(t + d).to_bytes(4, "big")100    ihdr = (1).to_bytes(4, "big") + (1).to_bytes(4, "big") + bytes([8, 2, 0, 0, 0])101    idat = zlib.compress(b"\x00\xff\x00\x00")102    png = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", idat) + chunk(b"IEND", b"")103    return base64.b64encode(png).decode()104105106TOOL = {"name": "get_weather", "description": "Get current weather for a city.",107        "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}108109if __name__ == "__main__":110    OUT.mkdir(parents=True, exist_ok=True)111    # (a) minimal112    call("a_minimal", "POST", "/v1/messages", msg(), note="minimal; record undocumented fields")113    # (b) stream114    stream_probe("b_stream", msg(stream=True))115    # (c) system + sampling params + stop sequence116    st, out, _ = call("c_system_sampling_stop", "POST", "/v1/messages",117                      msg("Count: 1 2 3 4 5", system="You are terse. Output only what is asked.", temperature=0.2, top_p=0.9, top_k=40,118                          stop_sequences=["3"], max_tokens=32), note="temperature+top_p+top_k together + stop_sequences")119    if st == 400:120        call("c2_temperature_only_stop", "POST", "/v1/messages",121             msg("Count: 1 2 3 4 5", system="You are terse. Output only what is asked.", temperature=0.2, top_k=40,122                 stop_sequences=["3"], max_tokens=32), note="retry without top_p")123    # (d) max_tokens 1124    call("d_max_tokens_1", "POST", "/v1/messages", msg("Write a paragraph about oceans.", max_tokens=1), note="expect stop_reason max_tokens")125    # (e) metadata.user_id126    call("e_metadata_user_id", "POST", "/v1/messages", msg(metadata={"user_id": "atlas-user-0001"}), note="metadata.user_id")127    # (f) prefill128    prefill = [{"role": "user", "content": "Say the word OK and nothing else."}, {"role": "assistant", "content": "The word is:"}]129    call("f_prefill_haiku", "POST", "/v1/messages", {"model": MODEL, "max_tokens": 8, "messages": prefill}, note="assistant prefill on Haiku 4.5")130    call("f2_prefill_sonnet5", "POST", "/v1/messages", {"model": "claude-sonnet-5", "max_tokens": 8, "messages": prefill},131         note="assistant prefill on claude-sonnet-5 (expect 400)", model="claude-sonnet-5")132    call("f3_multiturn", "POST", "/v1/messages", {"model": MODEL, "max_tokens": 8, "messages": [133        {"role": "user", "content": "Remember the code word: PLUM."}, {"role": "assistant", "content": "Noted."},134        {"role": "user", "content": "Reply with only the code word."}]}, note="multi-turn")135    # (g) count_tokens136    base_ct = {"model": MODEL, "messages": [{"role": "user", "content": "Reply with OK."}]}137    call("g1_count_simple", "POST", "/v1/messages/count_tokens", base_ct, note="count_tokens simple")138    call("g2_count_system", "POST", "/v1/messages/count_tokens", {**base_ct, "system": "You are a helpful assistant."}, note="with system")139    call("g3_count_tool", "POST", "/v1/messages/count_tokens", {**base_ct, "tools": [TOOL]}, note="with tool")140    call("g4_count_image", "POST", "/v1/messages/count_tokens", {"model": MODEL, "messages": [{"role": "user", "content": [141        {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": one_px_png()}},142        {"type": "text", "text": "Reply with OK."}]}]}, note="with 1x1 PNG")143    call("g5_count_document", "POST", "/v1/messages/count_tokens", {"model": MODEL, "messages": [{"role": "user", "content": [144        {"type": "document", "source": {"type": "text", "media_type": "text/plain", "data": "The sky is blue."}, "title": "note",145         "citations": {"enabled": True}}, {"type": "text", "text": "Reply with OK."}]}]}, note="with text document + citations")146    call("g6_count_thinking", "POST", "/v1/messages/count_tokens", {**base_ct, "thinking": {"type": "enabled", "budget_tokens": 1024}}, note="with thinking enabled")147    call("g7_count_output_config", "POST", "/v1/messages/count_tokens", {**base_ct, "output_config": {"format": {"type": "json_schema", "schema": {148        "type": "object", "properties": {"answer": {"type": "string"}}, "required": ["answer"], "additionalProperties": False}}}}, note="with output_config.format")149    call("g8_count_server_tool", "POST", "/v1/messages/count_tokens", {**base_ct, "tools": [{"type": "web_search_20250305", "name": "web_search"}]},150         note="server tool (docs say rejected)")151    # (h) service_tier152    call("h1_service_tier_standard_only", "POST", "/v1/messages", msg(service_tier="standard_only"), note="service_tier standard_only")153    call("h2_service_tier_auto", "POST", "/v1/messages", msg(service_tier="auto"), note="service_tier auto")154    # (i) errors155    call("i1_invalid_model", "POST", "/v1/messages", msg(model="claude-does-not-exist"), note="expect 404 not_found_error")156    b = msg(); del b["max_tokens"]157    call("i2_missing_max_tokens", "POST", "/v1/messages", b, note="expect 400")158    call("i3_bogus_api_key", "POST", "/v1/messages", msg(), headers={"x-api-key": "invalid-key-placeholder"}, note="expect 401")159    call("i4_max_tokens_too_large", "POST", "/v1/messages", msg(max_tokens=1_000_000), note="expect 400")160    call("i5_invalid_version", "POST", "/v1/messages", msg(), headers={"anthropic-version": "2020-01-01"}, note="invalid anthropic-version")161    call("i6_unknown_beta", "POST", "/v1/messages", msg(), beta="does-not-exist-2099-01-01", note="unknown beta header value")162    call("i7_temperature_5", "POST", "/v1/messages", msg(temperature=5), note="expect 400")163    call("i8_top_k_string", "POST", "/v1/messages", msg(top_k="abc"), note="wrong type")164    call("i9_empty_messages", "POST", "/v1/messages", msg(messages=[]), note="empty messages")165    st, out, hdrs = anthropic_request("POST", "/v1/messages", data=b'{"model": ', note="core-probe i10_bad_json")166    save_sanitized({"name": "i10_bad_json", "status": st, "response": out if not isinstance(out, bytes) else out.decode(), "response_headers": interesting_headers(hdrs)}, OUT / "i10_bad_json.json")167    SUMMARY.append({"name": "i10_bad_json", "status": st, "error": out.get("error") if isinstance(out, dict) else None}); print(f"[{st}] i10_bad_json")168    call("i11_not_found_path", "GET", "/v1/does-not-exist", None, note="unknown path")169    call("i12_tool_use_without_result", "POST", "/v1/messages", {"model": MODEL, "max_tokens": 8, "tools": [TOOL], "messages": [170        {"role": "user", "content": "Weather in Paris?"},171        {"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_01ABCDEFGHIJKLMNOPQRSTUV", "name": "get_weather", "input": {"city": "Paris"}}]},172        {"role": "user", "content": "thanks"}]}, note="tool_use without tool_result")173    call("i13_thinking_adaptive_haiku", "POST", "/v1/messages", msg(thinking={"type": "adaptive"}), note="adaptive thinking on 4.5 model (expect 400)")174    call("i14_output_format_legacy_field", "POST", "/v1/messages/count_tokens", {**base_ct, "output_format": {"type": "json_schema", "schema": {"type": "object"}}},175         note="legacy output_format field without beta")176    # (j) models177    call("j1_get_model_alias", "GET", "/v1/models/claude-haiku-4-5", None, note="alias resolution")178    call("j2_list_models", "GET", "/v1/models?limit=3", None, note="list models limit 3")179    # (k) legacy complete180    call("k1_complete_legacy", "POST", "/v1/complete", {"model": "claude-2.1", "max_tokens_to_sample": 8, "prompt": "\n\nHuman: Reply with OK.\n\nAssistant:"},181         note="legacy text completions, retired model", model="claude-2.1")182    call("k2_complete_haiku", "POST", "/v1/complete", {"model": MODEL, "max_tokens_to_sample": 8, "prompt": "\n\nHuman: Reply with OK.\n\nAssistant:"},183         note="legacy text completions with current model")184    # (l) old version header185    call("l_version_2023_01_01", "POST", "/v1/messages", msg(), headers={"anthropic-version": "2023-01-01"}, note="anthropic-version 2023-01-01")186    # tool_use stop reason (cheap) + stream with tool for input_json_delta187    call("m1_tool_use_stop", "POST", "/v1/messages", {"model": MODEL, "max_tokens": 64, "tools": [TOOL], "tool_choice": {"type": "tool", "name": "get_weather"},188                                                       "messages": [{"role": "user", "content": "Weather in Paris?"}]}, note="forced tool_use → stop_reason tool_use")189    stream_probe("m2_stream_tool_use", {"model": MODEL, "max_tokens": 64, "stream": True, "tools": [TOOL], "tool_choice": {"type": "tool", "name": "get_weather"},190                                        "messages": [{"role": "user", "content": "Weather in Paris?"}]})191    # thinking stream on Haiku 4.5 (enabled, budget 1024) — small max_tokens to bound cost192    stream_probe("m3_stream_thinking", {"model": MODEL, "max_tokens": 1100, "stream": True, "thinking": {"type": "enabled", "budget_tokens": 1024},193                                        "messages": [{"role": "user", "content": "Reply with OK."}]})194    # cache_control (top-level) + max_tokens 0 pre-warm (documented) — tiny195    call("n1_max_tokens_0_prewarm", "POST", "/v1/messages", msg(max_tokens=0, cache_control={"type": "ephemeral"}), note="max_tokens 0 + top-level cache_control")196    # inference_geo param existence (may 400 depending on workspace)197    call("n2_inference_geo_us", "POST", "/v1/messages", msg(inference_geo="us"), note="inference_geo us")198    # stop_details / container / context_management presence with beta header for context management199    call("n3_beta_context_management", "POST", "/v1/messages", msg(context_management={"edits": [{"type": "clear_tool_uses_20250919"}]}),200         beta="context-management-2025-06-27", note="beta context_management shape")201    call("n4_speed_fast_haiku", "POST", "/v1/messages", msg(speed="fast"), beta="fast-mode-2026-02-01", note="speed fast on Haiku (expect 400)")202    call("n5_betas_field_in_body", "POST", "/v1/messages", msg(betas=["context-management-2025-06-27"]), note="betas as body field (SDK-only param?)")203204    summary = {"probe": "anthropic-core", "date": "2026-09-18", "model": MODEL, "total_est_cost_usd": round(TOTAL_COST, 6), "calls": SUMMARY}205    save_sanitized(summary, OUT / "_summary.json")206    print(json.dumps({"total_est_cost_usd": round(TOTAL_COST, 6), "n_calls": len(SUMMARY)}))207