@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-laya

npm · 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

Runtime Dependencies

  • @molecule/api-ai-decisions

  • @molecule/api-secrets

  • You run the model. LAYA_URL (default http://localhost:8000) points at a laya-serve host; set LAYA_API_KEY on 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/systemone server works. LAYA_URL can 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 or createProvider({ 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 per score, 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, not bond('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.