Skip to content

Let both SDKs search the catalog by scope with no query - #30

Merged
kev1n merged 1 commit into
mainfrom
feat/scope-only-catalog-search
Sep 5, 2026
Merged

Let both SDKs search the catalog by scope with no query#30
kev1n merged 1 commit into
mainfrom
feat/scope-only-catalog-search

Conversation

@kev1n

@kev1n kev1n commented Sep 5, 2026

Copy link
Copy Markdown
Member

What was broken

GET /catalog/search accepts any non-empty combination of q, category and platform. Measured against production today:

/catalog/search?platform=reddit    -> 200
/catalog/search?category=social    -> 200
/catalog/search                    -> 400 {"error":"name q, category, or platform"}

Both SDKs were stricter: query was typed as required and q was set unconditionally. So "every reddit API" or "everything in the social category" was expressible against the gateway and against MCP, and impossible through either SDK. Gateway PR getanyapi-com/anyapi#1013 relaxed the MCP side; the SDKs were never updated.

What changed

  • query is optional in TypeScript SearchOptions and in the Python sync and async search(). Every existing call site keeps compiling and sends the identical request.
  • q is set exactly when there is a query. An absent or empty query omits q from the query string rather than sending it empty, because an empty q is a different request.
  • A call naming none of the three never reaches the gateway to earn a 400. Both readers raise AnyAPIError with status 0, the status this repository already uses for a caller error detected before any HTTP (see the missing-API-key path).
  • The Python guard lives in the shared _account.search_request builder, so the sync and async clients have one owner for the rule.
  • SPEC.md records the rule beside the search(options) contract.
  • Both credentialless live discovery canaries now make a scope-only call against production, so CI proves the capability on the source tree, not only in a mock.

No generated code, openapi.json, catalog.json or IR was touched. The tolerant-reader rules are unchanged: the readers still reject credit keys and any non-AnyAPI provider, and derive nothing. A scope-only response is the same envelope with ranking: "keyword", which both readers already accepted.

Proof

pnpm check (dash guard, release tests, regen drift, generator, TypeScript, consumer-typecheck): EXIT=0, generator 158 tests, TypeScript 471 tests.

cd packages/python && pyright && mypy && pytest: pyright 0 errors, mypy clean on 86 source files, 498 tests passed.

New regression coverage fails without the fix (verified by reverting the builder): 3 TypeScript cases in tests/account.test.ts and 3 Python cases in tests/test_account.py covering the scope-only request, the empty-query omission, and the no-scope rejection with zero HTTP calls.

Live against api.getanyapi.com, through the built SDKs:

TypeScript

URL sent : https://api.getanyapi.com/catalog/search?platform=reddit&limit=3
ranking  : keyword | total: 11
  reddit.post                  social     from $0.0012
  reddit.post_comments         social     from $0.002
  reddit.post_transcript       social     from $0.002
URL sent : https://api.getanyapi.com/catalog/search?category=social&limit=3
ranking  : keyword | total: 167 | first: bluesky.post
no-scope : AnyAPIError status=0 search needs at least one of query, category, or platform
requests made: 2

Python

URL sent : https://api.getanyapi.com/catalog/search?platform=reddit&limit=3
ranking  : keyword | total: 11
  reddit.post                  social     from $0.0012
  reddit.post_comments         social     from $0.002
  reddit.post_transcript       social     from $0.002
URL sent : https://api.getanyapi.com/catalog/search?category=social&limit=3
ranking  : keyword | total: 167 | first: bluesky.post
no-scope : AnyAPIError status=0 search needs at least one of query, category, or platform
requests made: 2

Both live canaries pass end to end: PASS TypeScript live discovery: 363 APIs and PASS Python live discovery: 363 APIs.

Not in this PR

No tag and no publish. A release is a separate decision.

The three framework adapters (langchain-anyapi, llama-index-tools-anyapi, anyapi-haystack) each declare query: str as required in their own tool signatures and pass it straight through, so they all still block a scope-only search until they are relaxed too. That is a follow-up for after an SDK release, not a change here.

🤖 Generated with Claude Code

GET /catalog/search takes any non-empty combination of q, category and
platform. Both readers typed query as required and always set q, so
"every reddit API" or "everything in the social category" was expressible
against the gateway and against MCP and impossible through the SDKs.

query is now optional in TypeScript SearchOptions and in the Python sync
and async clients. Every existing call site keeps compiling and sends the
same request: q is set exactly when there is a query to send. An absent or
empty query omits q entirely rather than sending it empty, because an
empty q is a different request.

A call naming none of the three never reaches the gateway to earn a 400.
Both readers raise AnyAPIError with status 0, the status this repository
already uses for a caller error detected before any HTTP.

The credentialless live discovery canaries now make a scope-only call
against production in both languages, so CI proves the capability on the
source tree rather than only in a mock.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kev1n
kev1n merged commit ca4d08d into main Sep 5, 2026
4 checks passed
kev1n added a commit that referenced this pull request Sep 6, 2026
The first catalog refresh since v0.35.3 on 2026-09-01. Every regen run since then
classified blocked, so both packages have been carrying a five-day-old catalog.
v0.36.0 bumped versions alongside #30 but did not refresh the catalog, so this is
the first release that closes the gap. anyapi#940 merged three minutes after the
last green regen and started it; six more shipped changes stacked up behind it.

The classifier is right to stop: two published fields disappear. Both are
deliberate, both are already live, and each item is reviewed here against the
gateway PR that made it.

Blocked, with the slugs the machine summary omits:

- twitter.search: input.lang removed (anyapi#940). Deliberate retirement. `lang`
  was declared absent on all three cheap lanes, so sending it collapsed the SKU
  to one apify lane at 9x the price; X honours `lang:` inside the query, so the
  gateway now folds it into `query` before validation. A caller who still sends
  it keeps working against the API and reaches every lane. The generated field is
  gone, so a TypeScript caller passing it no longer compiles.
- youtube.search_hashtag: input.type enum member "shorts" removed (anyapi#961).
  Deliberate: no source we buy returns a Shorts row with the channel and publish
  time this endpoint requires, so the member could never be served.
- youtube.channel_shorts: output.shorts[].title is no longer required
  (anyapi#1023). A loosening to match what the lanes actually return.
- company_enrichment.crustdata_v3: output `name` is no longer required
  (anyapi#1044). Same shape of fix: one junk row with a null field was aborting
  the whole transform and losing the page it arrived on.
- tiktok.video_transcript_full: output confidence numeric bounds removed
  (anyapi#1023). The recognizer reports a negative log-scale score on audio it
  could not align, so 0 to 1 was never true. Bounds are doc-only in the emitters.
- yelp.search: input.limit default 20 recorded (anyapi#1003). The description
  already documented it. Its price also drops from $0.0605 to $0.0035 per
  request, from the litescrape lane.
- tiktok.video: input required fields changed (anyapi#1030). `id` joins `url`
  under anyOf, so both emit optional, matching youtube.video.

Minor rather than patch by the repo's own ladder: a new SKU (youtube.search_shorts)
and new optional fields (threads.search startDate/endDate, tiktok.video id) each
earn a minor on their own, and the two removals are why it needed a human.

Gate: pnpm check exit 0; pyright and mypy clean; 499 pytest tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant