Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ jobs:
registry-url: "https://registry.npmjs.org"
cache: pnpm
- run: pnpm install --frozen-lockfile
# Same gate ci.yml runs on a PR. The release commit reaches main through
# release-please rather than through CI, so without these the publish is
# the one step that ships unverified.
- run: pnpm build
- run: pnpm typecheck
- run: pnpm test
- run: pnpm publish --no-git-checks --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ dist
.env
*.log
.DS_Store

# Local codebase-analysis output. Not part of the package; keep it out of the
# public repo so a stray `git add -A` can't publish it.
.understand-anything/
20 changes: 20 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,28 @@
```bash
pnpm install
pnpm test # vitest (mocked fetch — no live API calls)
pnpm test:contract # just tests/contract — the API contract suite
pnpm typecheck # tsc over src + tests + examples
pnpm build # tsup → dist/
pnpm spec:refresh # re-vendor tests/contract/openapi.yaml from docs.tako.com
```

`pnpm test` includes `tests/contract/types.conformance.test.ts`, which shells out
to a cold `tsc` run, so expect it to take a second or two — much longer than the
rest of the suite. It is the slowest test and the one that fails if `src/types.ts`
drifts from the API.

## Keeping the API contract honest

`tests/contract/` checks this SDK's types against two pinned references: the
vendored `openapi.yaml` and the `tako-sdk` version in the lockfile. Both are
snapshots, so **the suite catches a regression in this repo, not a change Tako
ships.** To check for upstream drift, refresh them and re-run:

```bash
pnpm spec:refresh
pnpm update tako-sdk --latest
pnpm test
```

Examples make live calls; run them manually with keys set in `.env` (see `.env.example`):
Expand Down
103 changes: 103 additions & 0 deletions MIGRATING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Migrating

## 2.x → 3.0

3.0 realigns this SDK's types with the current Tako API. Every change below is a case where 2.x described something the API no longer does — so if code depended on it, it was already broken at runtime, whatever TypeScript said.

`tests/contract/` validates these types against Tako's published OpenAPI document and against [`tako-sdk`](https://www.npmjs.com/package/tako-sdk), Tako's official generated client, so a type that stops matching either one fails CI. Both are pinned snapshots, refreshed deliberately rather than continuously.

### Config

| 2.x | 3.0 |
| --- | --- |
| `sources.data.deferDataRetrieval` | **Removed.** No replacement. |

The API removed `defer_data_retrieval` from its data-source settings, and those settings forbid unknown properties — so any request that set this option was **rejected outright**, not silently ignored. Delete the option; the request starts working.

### Responses

| 2.x | 3.0 |
| --- | --- |
| `result.contents_total_cost: number` | **No replacement.** Read per-item `content.cost` / `content.export_pricing` (see below) |
| `content.format` | `content.content_format` |
| `TakoContentFormat = 'csv' \| 'text'` | `'csv' \| 'json_records' \| 'json_compact'` |

**Cost.** `contents_total_cost` no longer exists — confirmed absent from every live response. The spec defines `usage` as its successor, but **the API does not currently populate `usage` either**, so there is no drop-in replacement for an aggregate request cost:

```ts
// 2.x
const cost = result.contents_total_cost; // undefined at runtime

// 3.0 — typed, but currently always undefined in practice
const cost = result.usage?.total_cost_usd;

// 3.0 — where pricing actually lives today: per item
for (const card of result.cards) {
card.content?.cost; // USD, e.g. 0.001
card.content?.export_pricing; // rate card for a full /contents export
}
```

Verified 2026-08 across plain, `deep` and `includeContents` search, answer, and both contents modes: `usage` was absent from every response. It is typed `usage?: TakoUsage | null` so it will light up if Tako starts emitting it, but do not build cost tracking on it yet. Sum the per-item `cost` fields instead.

**Content format.** The field was renamed *and* its values changed. `'text'` is gone: web page text is signalled by the absence of a format. The field is optional as well as nullable, so it may arrive as `null` **or** be missing entirely — test it loosely with `== null`, never `=== null`.

```ts
// 2.x
if (item.format === 'csv') parseCsv(item.data);
else if (item.format === 'text') readProse(item.data); // both branches dead

// 3.0
if (item.content_format == null) readProse(item.data); // web page text
else parseCsv(item.data); // card data
```

Three payload fields were also missing and are now typed: `records` (for `json_records`), `dataset` (for `json_compact`), plus `export_pricing` and `manifest`.

### Source taxonomy

