PageSourceSearch

https://claude-triage-labs.vercel.app/assets/js/2af1d63c.05ab37a5.js

js claude-triage-labs.vercel.app collected 2026-10-03 05:16:52 UTC 21,170 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkwebsite||=[]).push([[3831],{7171(e,n,t){t.r(n),t.d(n,{assets:()=>h,contentTitle:()=>o,default:()=>c,frontMatter:()=>i,metadata:()=>s,toc:()=>l});const s=JSON.parse('{"id":"solutions/lab-9","title":"Lab 9 answers","description":"Q1. Half rate, higher bill. How?","source":"@site/docs/solutions/lab-9.md","sourceDirName":"solutions","slug":"/solutions/lab-9","permalink":"/docs/solutions/lab-9","draft":false,"unlisted":false,"editUrl":"https://github.com/mrlynn/claude-triage-api/edit/main/curriculum/solutions/lab-9.md","tags":[],"version":"current","sidebarPosition":9,"frontMatter":{"title":"Lab 9 answers","sidebar_position":9},"sidebar":"courseSidebar","previous":{"title":"Lab 8 answers","permalink":"/docs/solutions/lab-8"},"next":{"title":"Lab 10 answers","permalink":"/docs/solutions/lab-10"}}');var a=t(4848),r=t(8453);const i={title:"Lab 9 answers",sidebar_position:9},o=void 0,h={},l=[{value:"Extension notes",id:"extension-notes",level:2}];function d(e){const n={a:"a",code:"code",em:"em",h2:"h2",li:"li",ol:"ol",p:"p",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.p,{children:(0,a.jsx)(n.strong,{children:"Q1. Half rate, higher bill. How?"})}),"\n",(0,a.jsxs)(n.p,{children:["Because the batch discount and the prompt-cache discount apply to the same\n",(0,a.jsx)(n.a,{href:"/docs/glossary#token",children:"tokens"}),", and the cache one is much larger."]}),"\n",(0,a.jsxs)(n.p,{children:["A cache read costs ",(0,a.jsx)(n.strong,{children:"0.1\xd7"})," the input rate. The batch discount is ",(0,a.jsx)(n.strong,{children:"0.5\xd7"}),". On\na request dominated by a ~3,400-token cached prefix, the synchronous path pays\n",(0,a.jsx)(n.code,{children:"3400 \xd7 0.1"})," and the batch path pays ",(0,a.jsx)(n.code,{children:"3400 \xd7 0.5"})," on any request that misses\nthe cache \u2014 five times more for the prefix, before the 50% has done anything."]}),"\n",(0,a.jsxs)(n.p,{children:["The measurement: synchronous hit the cache ",(0,a.jsx)(n.strong,{children:"20/20"}),", batch ",(0,a.jsx)(n.strong,{children:"11/20"}),". Nine\nrequests paid full-rate-times-a-half for a prefix the synchronous run got for a\ntenth. Net +23%."]}),"\n",(0,a.jsx)(n.p,{children:"Why the misses happen is structural rather than incidental. Synchronous\nrequests in a loop arrive close together, so the prefix written by the first is\nwarm for the rest. A batch is executed on the provider's schedule and fanned\nout in parallel, so requests land whenever there is capacity and a warm prefix\nis a matter of luck rather than sequence."}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.strong,{children:"The general form, which is the transferable part: two discounts on the same\ntokens compete, they do not compose."})," Anywhere you have stacked optimizations,\ncheck whether the second one destroys the precondition of the first."]}),"\n",(0,a.jsx)(n.p,{children:(0,a.jsx)(n.strong,{children:"Q2. When does batch clearly win?"})}),"\n",(0,a.jsxs)(n.p,{children:["When the prefix is ",(0,a.jsx)(n.strong,{children:"small relative to the variable content"}),", or when requests\nare ",(0,a.jsx)(n.strong,{children:"spaced far enough apart that the cache would be cold anyway"}),"."]}),"\n",(0,a.jsx)(n.p,{children:"Concretely, batch wins when:"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsx)(n.li,{children:"Each request carries mostly unique content \u2014 long documents to summarize,\ndistinct transcripts to extract from \u2014 so the cacheable fraction is small and\nthere is little discount to lose."}),"\n",(0,a.jsx)(n.li,{children:"Requests are separated by more than the cache TTL (~5 minutes), so every\nrequest pays full rate under either scheme and the 50% is pure gain."}),"\n",(0,a.jsx)(n.li,{children:"You have no shared prefix at all."}),"\n"]}),"\n",(0,a.jsx)(n.p,{children:"Batch loses when a large stable prefix would otherwise stay warm \u2014 which is\nexactly this repo's shape, and, worth noticing, exactly the shape of most\npolicy-grounded or RAG-style systems. That is a large and popular category to\nbe quietly wrong about."}),"\n",(0,a.jsxs)(n.p,{children:["Note the framing the question forces: ",(0,a.jsx)(n.strong,{children:"ticket volume is not the variable."}),"\nVolume tells you the total bill; the prefix-to-variable ratio and the request\nspacing tell you which mechanism is cheaper per ticket. People reach for batch\nbecause the number of items is large, and the number of items is the one thing\nthat does not decide it."]}),"\n",(0,a.jsx)(n.p,{children:(0,a.jsx)(n.strong,{children:"Q3. Does this apply to a 400,000-ticket one-time backfill?"})}),"\n",(0,a.jsxs)(n.p,{children:["Probably not, and the reason is instructive: ",(0,a.jsx)(n.strong,{children:"the workload is different in a\nway that flips the mechanism back."})]}),"\n",(0,a.jsx)(n.p,{children:"At 400,000 requests, a batch runs for hours with continuous fan-out. The prefix\ngets written early and stays hot for the duration, so the cache-miss rate that\nhurt twenty tickets should be far lower across four hundred thous
1and. The\nmisses in this lab are largely a small-N startup effect: eleven of twenty were\nhits because the first few had to establish the prefix, and there was not enough\nvolume afterwards to amortise it."}),"\n",(0,a.jsxs)(n.p,{children:["What to measure before committing: ",(0,a.jsx)(n.strong,{children:"run a pilot batch of ~500 tickets and read\nthe cache-hit rate"}),", which ",(0,a.jsx)(n.code,{children:"npm run triage:queue:batch"})," now reports. If it is\nabove ~90%, the discounts effectively do stack and batch wins outright. If it is\nstill near 55%, they do not, and you either stay synchronous or restructure the\nprompt so the handbook is not on the cached path."]}),"\n",(0,a.jsx)(n.p,{children:"The habit worth taking: extrapolating a per-unit cost from twenty items to four\nhundred thousand assumes the mechanism is scale-invariant. Caching is not. A\npilot costs a few dollars and answers it."}),"\n",(0,a.jsx)(n.p,{children:(0,a.jsxs)(n.strong,{children:["Q4. Why wrap ",(0,a.jsx)(n.code,{children:"fetch"})," rather than use ",(0,a.jsx)(n.code,{children:".withResponse()"}),"?"]})}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.em,{children:"Coverage."})," The wrapper sees every call the client makes \u2014 ",(0,a.jsx)(n.code,{children:"messages.parse"}),",\n",(0,a.jsx)(n.code,{children:".stream()"}),", each internal turn of the beta tool runner, ",(0,a.jsx)(n.code,{children:"count_tokens"}),",\n",(0,a.jsx)(n.code,{children:"batches.*"}),". Instrumenting at the call sites means instrumenting the ones\nsomebody remembered, and the tool runner's internal turns are not call sites you\nhave access to at all. A snapshot you only collect when you remember is a\nsnapsh
1ot you do not have on the day it matters."]}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.em,{children:"The bug."})," ",(0,a.jsx)(n.code,{children:".withResponse()"})," does not exist at runtime on ",(0,a.jsx)(n.code,{children:"messages.parse()"}),".\n",(0,a.jsx)(n.code,{children:"parse()"})," is an ",(0,a.jsx)(n.a,{href:"/docs/glossary#sdk",children:"SDK"})," helper that returns a plain Promise, not the ",(0,a.jsx)(n.code,{children:"APIPromise"}),"\nthat carries ",(0,a.jsx)(n.code,{children:".withResponse()"}),". TypeScript accepted the call and it threw at\nruntime \u2014 a signature that type-checks and does not exist. The lesson is not\nabout this method: it is that a type is a claim about an interface, and helpers\nthat wrap the transport can present a narrower one than the thing they wrap.\nExecute the path before you rely on it."]}),"\n",(0,a.jsx)(n.p,{children:"There is a third reason worth naming: the routes stay unaware of it. Not one\nhandler mentions rate limits, so nobody has to remember the convention when\nadding a fifth route."}),"\n",(0,a.jsx)(n.p,{children:(0,a.jsx)(n.strong,{children:"Q5. Input order and no partial results \u2014 defend both."})}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.strong,{children:"Input order"}),", because the caller indexes results against the cases it sent.\nAn ",(0,a.jsx)(n.a,{href:"/docs/glossary#evaluation--eval",children:"eval"})," harness zips scores back onto ",(0,a.jsx)(n.code,{children:"dataset.jsonl"})," rows to report ",(0,a.jsx)(n.em,{children:"which"}),"\ncase moved, and ",(0,a.jsx)(n.a,{href:"/docs/labs/lab-0-scoreboard",children:"Lab 0"})," established that naming\nthe case is the whole value of the baseline \u2014 ",(0,a.jsx)(n.code,{children:"+2"})," may be noise, ",(0,a.jsx)(n.code,{children:"newly passing: eval-03"})," is something you can go look at. Completion order would\nsilently mis-attribute every score to the wrong ticket the moment one call ran\nslow, and nothing about the output would look wrong."]}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.strong,{children:"No partial results"}),', because a half-finished eval that reports a score is\nworse than one that fails loudly. A score is a number people paste into a PR\nand compare against a baseline, and it carries no marking that says "8 of 12\ncases actually ran." The failure mode of partial results is not a missing\nnumber, it is a ',(0,a.jsx)(n.em,{children:"confident wrong"})," number \u2014 which is the same argument\n",(0,a.jsx)(n.code,{children:"pricingFor()"})," makes when it throws on an unknown ",(0,a.jsx)(n.a,{href:"/docs/glossary#model",children:"model"})," id rather than\ndefaulting, and the same one ",(0,a.jsx)(n.a,{href:"/docs/labs/lab-8-trust-boundary",children:"Lab 8"})," makes\nabout fabricated citations. Silence and a plausible value is the expensive\ncombination."]}),"\n",(0,a.jsxs)(n.p,{children:["Worth noticing that both choices cost something real. Input order means one\nslow case holds a result slot while faster work finishes around it, and\nthrowing means a single flaky call discards eleven good results and the money\nspent on them. Both are the right trade for a harness you re-run in a minute\nfor $0.09. Neither would be right for a 400,000-ticket backfill, which is\nexactly the workload Q3 says to measure separately \u2014 there you want\ncheckpointing and a dead-letter path, and ",(0,a.jsx)(n.code,{children:"docs/architecture.md"})," lists both as\nthings this repo deliberately does not have."]}),"\n",(0,a.jsx)(n.p,{children:(0,a.jsx)(n.strong,{children:"Q6. Why not add a second retry layer?"})}),"\n",(0,a.jsx)(n.p,{children:"Because retries multiply rather than add. Three SDK retries inside three of\nours is nine requests for one logical call, arriving in a burst against the\nlimit that just rejected you \u2014 the mechanism that is supposed to relieve\npressure becomes the thing applying it. This is the classic retry-storm shape,\nand it is worse in an agent loop where a single user request already fans out\ninto several model calls."}),"\n",(0,a.jsx)(n.p,{children:"The second problem is attribution. With two layers, neither is tunable: the\nobserved backoff is the product of two policies configured independently, and\nchanging one has effects you cannot predict from reading it."}),"\n",(0,a.jsxs)(n.p,{children:["What ",(0,a.jsx)(n.code,{children:"AdaptiveGate"})," does instead is ",(0,a.jsx)(n.strong,{children:"backpressure, not retry"}),". It changes how\nmany requests are in flight, and leaves the fate of any individual request to\nthe SDK. One retry layer, one concurrency layer, clean separation. Halve on\nfailure and recover by one is additive-increase/multiplicative-decrease \u2014 TCP's\ncongestion control, for the same reason: shed load fast, reclaim it slowly. A\nsystem that recovers as aggressively as it backs off oscillates between\nhammering and hiding."]}),"\n",(0,a.jsx)(n.p,{children:(0,a.jsx)(n.strong,{children:"Q7. Your eval drops two points on a Tuesday. Nothing deployed."})}),"\n",(0,a.jsxs)(n.p,{children:["First: ",(0,a.jsx)(n.strong,{children:"do not assume it is the model."})," On a twelve-case set the run-to-run\nspread is already two cases with nothing changed, so a two-point move is inside\nthe noise. That is the honest first answer and most investigations should stop\nthere."]}),"\n",(0,a.jsx)(n.p,{children:"If you want to actually establish it, in order:"}),"\n",(0,a.jsxs)(n.ol,{children:["\n",(0,a.jsxs)(n.li,{children:[(0,a.jsx)(n.strong,{children:"Re-run several times."})," Five runs turn a point into a distribution, cost\nabout $0.45, and settle most of these."]}),"\n",(0,a.jsxs)(n.li,{children:[(0,a.jsx)(n.strong,{children:"Check which cases moved"}),", not how many. ",(0,a.jsx)(n.code,{children:"eval:quick"})," prints\n",(0,a.jsx)(n.code,{children:"regressed:"})," and ",(0,a.jsx)(n.code,{children:"newly passing:"})," by id. A drift in the model tends to move\na ",(0,a.jsx)(n.em,{children:"class"})," of cases; noise moves whichever case is marginal."]}),"\n",(0,a.jsxs)(n.li,{children:[(0,a.jsx)(n.strong,{children:"Rule out the model before blaming it."})," On ",(0,a.jsx)(n.code,{children:"claude-opus-5"})," and\n",(0,a.jsx)(n.code,{children:"claude-sonnet-5"})," the id is a pinned snapshot, so the same id means the\nsame model. Only an alias, like ",(0,a.jsx)(n.code,{children:"claude-haiku-4-5"}),", can resolve to\nsomething new. The response's ",(0,a.jsx)(n.code,{children:"model"})," field tells you what actually served\nthe request; log it. If it matches last week's, look at what did change:\nthe handbook, a prompt, a ",(0,a.jsx)(n.a,{href:"/docs/glossary#structured-outputs--schema",children:"schema"})," ",(0,a.jsx)(n.code,{children:".describe()"}
1),", the SDK version."]}),"\n"]}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.strong,{children:"What you needed beforehand"})," is the part that matters:"]}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsxs)(n.li,{children:["A ",(0,a.jsx)(n.strong,{children:"checked-in baseline"})," recording passing case ids, not just a count."]}),"\n",(0,a.jsxs)(n.li,{children:[(0,a.jsx)(n.strong,{children:"Explicit ids"})," for any tier still on an alias, and the response ",(0,a.jsx)(n.code,{children:"model"}),'\nfield recorded with every run, so "did the model change?" takes a grep.']}),"\n",(0,a.jsxs)(n.li,{children:["A ",(0,a.jsx)(n.strong,{children:"scheduled run"})," so the drift shows up on a Monday in a job summary rather\nthan in a Thursday incident. That is the ",(0,a.jsx)(n.code,{children:"model-upgrade"})," cron."]}),"\n"]}),"\n",(0,a.jsx)(n.p,{children:"None of that is buildable after the fact, which is the entire argument for\nbuilding it before you need it."}),"\n",(0,a.jsx)(n.p,{children:(0,a.jsx)(n.strong,{children:"Q8. Tool or resource?"})}),"\n",(0,a.jsx)(n.p,{children:(0,a.jsx)(n.strong,{children:"If the client would be equally well served by reading the thing, it is a\nresource. Tools are for work."})}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.code,{children:"search_policy"})," does retrieval: it takes a query, ranks sections, returns a\ncomputed subset. The client cannot produce that without calling us. The\nhandbook is content \u2014 it has a stable URI and it does not change per request.\nPublishing it as a tool would mean a model had to invent a search query to\nreach text it could simply have read, and would then get a ",(0,a.jsx)(n.em,{children:"subset"})," chosen by\nour ranking rather than the document."]}),"\n",(0,a.jsxs)(n.p,{children:["A second test that often decides it: ",(0,a.jsx)(n.strong,{children:"does the answer depend on the arguments?"}),"\nIf there are no meaningful arguments, you are probably describing a resource\nwith a function signature bolted on."]}),"\n",(0,a.jsxs)(n.p,{children:["Examples of things commonly mis-modelled as tools: ",(0,a.jsx)(n.code,{children:"get_current_config"}),",\n",(0,a.jsx)(n.code,{children:"list_available_regions"}),", ",(0,a.jsx)(n.code,{children:"fetch_pricing_table"}),", ",(0,a.jsx)(n.code,{children:"get_user_preferences"}),". Each\nis a stable document behind a verb. Modelling them as resources means a client\ncan read them once and cache them, instead of burning a tool call and a round\ntrip every time the model wonders."]}),"\n",(0,a.jsxs)(n.p,{children:["Note also the middle case this repo splits: the handbook is published whole\n",(0,a.jsx)(n.em,{children:"and"})," by section. A client that needs section 5 should not pay for all eight."]}),"\n",(0,a.jsx)(n.p,{children:(0,a.jsxs)(n.strong,{children:["Q9. Should ",(0,a.jsx)(n.code,{children:"/v1/resolve"})," get an orchestrator?"]})}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.em,{children:"For:"})," an orchestrator could decompose an unusual ticket into sub-questions,\nrun them in parallel, and synthesize. On a message raising three unrelated\nissues it would plausibly do better than one linear loop, and it would extend\nto tools nobody has written yet without changing the control flow."]}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.em,{children:"Against, and this is the answer:"})," the work is already bounded. There are three\ntools and a known set of questions \u2014 what did they order, who are they, what\ndoes policy say. ",(0,a.jsx)(n.code,{children:"/v1/resolve"})," earns being an agent because the ",(0,a.jsx)(n.em,{children:"order"})," of\nlookups depends on earlier results, and that is the whole of the dynamism\npresent. An orchestrator would add a model call whose job is to plan work that\nis already enumerable, buying latency and unpredictability in exchange for\nflexibility this domain does not use."]}),"\n",(0,a.jsxs)(n.p,{children:["It would also make the ",(0,a.jsx)(n.a,{href:"/docs/glossary#guardrail",children:"guardrails"})," harder. ",(0,a.jsx)(n.code,{children:"enforceAuthority"}),' reads a flat tool\ntrace; a fan-out produces a tree, and "which sub-agent looked up the refund\ntotal" becomes a question you have to answer before you can check the ceiling.\nComplexity in the control flow propagates into the controls.']}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.strong,{children:"What would change the answer:"})," if Northwind's tickets routinely raised\nseveral independent issues needing genuinely different resolution paths \u2014 a\nrefund ",(0,a.jsx)(n.em,{children:"and"})," a warranty claim ",(0,a.jsx)(n.em,{children:"and"})," an account change, each with its own policy\nand its own authority limit \u2014 then the linear loop starts serialising work that\nhas no reason to be serial, and the sub-tasks are no longer enumerable in\nadvance. Note that this is a claim about the ",(0,a.jsx)(n.em,{children:"ticket distribution"}),", and it is\nmeasurable: count the multi-intent tickets. ",(0,a.jsx)(n.code,{children:"NW-T-1060"})," is one. If it were\nthirty percent of the queue rather than one in twenty, the argument shifts."]}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.strong,{children:"The meta-point, and the reason this question is last:"})," naming a pattern you\ndid not need is more useful than naming the three you did. The four patterns\nare vocabulary, not a checklist, and the most common failure is reaching for the\nmost impressive one because it has a name."]}),"\n",(0,a.jsx)(n.h2,{id:"extension-notes",children:"Extension notes"}),"\n",(0,a.jsxs)(n.p,{children:["The question at the end is the real one. ",(0,a.jsx)(n.code,{children:"AdaptiveGate"})," halves on failure and\nrecovers by one, so if the true limit sits just below your starting width, the\nsteady state is: run at width ",(0,a.jsx)(n.code,{children:"w"}),", get a 429, drop to ",(0,a.jsx)(n.code,{children:"w/2"}),", climb back one at a\ntime, get a 429 again. You spend most of your time below capacity and you take\na rejection on every climb."]}),"\n",(0,a.jsx)(n.p,{children:"Three fixes, in increasing sophistication:"}),"\n",(0,a.jsxs)(n.ol,{children:["\n",(0,a.jsxs)(n.li,{children:[(0,a.jsx)(n.strong,{children:"Remember the last width that failed"})," and stop recovering one below it,\nwhich is TCP's slow-start threshold. Cheap and effective."]}),"\n",(0,a.jsxs)(n.li,{children:[(0,a.jsx)(n.strong,{children:"Recover on a timer rather than per batch"}),", so a long clean run widens and\na short one does not."]}),"\n",(0,a.jsxs)(n.li,{children:[(0,a.jsxs)(n.strong,{children:["Read ",(0,a.jsx)(n.code,{children:"anthropic-ratelimit-requests-remaining"})," and set the width from it\ndirectly"]})," rather than discovering the limit by hitting it. You already have\nthe header \u2014 ",(0,a.jsx)(n.code,{children:"readRateLimitHeaders"})," parses it \u2014 and probing a limit you were\ntold about is the kind of thing that looks sophisticated and is not."]}),"\n"]}),"\n",(0,a.jsx)(n.p,{children:"Option 3 is the one most teams skip, and it is the cheapest."})]})}function c(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,a.jsx)(n,{...e,children:(0,a.jsx)(d,{...e})}):d(e)}},8453(e,n,t){t.d(n,{R:()=>i,x:()=>o});var s=t(6540);const a={},r=s.createContext(a);function i(e){const n=s.useContext(r);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:i(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.