ComponentProvider
import { ComponentProvider } from '@rudra-js/core';ComponentProvider lets you connect the generator to any model. Write an object with this shape and pass it to createComponentGenerator as provider.
export interface ComponentProvider { readonly name: string; readonly model: string; generate(request: ProviderRequest): Promise<ProviderResult>; }Reference
ComponentProvider
Properties
name: A short name for your adapter, such as'anthropic'. Core puts it on each spec your model answered, asspec.provider.model: The model you call, such as'claude-sonnet-5'. Core puts it on the spec asspec.model.generate: Core calls it each time it needs the model, with onerequest. Resolve with aresult.request: What core sends:system: The instructions for the model. They are the same on every request.user: The page view, written out for the model.schema: The zod schema the answer has to fit. Check the reply withrequest.schema.safeParsebefore you return it.signal: AnAbortSignalthat fires whenmodelTimeoutMsruns out. Pass it to your HTTP call so the call stops too.
result: What to resolve with:spec: The answer, asrequest.schemaparsed it.usageoptional: The token counts your API reported. Core passes them on as the event'susage.inputTokensoptional: Tokens in the prompt.outputTokensoptional: Tokens in the answer.cacheReadTokensoptional: Prompt tokens read from your API's own prompt cache.cacheWriteTokensoptional: Prompt tokens written to your API's own prompt cache.
Returns
generate returns a promise of the result.
Caveats
- Throw when the call fails, the model refuses, or it stops before it finishes. Don't return part of a spec. Core catches the error and gives the shopper the block core builds without a model. The event carries your error as
error, withdegradedReason: 'provider-error'. - Keep
request.userout of the errors you throw. It can hold the text a shopper typed. nameandmodelare part of the cache key. Change either and the generator starts on fresh entries.
Usage
Using a model other than Claude
Write a function that returns the provider, the way createAnthropicProvider does. Take the API key as an option, so your app decides where it comes from.
import { createComponentGenerator, type ComponentProvider } from '@rudra-js/core';
interface InHouseOptions {
apiKey: string;
fetch?: typeof fetch;
}
interface ModelReply {
stop: 'done' | 'refused' | 'length';
output: unknown;
usage: { input: number; output: number };
}
export function createInHouseProvider(options: InHouseOptions): ComponentProvider {
const send = options.fetch ?? fetch;
return {
name: 'in-house',
model: 'recs-large-2',
async generate(request) {
const response = await send('https://models.internal.example/v1/generate', {
method: 'POST',
headers: {
authorization: `Bearer ${options.apiKey}`,
'content-type': 'application/json',
},
body: JSON.stringify({ system: request.system, prompt: request.user, format: 'json' }),
signal: request.signal,
});
if (!response.ok) throw new Error(`in-house model responded ${response.status}`);
const reply = (await response.json()) as ModelReply;
if (reply.stop !== 'done') throw new Error(`in-house model stopped early: ${reply.stop}`);
const parsed = request.schema.safeParse(reply.output);
if (!parsed.success) throw new Error('in-house model sent a spec that does not fit');
return {
spec: parsed.data,
usage: { inputTokens: reply.usage.input, outputTokens: reply.usage.output },
};
},
};
}
const stubFetch: typeof fetch = async () =>
Response.json({
stop: 'done',
output: {
tone: 'neutral',
headline: 'Picked for your next trail run',
subheadline: null,
blocks: [
{
kind: 'hero',
headline: 'Built for wet trails',
body: null,
sku: 'TR-104',
ctaLabel: null,
},
],
rationale: 'The shopper is browsing trail shoes',
},
usage: { input: 1200, output: 150 },
});
export const generator = createComponentGenerator({
provider: createInHouseProvider({ apiKey: 'test-key', fetch: stubFetch }),
});stubFetch answers the way the model's API would, so the sample runs on its own. In your app, pass your own key and leave out fetch, so the provider uses the global one.
Each way the call can fail ends in a throw: a bad status, a refusal, a reply cut short, or a spec that doesn't fit the schema.