feat: expose the 16 unreachable Tako request options - #11
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Final review fix wave on feat/request-options (5 findings): add key-parity tests for OutputSettings and Sources (the two request schemas without a drift trip-wire), fix the README takoAnswer web count default (3, not 5), drop a redundant config spread in contents.ts, and complete the quoteOnly and maxChars JSDoc/README defaults per the spec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
robertabbott
left a comment
There was a problem hiding this comment.
Findings are inline. The 16 mappings themselves check out field-by-field against the vendored spec, all four enums match, and every property of all seven request schemas is now reachable — the stated gap really is closed. What I flagged is around the edges: one README line that the PR falsifies and leaves in place, the tool description the model reads under quoteOnly, and the liveness of the parity gate the PR body leans on.
| | Option | Type | Notes | | ||
| | --- | --- | --- | | ||
| | `mode` | `"url" \| "inline"` | Default `"url"`. Changes the tool description the model reads. | | ||
| | `contentFormat` | `"csv" \| "json_records" \| "json_compact"` | Server default `"csv"` on this surface. | |
There was a problem hiding this comment.
README:177 still says Requesting a specific format is not configurable yet — is that line meant to survive this PR? It sits three lines under the table explaining which payload field each content_format populates, so a reader who reaches the records / dataset rows is told the option this PR adds does not exist. It contradicts the diff's own contentFormat rows here and at line 109. Was it missed because the new options landed in a new section rather than in the existing format table?
There was a problem hiding this comment.
Missed, and your diagnosis is right — the new options landed in a new section, so the existing format table was never re-read.
Fixed in abafe3d. That paragraph now says which surface returns what when contentFormat is unset, then names where to set it: on takoContents for an explicit fetch, on sources.data for a card inlined by a search.
| path: "/api/v1/contents", | ||
| apiKey: resolveApiKey(config), | ||
| body: buildContentsRequestBody(url, mode), | ||
| body: buildContentsRequestBody(url, config), |
There was a problem hiding this comment.
With quoteOnly: true in the config, the description the model reads still promises content — is that intended? The description branches on mode only (lines 31-35): "url" tells the model to surface a presigned link, "inline" tells it to compute over the numbers. The spec says a quote-only response returns cost + export_pricing with url, data, records and dataset all null. So the model is told to surface a link, gets null, and its cheapest recovery is to call again. Should quoteOnly add a third description branch?
There was a problem hiding this comment.
Not intended — this one was a real hole. Fixed in 6665f40.
quoteOnly now takes its own branch ahead of mode, because the API ignores mode on a quote: the description says the call returns cost and rate card, that url and data are always null, and not to call again expecting rows. The wire body still sends both fields; only the description reorders.
Covered by a test asserting all three variants are distinct — quote-only does not mention computing over numbers, and neither delivery branch mentions quotes.
| articleContentMaxChars: 1, publishedAfter: "2026-01-01", | ||
| publishedBefore: "2026-01-02", | ||
| }); | ||
| expect(Object.keys(body).sort()).toEqual(propertiesOf("WebSourceSettings").sort()); |
There was a problem hiding this comment.
This assertion fires only after somebody runs pnpm spec:refresh by hand — does that hold up the PR body's claim that the gap cannot silently reopen? propertiesOf reads the vendored tests/contract/openapi.yaml, a pinned snapshot; ci.yml runs build/typecheck/test, nothing refreshes it, and there is no scheduled workflow. When Tako adds a web option every parity test stays green until a human happens to refresh — the same silence the 3.0.0 drift ran in. Is a scheduled refresh-and-diff job in scope here?
There was a problem hiding this comment.
It does not, and the PR body was wrong. Corrected there.
These assertions fail when a refreshed spec disagrees with the mapping. They cannot detect an option Tako adds until someone runs pnpm spec:refresh. That is a regression gate on this repo, not a live upstream detector — the same overclaim I flagged on #9's oracle wording, which I then reproduced here.
The scheduled refresh-and-diff job is deliberately not in this PR: cadence and who triages the noise are decisions, and it is the same call #9 made. The body now states what the gate actually guarantees and names the remaining window.
One thing I did tighten within the snapshot: the three request enums were pinned only against the tako-sdk devDependency, so the two references could disagree. See the reply on types.conformance.ts:134.
| */ | ||
| export function buildDataSourceSettings(o: TakoDataSourceOptions): DataSourceSettingsBody { | ||
| if (o.strict && !o.nodeIds?.length) { | ||
| throw new Error( |
There was a problem hiding this comment.
strict without nodeIds is a static config mistake, but this throw runs inside execute — should it fire at takoSearch() instead? buildDataSourceSettings is called per tool call (src/tools/search.ts:48, answer.ts:48), so the AI SDK turns it into a tool error the model reads, once per invocation. The message tells the reader to add ids from /v1/graph or set strict: false; the model can do neither, and re-calling reproduces it until the step limit. The developer never sees it at wiring time.
There was a problem hiding this comment.
Agreed, and this overrides a deferral I accepted earlier on weaker grounds. Fixed in 6665f40.
Extracted assertValidDataSourceOptions plus assertValidRetrievalConfig, called from takoSearch() and takoAnswer() at construction. buildDataSourceSettings still calls the former, so direct callers stay guarded and the invariant lives in one place.
Your point about who reads the message is what decided it: the text names two remedies, and the model has access to neither. Three tests cover it — construction throws before any fetch, the deprecated tako alias throws too, and strict with nodeIds still builds and reaches the wire.
| * Map web source options to the API's `WebSourceSettings`. | ||
| * | ||
| * Numeric bounds are deliberately not checked here. The schema carries them and | ||
| * the API enforces them, so a limit raised by Tako needs no release of this SDK. |
There was a problem hiding this comment.
Lines 138-140 hardcode fast / US / en-US and send them on every request — doesn't that mirror three server defaults, which this comment says the file deliberately does not do? A default Tako changes server-side then needs an SDK release, the rot the policy exists to prevent. Related: the PR body says A CI check asserts no bound leaks into the two files where the options live, but ci.yml runs only build/typecheck/test and no test reads either file's source text — is that check still to come?
There was a problem hiding this comment.
Both halves were right. Fixed in 6665f40 and 756239b.
The three defaults. They mirrored the spec's own values ten lines under the comment saying this file does not do that. buildSearchRequestBody now sends only query unconditionally; effort, country_code and locale appear only when set. Behavior-neutral today — the spec's defaults are exactly fast/US/en-US and only query is required — so Tako can now change one without a release here. I had told myself these were fine as pre-existing behavior, which was true and beside the point.
The CI claim was false. I ran that grep by hand during verification and wrote it up as though it were automated. There is now an actual test over the source text of request.ts and types.ts, stripping comments first so limits stay documentable in prose. Verified load-bearing rather than assumed: a Math.min(count, 20) clamp fails it, const CEILING = 2000 fails it, and 2000 inside a comment still passes.
| | `maxChars` | `number` | Web page text only. Server default 1000000, the full page text. | | ||
| | `quoteOnly` | `boolean` | Price the export without fetching it. The request is free and the payload is null. | | ||
|
|
||
| This SDK does not check the numeric ranges. The API enforces them and returns a 400, so a limit Tako raises works immediately without an SDK release. |
There was a problem hiding this comment.
The API enforces them and returns a 400 — is that right for maxRows? The spec says Raise it to export more rows, up to the 2,000-row system ceiling (Tako clamps larger values), and that billing counts the rows actually returned. So an over-ceiling maxRows is silently truncated and billed, not rejected — the one numeric option where a caller who trusts the 400 gets a short export and no signal. Worth splitting maxRows out of this sentence?
There was a problem hiding this comment.
Right, and this is the one that can cost money quietly. Fixed in abafe3d.
maxRows is split out of that sentence. The note now says most out-of-range values return a 400, then separately: a value above the 2,000-row ceiling is clamped rather than rejected, billing counts the rows actually returned, so you get a short export, a charge for it, and no error. It points at total_rows and truncated as the check.
| // the API rejects the whole request. `request.contract.test.ts` does not reach | ||
| // them: it validates one effort value and iterates hand-written mode literals, | ||
| // so neither enumerates what the union declares. | ||
| export const effort: TakoSearchEffort = null as unknown as OfficialSearchEffortLevel; |
There was a problem hiding this comment.
These three request enums are gated against the tako-sdk devDependency but not against the vendored spec — is that asymmetry deliberate? response.contract.test.ts:81,109 pins ContentsFormat and TakoSourceIndex with enumOf(...) from the same snapshot these parity tests read. Without the equivalent here, a pnpm spec:refresh that adds a fourth effort level passes CI until someone separately bumps tako-sdk, so the two halves of the drift gate can disagree about which enum is current.
There was a problem hiding this comment.
Not deliberate — an asymmetry I did not notice. Fixed in 756239b.
Added enumOf() pins for SearchEffortLevel, ContentsDeliveryMode and WebCategory against the vendored spec, matching how response.contract.test.ts:81,109 pins the response-side enums. Both halves of the gate now read the same snapshot, so a refresh that adds a fourth effort level fails immediately instead of waiting on a tako-sdk bump.
The tako-sdk assignments stay — they catch the reverse case, where the generated client moves and the spec has not been refreshed.
|
|
||
| export interface TakoSourceOptions { | ||
| /** Max results for this source, 1–20 (server default 5). */ | ||
| /** Max results for this source, 1-20 (server default 5). */ |
There was a problem hiding this comment.
server default 5 is the JSDoc a developer hovers on sources.web.count for both tools, but the spec's /v1/answer description says This endpoint returns 3 web results when you omit sources.web.count — should the shared comment carry the split the README states at line 118? Same shape two blocks down: the publishedAfter / publishedBefore JSDoc (lines 81-84) omits Results with no known publication date are kept, so a caller reading it as a recency guarantee gets undated pages.
There was a problem hiding this comment.
Both fixed in abafe3d.
TakoSourceOptions.count now carries the split the README states: 1-20, takoSearch returns 5, takoAnswer returns 3, set it when you need the same count from both. It is the shared type, so the hover was telling half the readers the wrong number.
publishedAfter/publishedBefore now say outright that this is not a recency guarantee and that the API keeps a result whose publication date it does not know. That was the reading your comment describes, and the old text invited it.
…de, and two JSDoc gaps README said requesting a specific format is not configurable yet. This PR makes that false: contentFormat is settable on takoContents and on sources.data. The numeric-range note claimed the API returns a 400. maxRows does not. A value above the 2000-row ceiling is clamped and billing counts the rows actually returned, so a caller who trusts the 400 gets a short export, a charge, and no signal. Split maxRows out and name total_rows/truncated as the check. TakoSourceOptions.count is shared by both tools but said server default 5; takoAnswer returns 3. publishedAfter/publishedBefore read as a recency guarantee while the API keeps results whose publication date it does not know. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…escribe quote-only mode Three review findings, all about a caller or a model acting on something this package told them wrongly. The strict/nodeIds guard fired inside `execute`, so the AI SDK handed it to the model as a tool error, once per invocation. The message says to add ids from /v1/graph or set strict to false; the model can do neither, and its cheapest recovery is to call again until the step limit. A contradictory config is a wiring mistake, so it now throws from takoSearch() and takoAnswer(), where the developer sees it. Extracted assertValidDataSourceOptions and assertValidRetrievalConfig; buildDataSourceSettings still calls the former, so direct callers stay guarded and the invariant lives in one place. buildSearchRequestBody sent effort=fast, country_code=US and locale=en-US on every request. Those are the spec's own defaults, restated where they can rot — the same failure that made 3.0.0 necessary, and the thing the comment ten lines above claims this file does not do. Only `query` is required, so only `query` is now unconditional. Verified behavior-neutral: the spec's defaults for all three are exactly these values, so Tako applies the same ones and can change them without a release here. takoContents' description branched on `mode` alone. With quoteOnly the API ignores mode and returns null for url and every payload field, so the model was told to surface a link or compute over numbers, got null, and would call again. quoteOnly now takes its own branch and says there is no content to expect. The wire body still carries both fields. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ds policy The three request-side enums were gated only against the tako-sdk devDependency. The two references move independently, so a `pnpm spec:refresh` adding a fourth effort level would pass CI until somebody separately bumped tako-sdk, letting the two halves of the drift gate disagree about which enum is current. Pinned with enumOf() against the vendored spec, matching how response.contract.test.ts already pins ContentsFormat and TakoSourceIndex. The no-mirrored-bounds rule was a comment and a claim in the pull request body, not a check. It is now a test over the source text of request.ts and types.ts. Comments are stripped first, because the bounds are documented in prose on purpose and only executable code can rot. Verified load-bearing: a Math.min(count, 20) clamp fails it, `const CEILING = 2000` fails it, and 2000 in a comment still passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Addressed 8 comments from this review round — 3 behavior changes, 4 doc corrections, 1 test gap, plus two false claims retracted from the PR body. 84 tests (was 74). @claude-address |
Every test in this repo imported from src/, so a fault that exists only in the published tarball had nothing looking for it: a file missing from `files`, an `exports` map a real resolver rejects, a devDependency imported at run time, or a peer dependency the package needs and does not declare. The previous check for this was a grep of dist/ for "tako-sdk", which covers one of the four. `npm pack` produces the same tarball `npm publish` uploads, so none of this needs a publish to find — which matters because an npm version cannot be republished. Both new steps run in ci.yml and again in the publish job ahead of `pnpm publish`. scripts/verify-package.mjs packs, installs the tarball into a scratch project alongside the peer dependencies a consumer would install, imports the package, builds all three tools, asserts the contents description still varies with config and the strict/nodeIds guard survived the build, then type-checks a snippet against the shipped .d.ts under nodenext resolution — the mode that reads the `exports` map. Verified load-bearing against four injected faults, each caught with its own message: dropping a type export, adding tako-sdk as a runtime dependency, pointing `exports` at a missing file, and cutting dist from `files`. lint:package adds publint and are-the-types-wrong. Both run via npx, so the lockfile is untouched; publint is pinned to `--pack npm` so it does not depend on the local pnpm being runnable. are-the-types-wrong ignores cjs-resolves-to-esm, since ESM-only is deliberate here and dynamic import is the intended contract for a CommonJS consumer. publint found a real fault while being wired up: `exports["."]` listed `import` before `types`, and those conditions are order-sensitive, so a resolver can miss the types. are-the-types-wrong was green on it — TypeScript has a special case that hid it — which is the argument for running both. Reordering also cleared publint's second error about types resolving as ESM under `require`. Added `sideEffects: false` while there, so bundlers can tree-shake. The scratch type check sets skipLibCheck true deliberately: with it false, tsc audits every .d.ts under node_modules and the `ai` package's own tree reports missing @types/node and @types/json-schema, which is another package's noise and loud enough to hide a real failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Why
3.0.0 realigned every response type against the API and built a CI drift gate for them. It never touched the request surface. Counted against the vendored spec, 16 request options were unreachable from this SDK — so "the types match the current API" was true in one direction only.
One case was sharper than a gap. 3.0.0 added
TakoDataset/recordstypes to parsejson_recordsandjson_compactpayloads, butcontent_formatwas not settable on either surface, so no caller could ask for the shapes it had just learned to read.SearchRequestlocationDataSourceSettingsmode,content_format,node_ids,strictWebSourceSettingscategory,include_domains,exclude_domains,snippet_max_chars,article_content_max_chars,published_after,published_beforeContentsRequestcontent_format,max_rows,max_chars,quote_only/v1/answerand/v3/searchshare the identicalSearchRequestschema, so every search-side addition reaches both tools through one builder.The design decision that shaped the diff
No server-side numeric bound is mirrored client-side. The spec carries real limits —
maxItems: 20on the domain arrays,1..20on counts, a 2,000-row ceiling — and none of them appear insrc/. That is deliberate, and it is the lesson of 3.0.0: the "capped at 1000 rows" claim this package shipped for two months was stale precisely because a number was duplicated where it could rot. The API stays the authority, so a limit Tako raises works immediately with no release here. A test over the source text ofsrc/request.tsandsrc/types.tsenforces this, stripping comments first so the limits can still be documented in prose. It is verified load-bearing: aMath.min(count, 20)clamp fails it, and so doesconst CEILING = 2000.The one exception is a logical invariant, not a bound:
strictwithout a non-emptynodeIdscan never match a card, so it throws locally rather than spending a billed request. This follows thez.url()precedent from #9.Structure
Four builders in
src/request.ts, one per nested schema —buildDataSourceSettings,buildWebSourceSettings,buildOutputSettings,buildGeoLocation— composed bybuildSearchRequestBody. This is for testability, not tidiness: each of those schemas declaresadditionalProperties: false, so each builder validates against its own schema in isolation. A typo in one web field now fails a test named for that section instead of surfacing as a generic ajv error on a 40-key object.TakoSourceOptionssplits intoTakoDataSourceOptionsandTakoWebSourceOptions, becauseDataSourceSettingsandWebSourceSettingsgenuinely diverge — the previous shared type only worked because just the two common fields were exposed.Preventing recurrence
Every builder has a key-parity assertion:
Object.keys(body).sort()must equalpropertiesOf(<schema>).sort(), read from the vendored spec.Scoped honestly, after review:
propertiesOfreads a pinned snapshot, andci.ymlruns onlybuild/typecheck/test— nothing refreshes it. So these assertions fail the moment a refreshed spec disagrees with the mapping; they do not detect an option Tako adds until somebody runspnpm spec:refresh. That is a regression gate on this repo, not a live upstream detector. An earlier draft of this section claimed the gap "cannot silently reopen", which overstated it in exactly the way #9's oracle wording did. A scheduled refresh-and-diff job would close the remaining window and is deliberately not in this PR — it needs a decision on cadence and who triages the noise.Review of this branch found two schemas (
OutputSettings,Sources) where I had omitted that assertion, which would have left exactly the original bug reachable one level over. Both are now covered, and both new tests were verified to be load-bearing rather than decorative.Separately, request-side enums were ungated by the type-drift gate. Verified before the fix: a stale member on
TakoSearchEffortorTakoContentsModecompiled clean, while the same regression on any response-side enum was caught. Only the reverse (ours → official) assignment catches a stale member, and it was absent. Since this PR addsTakoWebCategory, a third such enum, leaving it ungated in the change that closes the request-surface gap would have been self-defeating. All three are now gated in both directions, and the gate was watched failing (TS2322, naming the offending type) before being reverted.Behavior notes, stated because they surprise
contentFormatdefaults differ by surface:json_compactonsources.data,csvon contents. Not normalized here — the SDK reports the API, it does not invent a third default.sources.data.modeis inert. The spec documents it as having no effect on Tako cards and says it "stays for schema stability". Exposed for parity; the JSDoc and README both say it does nothing.maxRowshas a billing consequence. The first 20 rows are free; rows above bill at the per-1,000-row rate. Called out in the README because a user raising it blind gets a surprise charge.quoteOnlyis a free pre-flight price check — it returnscostandexport_pricingwithout fetching content or charging.sources.web.countdefaults to 5 on search but 3 on answer. My first draft of the README table said 5 for both under a heading claiming the tools share a config. Caught in review; the spec states the asymmetry explicitly.What this does not do
inputSchemastays{ query }/{ url }. feat!: realign types with Tako's current API and gate drift in CI #9 already rewrote all three tool descriptions with no routing eval; adding agent-settable filters would stack a second unmeasured routing change. That belongs behind an eval./v1/graph/*.nodeIdsneeds ids from those endpoints. They are public and reachable with a plainfetch; wrapping four more endpoints is its own change.callTakostill performs no response-shape validation. Pre-existing and unchanged.Compatibility
Non-breaking. Every added field is optional,
TakoCardSourceOptionssurvives as a deprecated alias ofTakoDataSourceOptions, and no commit carries a!orBREAKING CHANGEfooter — release-please will cut 3.1.0.buildContentsRequestBody's signature changed from(url, mode)to(url, config), which is safe becausesrc/request.tsis not re-exported fromsrc/index.ts.Note this queues behind #8, the still-open 3.0.0 release PR.
Review round 1
Eight findings, all addressed. Three changed behavior:
strict/nodeIdsnow throws attakoSearch()/takoAnswer(), not insideexecute. The old placement handed the message to the model, which can neither supply graph ids nor edit the config, once per invocation.effort/country_code/localeare no longer sent unconditionally. They were the spec's own defaults restated inbuildSearchRequestBody— the same rot the no-bounds policy exists to prevent, ten lines under the comment that says so. Behavior-neutral: the spec's defaults are exactly those values.quoteOnlygets its own tool-description branch. The description branched onmodealone, so a quote-configured tool told the model to surface a link; the API nullsurland every payload field on a quote.Two documentation corrections were live defects rather than polish:
READMEstill said requesting a specific format "is not configurable yet" (this PR makes it settable), and the numeric-range note promised a 400 for every out-of-range value —maxRowsclamps instead, and bills the rows actually returned, so an over-ceiling value silently yields a short, charged export.Two claims in this body were false and are corrected above.
Test plan
maintypecheckclean; conformance project compiles cleanbuildclean;dist/contains zero references totako-sdkdependencies: {}— zero runtime dependencies preservedtests/contract/openapi.yaml, not against the testsTakoSearchEffortandTakoWebCategorysrc/request.tsorsrc/types.tsLines changed
558 insertions, 43 deletions across 10 files: 210 logic (
src/), 301 tests (tests/), 47 docs (README.md).🤖 Generated with Claude Code