| 2.x | 3.0 |
| --- | --- |
| `TakoCardSourceIndex = 'tako' \| 'web' \| 'connected_data' \| 'tako_deep_v2'` | `TakoSourceIndex = 'data' \| 'web'` |
| `TakoCardSourceIndexSegment` | **Removed** — never existed in the API |
| `TakoCardSourcePrivateIndex` | **Removed** — never existed in the API |
| `TakoKnowledgeCardSource` | `TakoCardSource` (old name kept as a deprecated alias) |

The curated Tako source is `'data'`, not `'tako'`. This one fails silently, so it's worth grepping for:

```ts
if (src.source_index === 'tako') // never matches — compiles fine, never runs
if (src.source_index === 'data') // correct
```

`source_index` is now the required two-member union `'data' | 'web'`. 2.x modelled it as a union that could also be an object (`{ index_type, segment_id }`), which the API has never sent on this surface — any code narrowing on that shape can be deleted.

### Guaranteed collections

The API guarantees only `request_id` (plus `answer` on the answer surface); the collections are not in its `required` list, so a valid response may omit them. In practice the API currently does send `cards: []` on a web-only search, so 2.x's always-present typing was a latent hazard rather than an active crash — but the contract permits omission, and `tako-sdk` decodes an absent collection to `undefined`.

3.0 keeps them non-optional **and makes it true**: the tools normalize absent collections to `[]` before returning. No caller changes needed, and no `?.` required.

If you want the unnormalized wire shape, import `TakoSearchResponse`, `TakoAnswerResponse` or `TakoContentsResponse`.

### New card fields

`TakoCard` gained five fields the API was already sending:

- **`exportable`** — whether `takoContents` can download this card's data. `false` means the call returns 403, so skip it. `true` is eligibility, not a guarantee.
- **`data_freshness`** — `{ data_as_of, last_updated }`.
- **`relevance_score`** — 1.0–5.0, populated for entitled accounts.
- **`nodes`** — the graph entities and metrics behind the card.
- **`metric_definitions`** — definitions of the metrics displayed.

`exportable` is the practical one; filtering on it avoids calls that cannot succeed:

```ts
const downloadable = result.cards.filter((c) => c.exportable);
```

### Also corrected

`TakoKnowledgeCardMethodology.methodology_name` and `.methodology_description` are required keys with nullable values (`string | null`), not optional — matching the spec.

Documentation fixes: 2.x claimed inline contents were "capped at 1000 rows". The real behaviour is a 20-row default against a 2,000-row ceiling. Raising it needs `max_rows`, which this SDK does not expose yet.
44 changes: 40 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ npm install @takoviz/ai-sdk ai

## Setup

