Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

License: MIT TypeScript Node Webhook-driven

gemini-batch-reshape

Cost effective LLM content creation.

Give it a topic. It researches it, reshapes the research into typed JSON, and never once blocks waiting on either step:

you: "Anthropic"
  -> Bright Data researches it live (real web search, real citations)
     -> BD's webhook delivers the essay to this app
        -> this app submits it to Gemini's Batch API
           -> Gemini's webhook fires when the reshape is done
              -> structured JSON, logged

Two different providers, two different async jobs, zero polling loops. Both halves report back on their own schedule via a webhook this app owns — you fire the topic and walk away.

Why two webhooks instead of one blocking call

The straightforward version of this — call an LLM to research a topic, call another LLM to reshape the answer, done — is one function with two blocking HTTP calls in it. That's not what's interesting here. What's interesting is that BOTH calls are cheaper in their async form (Bright Data's webhook delivery vs. its synchronous scrape; Gemini's Batch API vs. generateContent) and this app is built entirely around never holding a connection open to either provider. It fires a request, returns immediately, and lets each provider's own webhook tell it when to do the next thing.

The pattern underneath

export interface ReshapeStage<T> {
  systemPrompt: string;   // what to ask
  schema: ZodType<T>;     // what shape to demand back
  model?: string;
}

A stage never calls an API directly — it hands back a prompt and validates whatever text comes back against a schema (src/reshape.ts). Both the submit side (submitReshapeBatch) and the fetch side (fetchReshapeBatchResult, called once the Gemini webhook fires) build their request from the same stage.systemPrompt and route their response through the same schema.safeParse — a response that's malformed or off-schema fails the same way regardless of which run produced it.

The two webhook handlers

Route Fires when Does
api/bd-webhook.ts Bright Data finishes researching the topic Pulls the essay out of BD's record, submits it as a Gemini batch job, returns — no polling.
api/gemini-webhook.ts Gemini's Batch API resolves (batch.succeeded / .failed / .expired) Verifies the signature, fetches the batch's result, validates it against the stage's schema, logs it.

Both are deliberately minimal: verify the request is real, do the one thing this step is for, return fast. Neither one polls anything — that's the whole point of registering a webhook instead.

Each result also carries the real costUsd Google billed for that call (checkBatch in src/gemini-batch.ts computes it from the response's own usage metadata) — for a demo whose whole pitch is being cost effective, the actual number is worth keeping, not just the theoretical per-token rate.

What the Gemini webhook's signature actually requires

src/gemini-webhook-verify.ts implements the Standard Webhooks spec Google's Gemini API uses: the signed content is {webhook-id}.{webhook-timestamp}.{rawBody}, HMAC-SHA256, secret prefixed whsec_. The header can carry multiple space-delimited v1,<base64> entries during key rotation — a match on any one is a pass. A stale timestamp (more than 5 minutes old) is rejected outright, per the spec's own replay protection.

One correction to Google's own docs, confirmed against the live API: the batch webhook event enum is batch.succeeded / batch.failed / batch.expirednot batch.cancelled, which appears in Google's docs prose but the live API rejects as an invalid subscription value. scripts/register-gemini-webhook.mjs subscribes to the three that actually exist.

Running it yourself

npm install
npm run build   # type-check
npm test
  1. Deploy this to Vercel (or anywhere that'll run api/*.ts as HTTP functions) — Google needs a real, reachable URL before it'll register a webhook against it.
  2. npm run register-webhook -- --uri https://<your-deploy>/api/gemini-webhook — one-time. Prints GEMINI_WEBHOOK_SECRET; Google shows it exactly once, so store it in your deployment's env immediately.
  3. Set GEMINI_API_KEY, BRIGHT_DATA_API_KEY, BD_WEBHOOK_SECRET, and the GEMINI_WEBHOOK_SECRET from step 2 on the deployment.
  4. npm run trigger -- --topic "some company" --url https://<your-deploy> — fires the whole cycle. Watch it land in your deployment's logs.

Know the limits

  • No cohort pooling. This submits one Gemini request per topic — simpler to read, not the most cost-efficient shape at real volume. Packing many essays into fewer, fuller batch requests is a straightforward extension of submitReshapeBatch, deliberately left out to keep the demo legible.
  • No straggler retry. A batch item that comes back malformed or schema-invalid is logged as { ok: false, error } and left there.
  • Logged, not stored. This proves the cycle works; it doesn't ship a database. A real pipeline would persist the structured result somewhere a human or another system can actually query it.
  • This can take a while — plan for minutes, not seconds. Confirmed on a real run: Bright Data's own delivery landed within a few minutes of triggering, and Gemini's batch webhook fired about 5 minutes after that (one request in the batch). Neither provider gives a wall-clock guarantee faster than that, so don't build anything — including how you watch this demo — that assumes a fast turnaround.

Confirmed against a live delivery, not assumed from docs: the Gemini webhook's payload is {"id","type","created_at","data":{"id":"batches/<id>"}}data.id arrives already fully-qualified (batches/xyz...), matching submitBatch's own name field exactly, no normalization needed.

Where this came from

The reshape-stage pattern (one prompt + one schema, validated once) is the extracted, generalized shape of a reshape stage running in production against real research essays at real volume. The webhook wiring — both BD's and Gemini's — mirrors a real production integration; the credentials and any account-specific details are not.

License

MIT.

About

Cost effective LLM content creation

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages