跳到正文
GitHub·· 7 天前AI 评分50

rudra-js:用 TypeScript 库让语言模型安全生成商品推荐模块

clivedsouza1010/rudra-js

AI 导读

rudra-js 是一个 MIT 许可的开源 TypeScript 库,通过 npm 安装,让语言模型只输出封闭词表内的推荐规格,由 React 渲染,模型无法编造价格、商品名或链接。默认模式下,500 名购物者基准中每 1000 次页面浏览仅需 460 次模型调用,且支持完全不用模型的确定性模式。当前版本 0.7.6,处于早期阶段,需 React 18/19 和 Node 22.12 以上。

正文

rudra-js is an open-source TypeScript library (MIT) you install from npm. A language model designs one product-recommendation block for a page in your shop, rudra-js checks it against your catalog, and React renders it.

It is not a hosted platform, a visual or low-code app builder, or a workspace you sign up for.

Docs · npm · Demo shop

Every export is documented at rudrajs.com, with signatures and field tables generated from the published packages.

If a language model writes raw HTML, it can hallucinate a fake price or a product you don't sell onto your page. So rudra-js never lets the model write HTML. Instead it returns a specification drawn from a closed vocabulary: it picks a layout, writes a headline, and provides the copy. There are simply no fields for it to invent a price, a product name, an image, or a link. Products are referenced only by SKU, and only from the list you provide. In the default mode, the products shown in a grid or carousel are chosen per request from that list, not by the model.

The example shop's product page. The heading, the order, the highlighting and the paragraph are the model's. Every title, price and image is the shop's.

@rudra-js/react turns that specification into markup, reading the title, price, image, and link from your catalog as the page is served. Whatever the model wrote is rendered as escaped text.

We also read every word the model writes, in three passes. Patterns for prices, discounts, ratings, delivery dates and stock counts. A check that every digit in the sentence is one your own payload supplied — a category or a tag on a candidate we showed the model. And a list of claims that carry no number to check, like "top pick" and "customer favourite". Any field that fails a pass is dropped.

Two of the three are word lists, so a rewording that sits on neither gets through, and the digit check reads digits, so a number spelled out in words isn't a number to it. "Four and a half stars from other hikers" walks past all three. The tests hold a list of rewordings that still pass, so the gap has a size you can go and read. That's why the structural boundaries are the real defence: the model is never told a price, and the specification has nowhere to put one.

Because the block is in the initial HTML response and needs zero client-side JavaScript, a crawler that never runs JavaScript still reads it. It's efficient too: on our 500-shopper benchmark the default mode makes 460 model calls per 1,000 page views, compared to 1,000 if you generated for every shopper. And if you want to run with no model at all, that's a fully supported setting rather than a stub, and it won't cost you a penny.

Status: 0.7.6, early. It installs and it works. The Getting started below actually runs as a test on every commit. Just keep in mind that the public contracts might still shift between minor versions before we hit 1.0, and the changelog will always tell you when they do.

Getting started

You don't need an API key to get going. Leave the provider out and you'll get the deterministic component, which is a fully supported setting and the right one until you've settled on a model.

npm install @rudra-js/core @rudra-js/react @rudra-js/attested

A quick heads-up: every package here lives under the @rudra-js scope. The unscoped rudra-js package on npm belongs to someone else, so make sure you include the @.

You don't need zod yourself. @rudra-js/core brings zod 4 as a dependency, and an app already on zod 4 shares that copy. On zod 3, npm puts a zod 4 under core, and another under @rudra-js/anthropic if you use it. Your zod 3 can't mix with the schemas core exports: productSchema.array() works, your z.array(productSchema) won't.

You'll need React 18 or 19. You don't need Server Components: @rudra-js/react is plain React with no hooks, so it works with renderToString, in a client-rendered app, and as a Server Component where your framework supports them. Rendering it on the server is what puts the block in the first HTML response.

You'll also need Node 22.12 or later. Node 20 is end of life, so we don't build or test on it.

import { createComponentGenerator, parseTrackingInput } from '@rudra-js/core';
import { RudraComponent } from '@rudra-js/react';

const catalog = [
  { sku: 'A-1', title: 'Cast iron skillet', category: 'Cookware', price: 39, currency: 'USD' },
  { sku: 'A-2', title: 'Enamel dutch oven', category: 'Cookware', price: 89, currency: 'USD' },
  { sku: 'A-3', title: 'Chef knife', category: 'Knives', price: 55, currency: 'USD' },
];

async function recommendations() {
  // Passing no provider means no API key and no spend.
  // You get a reliable, deterministic component.
  const generator = createComponentGenerator({ provider: null });

  // Parsing fills in what you left out and strips anything that doesn't belong.
  // Always pass the parsed candidates to the renderer, not your raw objects.
  const input = parseTrackingInput({
    user: { id: 'shopper-1' },
    context: { surface: 'pdp', currentSku: 'A-1', currentCategory: 'Cookware' },
    candidates: catalog,
    signals: { cart: [{ sku: 'A-2', at: Date.now() }] },
  });

  const spec = await generator.generate(input);

  return <RudraComponent spec={spec} products={input.candidates} locale="en-US" />;
}

In Next.js, adding export default recommendations at the bottom turns that into a page. A shopper looking at the skillet with the dutch oven already in their cart is shown the knife, under the heading "Goes with your cart". The page they're on and the thing they've already chosen are both left out. No model was asked, and nothing was billed.

When you're ready to bring a model in, add @rudra-js/anthropic and pass it as the provider. Everything above stays the same, except that the model now writes the wording, the layout and the emphasis. By default the products in a grid or carousel are still chosen by the core logic rather than by the model.

Before you send real shopper traffic, read Running it in a regulated shop.

That adapter is one option, not the only one. Any model can sit behind the small interface described in Any provider.

We run the code block above as a test on every commit, so if a change breaks it, CI catches it straight away.

Packages

Package What it does
@rudra-js/core The contracts and logic that turn one tracking payload into one renderable component specification, screened with @rudra-js/attested
@rudra-js/react Renders that specification with React, on the server or in the browser
@rudra-js/anthropic Talks to the Anthropic API, and is the only package that makes a billed call
@rudra-js/attested Checks model-written copy against the facts a shop stands behind, with no dependencies

Development

You'll need Node >=22.12 to work on this. Node 20 is end of life and doesn't have the --experimental-strip-types flag that npm run verify:consumer relies on. .nvmrc names an exact Node 22 so the version is never a guess, and engine-strict=true turns a mismatch into a readable install error. CI runs the checks on 22.12.0 as well as on the latest Node 22, so the floor we claim is genuinely exercised.

nvm use
npm install

npm run check        # all six of the below, in order

npm run build        # tsc -b across the workspace
npm run typecheck    # includes test files, which the build does not
npm run lint
npm run format:check
npm test
npm run verify:consumer   # packs all four packages and uses them from outside the repo

To see what each generation mode costs under a stub model, run ANTHROPIC_API_KEY= RUDRA_REPLAY_ONLY=1 npm run bench. bench/README.md explains what the columns mean.

If you keep a key exported in your shell, run the tests as ANTHROPIC_API_KEY= npm test. vitest.config.ts sets RUDRA_REPLAY_ONLY=1 for every test run, and the shop deliberately throws at start-up when that flag and a key are both present, so a run with a key in the shell fails to load rather than quietly spending money.

We run all six checks on every pull request as separate steps, so a failure names itself, and a second job builds the example shop and checks its page still reads as one to a crawler. CI sets no mode, so the shop replays and that job runs the build plainly:

npm run build --workspace @rudra-js/example-shop
npm run verify:crawlable

Locally that build is safe on its own: building calls no model, and serving the shop only spends with RUDRA_SHOP_MODE=record. The prefix below just makes it a rule rather than a default:

ANTHROPIC_API_KEY= RUDRA_REPLAY_ONLY=1 npm run build --workspace @rudra-js/example-shop

Example

examples/shop is a small Next.js storefront that puts the architecture through a real page: a product page asks a language model for a recommendation component and server-renders the result into the same HTML response, rather than fetching it after the page loads.

npm run dev --workspace @rudra-js/example-shop
# then visit http://localhost:3000/product/RJ-00001?shopper=S-0001

That replays the transcripts committed under examples/shop/recordings/, so it bills nothing whatever keys you have in your environment. RUDRA_SHOP_MODE is the switch, and record is the one value that spends money:

RUDRA_SHOP_MODE=record npm run dev --workspace @rudra-js/example-shop

That calls Claude once for each page that has no transcript yet and saves the answer as a new one. Pages that already have one are still replayed, so browsing costs nothing after the first time.

examples/shop/README.md covers the rest: the environment variables, how to re-record a transcript after a prompt change, and why the files under recordings/ must only ever hold demo shoppers and demo catalogs.

Getting help

  • The API reference — every export, at rudrajs.com.
  • Questions and ideas — come and talk in Discussions.
  • Bugs — open an issue.
  • Vulnerabilities — please report them privately, see SECURITY.md.

Contributing

Contributions are welcome. CONTRIBUTING.md covers setup, the scope the project holds to, and its naming and testing conventions. Everyone taking part is expected to follow the code of conduct.

Changes are recorded in the changelog.

Patent

Patent pending in the US. A provisional application for the cohort caching design — the coupled cache key and prompt projection in spec-cache.ts — was filed on 20 September 2026, application 64/158,649.

This doesn't change how you can use rudra-js. The code is MIT and stays MIT. The application was filed defensively, so that nobody else patents the design and comes after the people using it. A provisional isn't examined and may never become a granted patent.

Licence

MIT © Clive Dsouza

来源:GitHub · github.com