Get an API key from the [Tako developer console](https://developer.tako.com/console/api-keys) and set it as an environment variable:
Get an API key from the [Tako developer console](https://tako.com/console/api-keys) and set it as an environment variable:

```bash
export TAKO_API_KEY=your_api_key_here
Expand Down Expand Up @@ -63,7 +63,7 @@ takoSearch({
baseUrl: 'https://tako.com', // optional; override for staging
effort: 'fast', // 'fast' (default) | 'instant' | 'deep'
sources: { // a source is searched iff its key is present; omit to search both
data: { count: 5, includeContents: false, deferDataRetrieval: false }, // legacy alias: tako
data: { count: 5, includeContents: false }, // legacy alias: tako
web: { count: 5, includeContents: false },
},
countryCode: 'US', // default 'US'
Expand Down Expand Up @@ -96,12 +96,40 @@ The LLM supplies only the dynamic input: `{ query }` for `takoSearch`/`takoAnswe
{
cards: TakoCard[]; // Tako knowledge cards (title, description, image_url, webpage_url, sources, ...)
web_results: TakoWebResult[];
contents_total_cost: number;
request_id: string;
usage?: TakoUsage | null; // { total_cost_usd, compute?, data? } — see note
}
```

`takoAnswer` additionally includes `answer: string` (with `cards[0]` as the lead card). `takoContents` resolves to `{ contents: TakoContentItem[]; request_id: string }`, where each item has a `format` (`'csv'` | `'text'`), a `cost`, and either a presigned `url`/`expires_at` (url mode) or inline `data`/`total_rows`/`truncated` (inline mode).
> **Cost reporting.** `usage` is what the API spec defines for per-request cost, but as of 2026-08 it is not populated on any endpoint. For pricing today, read the per-item `content.cost` and `content.export_pricing` on each card, which are populated.

`takoAnswer` additionally includes `answer: string` (with `cards[0]` as the lead card). `takoContents` resolves to `{ contents: TakoContentItem[]; request_id: string; usage? }`.

The API guarantees only `request_id` — the contract permits omitting the collections — so the tools normalize: `cards`, `web_results` and `contents` are **always arrays**. No `?.` needed.

### Reading a card

Two fields are worth knowing about:

- **`exportable`** — whether `takoContents` can download that card's data. `false` means don't bother; the call returns 403. `true` is eligibility, not a guarantee, so still handle errors.
- **`data_freshness`** — `{ data_as_of, last_updated }`, so you can tell how current a number is.

### Reading a contents item

Each item carries a `cost` (USD) and either a presigned `url` + `expires_at` (url mode) or an inline payload (inline mode). `content_format` tells you what you got:

| `content_format` | Payload field | Meaning |
| --- | --- | --- |
| `null` *or absent* | `data` | A web page's extracted text |
| `'csv'` | `data` | Card data as CSV |
| `'json_records'` | `records` | Card data as row objects |
| `'json_compact'` | `dataset` | Card data as typed columns + positional rows |

`total_rows` and `truncated` tell you whether the card held more rows than were returned.

Which format you get depends on the surface: `takoContents` returns `'csv'` for cards and no format for web pages, while a card inlined by `sources.data.includeContents` arrives as `'json_compact'` (a `dataset`). Requesting a specific format is not configurable yet.

`content_format` is optional as well as nullable, so branch on it loosely — `content_format == null` means web text; `=== null` misses the absent case.

Full type definitions ship with the package.

Expand All @@ -115,17 +143,25 @@ import type {
TakoAnswerResult,
TakoContentsResult,
TakoCard,
TakoCardSource,
TakoWebResult,
TakoContentItem,
TakoDataset,
TakoUsage,
} from '@takoviz/ai-sdk';
```

The types mirror Tako's published OpenAPI document. `tests/contract/` validates them against a vendored copy of that spec and against [`tako-sdk`](https://www.npmjs.com/package/tako-sdk), Tako's official generated client, so a type that stops matching the API fails CI. Both references are pinned snapshots, refreshed by `pnpm spec:refresh` and a `tako-sdk` bump.

If you need the raw wire shapes (where collections are optional, before the tools normalize them), import `TakoSearchResponse`, `TakoAnswerResponse` or `TakoContentsResponse`.

## License

MIT

## Links

- [Migrating from 2.x](./MIGRATING.md)
- [Tako documentation](https://docs.tako.com)
- [Vercel AI SDK](https://sdk.vercel.ai/docs)
- [GitHub repository](https://github.com/TakoData/ai-sdk)
47 changes: 36 additions & 11 deletions examples/contents.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { takoSearch, takoContents } from "../src/index";
import type { TakoContentsResult, TakoSearchResult } from "../src/index";

// Run: pnpm exec tsx --env-file=.env examples/contents.ts
// Cast: calling a tool's execute() directly (outside generateText) needs a
Expand All @@ -10,26 +11,50 @@ async function main() {
const result = (await search.execute!(
{ query: "Nvidia full-time employees since 2013" },
opts,
)) as any;
)) as TakoSearchResult;

// Candidate URLs to drill into: card data first, then web results. Some Tako
// cards come from protected sources whose data can't be exported (the API
// returns 403) — so try each until one resolves, the way an agent would.
// `cards` and `web_results` are always arrays — the tools normalize the
// API's optional collections, so no guard is needed here.
console.log(`${result.cards.length} cards, ${result.web_results.length} web results`);

// Only exportable cards can be downloaded; a non-exportable card returns 403,
// so filtering on the flag avoids a call that cannot succeed. Web results are
// always downloadable as text.
const candidates: string[] = [
...(result.cards ?? []).map((c: any) => c.webpage_url),
...(result.web_results ?? []).map((w: any) => w.url),
].filter(Boolean);
...result.cards.filter((c) => c.exportable).map((c) => c.webpage_url),
...result.web_results.map((w) => w.url),
].filter((url): url is string => Boolean(url));

const skipped = result.cards.filter((c) => !c.exportable).length;
if (skipped) console.log(`Skipped ${skipped} card(s) whose data is not exportable.`);

const contents = takoContents({ mode: "inline" });
for (const url of candidates) {
try {
const downloaded = (await contents.execute!({ url }, opts)) as any;
const item = downloaded.contents?.[0];
const downloaded = (await contents.execute!({ url }, opts)) as TakoContentsResult;
const item = downloaded.contents[0];
if (!item) continue;

console.log("Source:", url);
console.log("Format:", item?.format, "| cost:", item?.cost);
console.log("Data (first 500 chars):\n", item?.data?.slice(0, 500));
// content_format names the serialization for a card's tabular data. For web
// page text it is null — or absent entirely, since the field is optional —
// so compare loosely rather than with `=== null`.
console.log(
item.content_format == null
? "Web page text"
: `Card data (${item.content_format})`,
"| cost:",
item.cost,
);
if (item.total_rows != null) {
console.log(`Rows: ${item.total_rows}${item.truncated ? " (truncated)" : ""}`);
}
console.log("Data (first 500 chars):\n", item.data?.slice(0, 500));
// Per-item cost is what the API populates; the aggregate `usage` is not.
console.log("Item cost (USD):", item.cost ?? 0);
return;
} catch (err) {
// `exportable: true` is eligibility, not a guarantee — still fall back.
console.log(`Skip ${url}: ${(err as Error).message}`);
}
}
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"typecheck": "tsc --noEmit -p tsconfig.check.json",
"test": "vitest run",
"test:watch": "vitest",
"test:contract": "vitest run tests/contract",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding: CONTRIBUTING.md was not updated for the new scripts — it still lists only pnpm test / typecheck / build, and doesn't mention that pnpm test now includes a cold tsc run via the conformance test (the slowest test in the suite).

Evidence: CONTRIBUTING.md:6-9 at this commit lists the three original commands; test:contract and spec:refresh appear nowhere in it.

Proposed fix: Add the two scripts to CONTRIBUTING.md with one line each, plus a sentence noting the conformance test shells out to tsc so a slow first run is expected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added both scripts to CONTRIBUTING.md, plus the note that pnpm test shells out to a cold tsc so a slow first run is expected.

Also added a short section on refreshing the two pinned references (pnpm spec:refresh + a tako-sdk bump), which ties into your package.json:24 comment about the oracles being snapshots.

"spec:refresh": "curl -fsSL https://docs.tako.com/api-reference/openapi.yaml -o tests/contract/openapi.yaml",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What refreshes the two oracles? openapi.yaml is vendored and only moves when someone runs this script by hand, and tako-sdk is pinned to exactly 1.1.10 in pnpm-lock.yaml with CI running --frozen-lockfile. I don't see a dependabot/renovate config or a scheduled workflow.

That makes the suite a regression test against the 2026-08-03 snapshot rather than a drift detector — it can only fail after a human already suspected drift and refreshed. The claim in the PR body, MIGRATING.md:7 and the README ("API drift fails CI rather than shipping") reads as continuous. Worth either a scheduled job that runs spec:refresh + bumps tako-sdk and opens a PR, or softening the wording to what it actually guarantees?

Separately: release-please.yml's publish job runs install/build/publish with no test or typecheck, so the gate is absent at the moment a version actually ships. Is that intentional given ci.yml covers the PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right on both counts, and I did the honest half now while leaving two decisions open.

Wording — fixed. "API drift fails CI rather than shipping" overclaimed. README, MIGRATING.md and CONTRIBUTING.md now say a type that stops matching either reference fails CI, and that both are pinned snapshots refreshed deliberately. CONTRIBUTING gained a short section on how to refresh them. So the suite is accurately described as a regression gate on this repo, not a continuous upstream detector.

Scheduled refresh — not done, deferred to the repo owner. A weekly job running spec:refresh + pnpm update tako-sdk --latest and opening a PR on any diff is the right shape and would make the original claim true, but adding a scheduled workflow is beyond a wire-correctness PR and wants a decision on cadence and who triages the noise.

release-please publish gate — also deferred, and I think you found a real hole. release-please.yml's publish job runs install/build/publish with no test or typecheck, so nothing gates the moment a version actually ships. ci.yml covers the PR, but the release commit lands on main and publishes without re-verification. Adding the two steps is cheap insurance; I have deliberately not touched the publish pipeline unilaterally.

Both flagged to the repo owner rather than folded in here.

"prepublishOnly": "pnpm build"
},
"keywords": [
Expand Down Expand Up @@ -50,9 +52,13 @@
"@ai-sdk/openai": "^4.0.0",
"@types/node": "^24.10.1",
"ai": "^7.0.0",
"ajv": "^8.20.0",
"ajv-formats": "^3.0.1",
"tako-sdk": "^1.1.10",
"tsup": "^8.5.0",
"tsx": "^4.20.6",
"typescript": "^5.9.3",
"vitest": "^3.0.0"
"vitest": "^3.0.0",
"yaml": "^2.9.0"
}
}
Loading
Loading