@molecule/api-ai-decisions-laya
Provider bond · ai-decisions · API (Node) · v1.0.0 · Apache-2.0
Laya decisions provider for molecule.dev — typed choice/score/yes-no answers from the open-weights Laya model on your own laya-serve host
npm install @molecule/api-ai-decisions-layanpm · Source on GitHub · Implements @molecule/api-ai-decisions
How it works
@molecule/api-ai-decisions-laya is a provider bond on the API (Node) side: it implements the ai-decisions core interface (@molecule/api-ai-decisions) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
# Run the model server (CPU works; a GPU is ~10x faster)
pip install "laya[serve]"
LAYA_API_KEY=change-me laya-serve # http://0.0.0.0:8000
# or: docker compose up (compose.yaml in github.com/NandhaKishorM/laya)Works with: @molecule/api-ai-decisions, @molecule/api-secrets
Secrets: LAYA_URL (optional), LAYA_API_KEY (optional)
Reference
Laya decisions provider for molecule.dev — typed decisions from the
open-weights Laya model (Apache-2.0) on a laya-serve host you run.
Laya is a ~421M-parameter encoder (ModernBERT-large; a 322M multilingual
checkpoint covers 100+ languages) that answers choice, score and yes/no
questions in one forward pass — about 33–40 ms per question on a T4 GPU,
with no text generation. This bond speaks laya-serve's /v1/systemone
route, the same wire protocol as TypeSafe's Jev, so swapping to
@molecule/api-ai-decisions-jev is a one-line change.
Quick Start
# Run the model server (CPU works; a GPU is ~10x faster)
pip install "laya[serve]"
LAYA_API_KEY=change-me laya-serve # http://0.0.0.0:8000
# or: docker compose up (compose.yaml in github.com/NandhaKishorM/laya)
import { setProvider, requireProvider } from '@molecule/api-ai-decisions'
import { provider } from '@molecule/api-ai-decisions-laya'
setProvider(provider) // reads LAYA_URL + LAYA_API_KEY on first use
const { answers } = await requireProvider().decide({
state: 'My card was charged twice, please refund one of them',
questions: {
queue: {
type: 'choice',
instructions: 'Which team?',
criteria: { billing: 'charges, refunds', tech: 'bugs, login' },
},
refund: { type: 'yesNo', instructions: 'The customer wants a refund.' },
},
})
answers.queue.choice // 'billing'
answers.refund.probability // 0.96
Type
provider
Installation
npm install @molecule/api-ai-decisions-laya @molecule/api-ai-decisions @molecule/api-secrets
API
Interfaces
LayaConfig
Configuration for the Laya decisions provider.
interface LayaConfig {
/** Base URL of the `laya-serve` host. Defaults to `LAYA_URL`, then `http://localhost:8000`. */
baseUrl?: string
/** Bearer token, when the server sets `LAYA_API_KEY`. Defaults to the `LAYA_API_KEY` env var. */
apiKey?: string
/**
* Default checkpoint: `'english'`, `'multilingual'` or `'typed-decisions'`
* (or a Hugging Face id such as `'convaiinnovations/laya-multilingual'`).
* Omit to let the server route by the input's language.
*/
model?: string
}
PostOptions
Options for postSystemOne.
interface PostOptions {
/** Full endpoint URL. */
url: string
/** Bearer token, if any. */
apiKey?: string
/** Request body. */
body: Record<string, unknown>
/** Abort signal. */
signal?: AbortSignal
/** Label for error messages (`'Laya'`, `'Jev'`). */
label: string
/** Max retries on a retryable status (default 3). */
maxRetries?: number
}
WireAnswer
One answer as received on the wire (only the fields we read).
interface WireAnswer {
type?: string
choice?: string
score?: number
noul?: number
probabilities?: Record<string, number>
}
WireQuestion
One question as sent on the wire.
interface WireQuestion {
type: 'choice' | 'score' | 'noul'
instructions: string
criteria?: Record<string, string> | string[]
}
WireResponse
The response body (only the fields we read).
interface WireResponse {
model?: string
answers?: Record<string, WireAnswer>
usage?: { input_tokens?: number; output_tokens?: number }
}
Functions
createProvider(config)
Creates a Laya decisions provider.
function createProvider(config?: LayaConfig): AIDecisionsProvider
config— Base URL, API key and default checkpoint.
Returns: An AIDecisionsProvider backed by a laya-serve host.
fromWireAnswer(id, question, wire, minConfidence)
Converts one wire answer into the core's answer for question.
function fromWireAnswer(
id: string,
question: DecisionQuestion,
wire: WireAnswer | undefined,
minConfidence?: number,
): DecisionAnswer
id— The question id (for error messages).question— The question that was asked.wire— The wire answer.minConfidence— Optional low-confidence threshold.
Returns: The typed answer.
fromWireResponse(questions, body, minConfidence)
Converts a whole wire response into the core's result.
function fromWireResponse(questions: Q, body: WireResponse, minConfidence?: number): DecideResult<Q>
questions— The questions that were asked.body— The parsed response body.minConfidence— Optional low-confidence threshold.
Returns: The typed result.
postSystemOne(opts)
POSTs a /v1/systemone request with retry on 429/5xx-busy, honouring
Retry-After. Throws an Error carrying status on a non-2xx response.
function postSystemOne(opts: PostOptions): Promise<WireResponse>
opts— Request options.
Returns: The parsed response body.
toWireQuestions(questions)
Converts the core's questions into wire questions (yesNo → noul).
function toWireQuestions(questions: Record<string, DecisionQuestion>): Record<string, WireQuestion>
questions— The questions keyed by id.
Returns: The wire questions object.
Constants
aiDecisionsLayaSecretDefinitions
Secret definitions used by the Laya decisions bond.
const aiDecisionsLayaSecretDefinitions: SecretDefinition[]
DEFAULT_LAYA_URL
laya-serve's default bind (LAYA_PORT defaults to 8000).
const DEFAULT_LAYA_URL: 'http://localhost:8000'
provider
The provider implementation — lazy, so env vars are read on first use.
const provider: AIDecisionsProvider
Core Interface
Implements @molecule/api-ai-decisions interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/api-ai-decisions'
import { provider } from '@molecule/api-ai-decisions-laya'
export function setupAiDecisionsLaya(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-ai-decisions^1.0.0@molecule/api-secrets^1.0.1
Environment Variables
LAYA_URL(optional) — Laya server URL- Setup: The base URL of your laya-serve host (pip install "laya[serve]" && laya-serve, or the Docker image). Defaults to http://localhost:8000.
- Get it here: https://github.com/NandhaKishorM/laya/blob/main/docs/http-api.md
- Example:
http://localhost:8000
LAYA_API_KEY(optional) — Laya server API key- Setup: The bearer token your laya-serve host requires, if you set LAYA_API_KEY on the server.
- Get it here: https://github.com/NandhaKishorM/laya/blob/main/docs/http-api.md
- Example:
a-long-random-string
Runtime Dependencies
-
@molecule/api-ai-decisions -
@molecule/api-secrets -
You run the model.
LAYA_URL(defaulthttp://localhost:8000) points at alaya-servehost; setLAYA_API_KEYon BOTH sides to require a bearer token — without it the server is open to anyone who can reach it, so never expose an unauthenticated one publicly. -
Any
/v1/systemoneserver works.LAYA_URLcan point at another self-hosted server of the same protocol — e.g. Kev (github.com/jaredpalmer/kev, Apache-2.0 LoRA adapters on Qwen, 0.8B–27B, needs a GPU or Apple MLX) — with no code change. -
Checkpoints: pass
model: 'english' | 'multilingual' | 'typed-decisions'(per call orcreateProvider({ model })); omit it and the server routes by the input's script/language. Any other value (e.g. a Jev id) is ignored by the server, not rejected. -
Server limits (each a 413): 64 questions/request, 100 options per
choice, 32 levels perscore, 512 options total, 50,000-char state, 2 MiB body. Option texts must also fit a ~192-token window (else 422) — keep descriptions short. Busy servers answer 503 +Retry-After; this bond retries 429/503/529 up to 3 times. -
Accuracy is yours to measure. Base checkpoints are near chance on unfamiliar decision sets and ship over-confident; fine-tune (the repo has a free Kaggle notebook) and calibrate on your own labelled data, and gate on
minConfidence. See the core's remarks. -
Use the core's
setProvider, notbond('ai-decisions', …)directly.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- Each flow that makes a decision (routing, triage, moderation, a guardrail) runs it from the real UI, and the answer DRIVES what happens next (the item lands in the chosen queue, the badge shows, the action is blocked) — not just printed.
- Both directions: a clearly-billing input routes to billing AND a clearly-technical one routes elsewhere. One label for every input is a broken integration.
- A low-confidence answer takes the app's fallback path (human review, "unsure" state) instead of being acted on.
- Provider errors (service down, bad key) show a visible, recoverable state — never a blank screen or an unhandled rejection.
- The call runs server-side: no provider request or key in the browser's Network tab.