npm.io
4.9.16 • Published 13h agoCLI

@x12i/funcx

Licence
MIT
Version
4.9.16
Deps
17
Size
77.4 MB
Vulns
0
Weekly
976

FuncX (@x12i/funcx)

Turn prompts into real functions — typed I/O, JSON-safe execution, evaluation, and instruction optimization.

  • Use it as an npm library (you're in full control; no proxy required).
  • Or run the included stateless REST server to expose functions over HTTP and manage the full function lifecycle.

Terminology

  • Function: canonical public term for an ability in this library.
  • Catalox: runtime content store — catalog id ai-functions (app funcx). Each function is one native row keyed by indexed.functionId, with structured fields in data (title, description, instructionsText, rules, io, testCases, meta). CRUD goes through FuncX, not Catalox directly — see .docs/AI-FUNCTIONS-CATALOG-CATALOX.md. Model/provider metadata comes from @x12i/ai-profiles, not Catalox (see .docs/CATALOX-MODEL-CATALOG-VERIFICATION.md).

FuncX v4 breaking changes

v3 v4
executeSkill executeFuncx
runWithContent executeFuncxFromContent
getSkillNames() getFuncxNames()
getSkillsResolver() getFuncxResolver()
runFunctionWithCatalox runFuncxWithCatalox
Catalox data.instructions duplicate data.body only
Git content:migrate:catalox content:primitives:sync from content-seed/functions/*.seed.json
/skills/* HTTP 410 — use /functions/* and POST /run

Workflow: edit seeds → npm run content:emit-code-bound-seed (when code-bound ids change) → npm run content:primitives:syncnpm run content:emit-library → commit seeds + functions/generated/.

Execution models (see functions/funcxRegistry.ts and each function’s meta.execution):

meta.execution Runtime Catalox
code TypeScript handler in the router Readonly meta + IO schemas (requiresInternalCode, implementation, internalCodeReason)
content Catalox instructions + prompt via executeFuncxFromContent Editable prompts, rules, tests

Verify bundled metadata: npm run content:verify-execution-metadata. Catalog readiness audit (all 151 ids): npm run audit:catalog or npm run test:readiness. Code-bound seed emit: npm run content:emit-code-bound-seed.

Sidekick run() path (v4.9.10) — ten gateway-aligned ids use content execution with envelopeKind and envelopeToTemplateVariables. Import helpers from @x12i/funcx/functions; golden envelopes and prompt hashes ship in @x12i/funcx/fixtures.

Built-in Default output
execution-plan, research-plan-questions, execution-evaluate-result, pre-synthesize, post-audit, post-fix, post-pick-best, post-craft-final JSON object
post-audit-checklist Markdown string (args.outputMode: "json" for structured { checks, overallFeedback })
post-audit-merge Plain text string (merged result only)
import { run, getFuncxOutputSchema } from "@x12i/funcx/functions";
import { loadSidekickEnvelope, loadSidekickFixtureManifest } from "@x12i/funcx/fixtures";

const checklist = await run("post-audit-checklist", loadSidekickEnvelope("post-audit-checklist"));
// → markdown string with ### Checks / ### Overall feedback

const schemas = getFuncxOutputSchema("post-audit-checklist"); // string (markdown default)
const jsonSchema = getFuncxOutputSchema("post-audit-checklist", { outputMode: "json" });

Regenerate golden prompt hashes after seed edits: npm run content:emit:sidekick-fixtures.

Typed imports: import { semanticCompare } from "@x12i/funcx/functions" (codegen). Dynamic: run("semanticCompare", input, { resolver }) unchanged.

FuncX v5 — normalized ai-functions catalog
v4 (legacy blobs) v5 (current)
~6 Catalox rows per function (fx/<id>/instructions, rules, meta.json, …) One catalog item per function
Opaque data.body string blobs Structured data.instructionsText, data.rules, data.io, …
Identity via data.funcxRef / indexed.skillId Identity via indexed.functionId
Library index in fx/index.v1.json + fx/index/v1/*.json Index derived from catalog list; enrichment writes normalized items

Upgrade an existing Catalox env (once, before deploying v5 code):

npm run content:migrate:ai-functions:dry    # review report
npm run content:migrate:ai-functions:apply  # collapse rows + delete legacy blobs
npm run content:primitives:sync             # optional: refresh from seeds

Fresh installs: skip migration — content:primitives:sync writes the normalized shape directly.

Full CRUD, data shape, and operator workflows: .docs/AI-FUNCTIONS-CATALOG-CATALOX.md.

Metadata tier API: aiFunctionsCatalogService (TS) and GET|POST|PUT|PATCH|DELETE /functions/catalog/* (HTTP).


Why this exists

LLM "functions" start easy and get messy:

  • output drifts
  • JSON breaks
  • edge cases show up
  • you add retries, parsing, validation, model picking… and end up with glue code

FuncX (@x12i/funcx on npm) standardizes that work:

  • Typed contracts (input/output schemas)
  • JSON-only execution with retries + repair
  • Evaluation (judge/rules)
  • Optimization loop (run → judge → fix → repeat)
  • Skill packs stored as files (weak/strong/ultra), so prompts aren't buried in code
  • Functions lifecycle (create → validate → release) with score-gated promotion

Install

Node 20+ required (see engines in package.json).

npm i @x12i/funcx
Optional local CPU backends
npm i node-llama-cpp          # GGUF via llama.cpp
npm i @huggingface/transformers  # Transformers.js

FuncX MCP

Expose the FuncX catalog as an MCP server so assistants can call your functions as tools, read catalog resources, and use bundled prompts.

  • Library: import { createFuncXMcpServer, ... } from "@x12i/funcx/mcp"
  • CLI: funcx-mcp (installed with the package: npx funcx-mcp)

Run npm run build before local tests; unit tests load the compiled dist/ entry points like the rest of this repo.

Transports

stdio (default) — wire Cursor, Claude Desktop, or other MCP clients to a subprocess:

{
  "mcpServers": {
    "funcx": {
      "command": "npx",
      "args": ["funcx-mcp", "--transport", "stdio"]
    }
  }
}

Streamable HTTP — same process flags or env; defaults bind 127.0.0.1:3333 at /mcp:

npx funcx-mcp --transport http --host 127.0.0.1 --port 3333 --endpoint /mcp

Point the client at http://127.0.0.1:3333/mcp. When FUNCX_MCP_REQUIRE_AUTH is true or you bind a non-loopback host without opting out, set FUNCX_MCP_API_KEY and send Authorization: Bearer <key> or x-api-key: <key>.

Browser-like requests send an Origin header; only localhost-style origins and same-host origins are allowed unless you extend that logic in your integration.

Exposure filters

CLI flags (--allowed-prefix, --blocked-prefix, --allowed-function-id, --blocked-function-id) and env vars (FUNCX_MCP_ALLOWED_PREFIXES, etc., comma-separated) match the exposure options on CreateFuncXMcpServerOptions. Draft content skills are hidden when FUNCX_MCP_ACTIVE_ONLY is true (default) unless FUNCX_MCP_INCLUDE_DRAFT or --include-draft is set.

Programmatic use
import { createFuncXMcpServer } from "@x12i/funcx/mcp";

const server = await createFuncXMcpServer({
  transport: "stdio",
  exposure: { exposeCatalogResources: true, exposePrompts: true },
});
await server.start();

Use createFuncXMcpStdioServer / createFuncXMcpHttpServer when you need a specific transport. refreshCatalog() reloads the catalog and updates registrations for stdio / new HTTP sessions.

See .env.example for FUNCX_MCP_* names.


Quick start

1) Use prebuilt functions
import { classify, summarize, match, matchLists } from "@x12i/funcx/functions";

const { categories } = await classify({
  text: "I was charged twice this month.",
  categories: ["Billing", "Auth", "Support"],
});

const { summary } = await summarize({ text: longDoc, length: "brief" });

// One query → best matching candidates from a labelled list
const result = await match({
  query: "CVSS base score",
  candidates: [
    { id: "cvss_base",    label: "CVSS base score | numeric severity 0–10" },
    { id: "cvss_vector",  label: "CVSS vector string | attack surface encoding" },
    { id: "patch_status", label: "Patch status | applied / missing / n/a" },
  ],
  maxResults: 1,
});
// result.noMatch → false; result.matches[0].id → "cvss_base"

const r = await matchLists({
  list1: sourceFields,
  list2: targetFields,
  guidance: "Match by semantic meaning.",
});
match() — one query, many candidates

Finds the best-matching candidates from an arbitrary list for a single query string. Returns stable IDs, confidence scores, and optional reasons. Never throws on model parse failures — returns { noMatch: true } instead.

import { match } from "@x12i/funcx/functions";

const result = await match({
  query: "affected product / version",
  candidates: fieldList,   // MatchCandidate[]  { id, label }
  maxResults: 3,           // default 3
  minScore: 0.4,           // optional threshold
  allowNoMatch: true,      // default true  → noMatch result instead of throw
  returnReasons: true,     // default true  → reason string per match
  maxCandidates: 80,       // default 100   → cap sent to model; excess pre-ranked by trigram overlap
  model: "deepseek/deepseek-v4-flash",
});

if (result.noMatch) {
  console.log("no match found");
} else {
  console.log(result.matches[0].id, result.matches[0].score, result.matches[0].reason);
}

MatchInput fields:

Field Type Default Description
query string required Free-text search query
candidates MatchCandidate[] required { id: string|number; label: string }
maxResults number 3 Maximum matches returned
minScore number Drop matches below this confidence (0–1)
allowNoMatch boolean true Return { noMatch: true } instead of throwing when no match / model error
returnReasons boolean true Include a short reason per match
maxCandidates number 100 Hard cap on candidates sent to the model; excess are pre-ranked by character-trigram overlap
guidance string Extra matching guidance appended to the prompt
additionalInstructions string Appended verbatim to all instruction tiers
model, mode, temperature, maxTokens (number or "auto"), timeoutMs, vendor, client Standard LLM options

MatchOutput:

{
  query: string;
  noMatch: boolean;
  matches: Array<{ id: string | number; score: number; reason?: string }>;
}

Large candidate sets: pass maxCandidates to guard against context-window overflow. The library pre-selects the most relevant candidates using trigram overlap before the model call, so quality stays high even when the full list is large. Default cap is 100; tune down to ~40–60 for lightweight profile choices (cheap/default) or local llama.

2) Run any function by name
import { run } from "@x12i/funcx/functions";

const result = await run("extract-invoice-lines", { text: invoiceText });
// → { lines: [{ description: "...", amount: 4500 }, ...] }

Success shape: In-process run() returns the skill value directly (Promise<unknown>). JSON skills normally return a plain object; validateOutput: true yields { result, validation }. The optional HTTP server wraps payloads as { result, usage, requestId, ... } — use getRunJsonResult from @x12i/funcx/functions so adapters see the same inner JSON whether you call run() locally or parse HTTP bodies.

Input validation (v4.9.14+): Content-backed functions validate the request against catalog io.inputSchema before any LLM call. Invalid input throws FuncxInputValidationError (code: INPUT_VALIDATION; HTTP 400 on POST /run). Run-control fields (client, model when not in schema, etc.) are stripped before validation.

Generic execution envelope: Orchestrators can standardize on capability ids such as execution/plan, execution/evaluate-result, and research/plan-questions (slash forms normalize to hyphen keys). Import canonical string constants FUNCX_EXECUTION_PLAN_FUNCTION_ID, FUNCX_EXECUTION_EVALUATE_RESULT_FUNCTION_ID, and FUNCX_RESEARCH_PLAN_QUESTIONS_FUNCTION_ID from @x12i/funcx/functions to avoid drift with other packages. Payloads share GenericExecutionEnvelope (goal, optional input, context, result, args, attribution); TypeScript types and a draft genericExecutionEnvelopeJsonSchema export from @x12i/funcx/functions. Forward nested telemetry with buildAskAttribution on custom builtins (same merge rules as the generic built-ins).

Migration table (legacy ai-tasks envelopes vs generic ids), Catalox seed checklist, and upgrade notes: docs/migration-generic-execution.md.

Full list (manual mode): All library functions are available for programmatic use with rules and optimization. See Library functions — manual mode for the complete list and how to run them by name with a content provider or resolver.


Runx authoring sets (built-in FuncX)

Six specialized authoring families are registered as built-ins (callable via run() / the REST API). Each uses strict JSON I/O schemas, askJson-style generation, and optional Catalox seed content under content-seed/functions/. They model FuncX → structured draft → Runx validates and stores in Catalox; execution of produced scripts or UI is a separate concern.

Set ID prefix Deliverables
Generic JavaScript runx.* Small reusable JS capabilities (normalize, plan reuse, generate code/tests, review, repair, build draft). Optional authoringHints / reuseHints for evolution-driven parameterization.
API adapter runx.api.* HTTP adapters using context.http, contracts, tests, Catalox-ready api-adapter drafts
UI authoring runx.ui.* OpenUI Lang, React, HTML, UI tests, review/repair, Catalox-ready ui drafts
Script / ops runx.script.* PowerShell, bash/sh, Windows CMD, network CLI, tests, review/repair, Catalox-ready script drafts
Condition runx.condition.* Executable guard predicates (JS / JSONLogic / JSONata / filter-predicate) with smoke tests and auto-repair
Evolution runx.evolution.* Health assessment, repair, gap classification (parameterize-candidate), composition discovery, test-case generation, parameterization hints for normalize/plan

Example:

import { run } from "@x12i/funcx/functions";

const draft = await run("runx.script.normalizeScriptRequest", {
  requestText: "Read-only bash snippet to print df -h.",
  preferredTarget: "bash",
});

Runners and schema exports live on @x12i/funcx/functions (for example runxScriptNormalizeScriptRequest, runxUiPlanUiDelivery, runxApiPlanApiAdapter).

Refresh seed JSON after editing TypeScript IO schemas (functions/catalog/runx/**/ioSchemas.ts):

npm run build
npx tsx scripts/emitRunxSeedSchemas.ts && npx tsx scripts/emitRunxSeedBundle.ts
npx tsx scripts/emitRunxApiSeedSchemas.ts && npx tsx scripts/emitRunxApiSeedBundle.ts
npx tsx scripts/emitRunxUiSeedSchemas.ts && npx tsx scripts/emitRunxUiSeedBundle.ts
npx tsx scripts/emitRunxScriptSeedSchemas.ts && npx tsx scripts/emitRunxScriptSeedBundle.ts
npx tsx scripts/emitRunxConditionSeedSchemas.ts && npx tsx scripts/emitRunxConditionSeedBundle.ts
npx tsx scripts/emitRunxEvolutionSeedSchemas.ts && npx tsx scripts/emitRunxEvolutionSeedBundle.ts

Then push merged seeds to Catalox (requires your Firebase / FuncX Catalox setup):

npm run content:primitives:sync

Core concepts

Function packs (file-based prompts)

Functions live as files in a content store (git repo or local folder). Canonical paths use the functions/ prefix:

functions/<id>/weak       # local/cheap instructions
functions/<id>/strong     # cloud/high-quality instructions (mode "normal" uses this)
functions/<id>/ultra      # optional highest-tier instructions
functions/<id>/rules      # optional judge rules (JSON)
functions/<id>/meta.json  # status: draft | released, version, scoreGate
functions/<id>/test-cases.json   # stored test cases for validate/optimize
functions/<id>/race-config.json  # race defaults + winner profiles (best/cheapest/fastest/balanced)
functions/<id>/races.json        # race history (append-only, capped)

Content can be loaded from a git-backed resolver (e.g. .content repo) or via a content provider (shared-store or inline). The library exports createFunctionContentProvider and ResolverBackedContentProvider for use with run(skillName, request, { contentProvider, scopeId?, model?, validateOutput? }). Use options.profile on HTTP /run only for race winner keys (best | cheapest | fastest | balanced), not ai-profiles tiers.

Docs: COLLECTIONS_MAPPING.md and DATA_MAPPING.md (when present in docs/) describe collections, schema, and data; see also .docs/ for in-repo design docs.

Prompts are:

  • reviewable in PRs
  • shareable across projects
  • versionable like code
Modes
Mode Typical backend Use
weak local (CPU) dev/offline/cheap
normal / strong cloud production default
ultra cloud strictest / highest-tier

The safety layer (JSON + validation + retries)

For any JSON-producing call, the library applies:

  • extract-first JSON (prefers ```json fences, then first balanced object/array)
  • safe parsing (guards against prototype poisoning)
  • optional schema validation (Ajv)
  • deterministic retries (normal → JSON-only guard → fix-to-schema with errors)
import { runJsonCompletion } from "@x12i/funcx/functions";

const { parsed, text, usage } = await runJsonCompletion({
  instruction: "Extract line items from this invoice. Return JSON only.",
  options: { model: "deepseek/deepseek-v4-flash", maxTokens: 800 },
});
Example generation (schemas, Markdown, plain content)

Use these built-ins to produce one representative sample from a contract plus guidance—useful for smoke inputs, docs, and UI previews. They use the same JSON pipeline and LLM options (model, mode, temperature, maxTokens, timeoutMs, vendor, client) as other functions.

Fake data / secrets: models are instructed to use obviously fictional placeholders (e.g. example.com, proj_test_01) and not to emit realistic API keys, tokens, passwords, or private identifiers. Always review generated samples before shipping or persisting them.

import {
  generateJsonExample,
  generateMdExample,
  generateContentExample,
  run,
} from "@x12i/funcx/functions";

// JSON example that should match your JSON Schema (validated when the schema compiles in Ajv)
const jsonOut = await generateJsonExample({
  jsonSchema: {
    type: "object",
    additionalProperties: false,
    properties: { userId: { type: "string" }, count: { type: "integer", minimum: 0 } },
    required: ["userId"],
  },
  guidance: [
    "Use realistic but fake values.",
    "Prefer a minimal happy-path payload.",
  ],
  examples: [{ userId: "usr_sample_1", count: 0 }],
  model: "deepseek/deepseek-v4-flash",
});

// Markdown example
const mdOut = await generateMdExample({
  guidance: ["Write a concise operator-facing runbook example."],
  examples: ["# Existing example\n\n..."],
});

// Plain text / CSV / email body / etc.
const textOut = await generateContentExample({
  contentType: "text/plain",
  guidance: ["Generate a realistic email body for this workflow."],
});

// Same functions via run() by name
const same = await run("generate-json-example", {
  jsonSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] },
  guidance: ["Single smoke-test object."],
  client: myOpenRouterClient,
});

Evaluation & optimization

Judge a response
import { judge } from "@x12i/funcx/functions";

const verdict = await judge({
  instructions: "...",
  response: "...",
  rules: [
    { rule: "Must output valid JSON only", weight: 3 },
    { rule: "Field names must match the schema exactly", weight: 2 },
  ],
  threshold: 0.8,
  mode: "strong",
});
// verdict.pass, verdict.scoreNormalized, verdict.ruleResults
Generate rules from instructions
import { generateJudgeRules } from "@x12i/funcx/functions";

const { rules } = await generateJudgeRules({ instructions: myPrompt });

Methodology: When providing good/bad examples (e.g. POST /optimize/rules or optimizeJudgeRules), include a brief rationale (why it's good or bad) when possible; it improves rule quality.

Generate / improve instructions until they pass
import { generateInstructions } from "@x12i/funcx/functions";

const best = await generateInstructions({
  seedInstructions: myPrompt,
  testCases: [{ id: "t1", inputMd: "Invoice #1234\nConsulting: $4,500" }],
  call: "ask",
  targetModel: { model: "deepseek/deepseek-v4-flash", class: "normal" },
  judgeThreshold: 0.8,
  targetAverageThreshold: 0.85,
  loop: { maxCycles: 5 },
  optimizer: { mode: "strong" },
});
// best.achieved, best.best.instructions, best.best.avgScoreNormalized
Fix instructions from judge feedback
import { fixInstructions } from "@x12i/funcx/functions";

const { fixedInstructions, changes } = await fixInstructions({
  instructions: myPrompt,
  judgeFeedback: verdict,
});
Compare two instruction versions
import { compare } from "@x12i/funcx/functions";

const result = await compare({
  instructions: baseInstructions,
  responses: [
    { id: "v1", text: responseFromVersionA },
    { id: "v2", text: responseFromVersionB },
  ],
  threshold: 0.8,
});
// result.bestId, result.ranking
Benchmark models
import { raceModels } from "@x12i/funcx/functions";

const ranking = await raceModels({
  taskName: "invoice-lines",
  call: "askJson",
  testCases: [{ id: "t1", inputMd: "..." }],
  threshold: 0.8,
  models: [
    { id: "deepseek", model: "deepseek/deepseek-v4-flash", vendor: "deepseek", class: "strong" },
    { id: "claude", model: "anthropic/claude-3-5-haiku", vendor: "anthropic", class: "strong" },
  ],
});

Client (one API across providers)

import { createClient } from "@x12i/funcx";

const ai = createClient({ backend: "openrouter" });

const res = await ai.ask("Write a product tagline.", {
  model: "deepseek/deepseek-v4-flash",
  maxTokens: 200,
  temperature: 0.7,
});
// res.text, res.usage, res.model, res.routing?, res.cost?, res.timing?

FuncX does not ship a built-in OpenRouter default. Pass model on each call as either a profile/choice key (cheap/default) or a catalog slug (deepseek/deepseek-v4-flash) — FuncX resolves both via @x12i/ai-profiles (see Profiles + choice). configureDefaultLlmModel({ model }) is for unit/live test bootstrap only.

Trace / diagnostics metadata (authoritative when available)

client.ask() returns a stable, additive metadata envelope for downstream trace-mode consumers:

  • res.usage: provider token usage when available, plus funcx echoes usage.maxTokensRequested (the cap you passed in AskOptions.maxTokens). For convenience, usage.tokensPrompt|tokensCompletion|tokensTotal mirror prompt_tokens|completion_tokens|total_tokens.
  • res.routing: stable provider label + request-id bag for correlation (keys are additive; existing keys won’t be renamed).
  • res.cost: costUsd, costStatus (priced | unpriced), and optional costBreakdown from resolveInvokeBilling.
  • res.timing: ISO timestamps and durations when tracked by the backend.

Notes:

  • These fields are optional and are populated only when the backend can extract them from response bodies/headers. (OpenRouter provides exact cost via usage.cost and may provide request IDs via headers.)
  • The raw response remains opt-in; result payloads stay small by default.
Backends
createClient({ backend: "openrouter", openrouter?: { apiKey?, baseUrl?, appName?, appUrl? } })
createClient({ backend: "llama-cpp", llamaCpp: { modelPath, contextSize?, threads? } })
createClient({ backend: "transformersjs", transformersjs: { modelId?, cacheDir?, device?: "cpu" } })

FuncX function execution (run(), callAI, content-backed /run) accepts model as a profile/choice key (cheap/default, balanced/default, …) or a catalog slug (google/gemini-2.5-flash-lite). Resolution and catalog validation use @x12i/ai-profiles at invoke time. Local CPU runs require explicit paths: createClient({ backend: "llama-cpp", llamaCpp: { modelPath } }) or env LLAMA_CPP_MODEL_PATH; transformersjs likewise with modelId / TRANSFORMERS_JS_MODEL_ID (no package defaults).

Race evaluation keys (best, cheapest, fastest, balanced) remain FuncX scope labels for stored winners; winners persist model only.

See .docs/funcx-llm-call-contracts.md for the full contract.


Configuration

.env (all optional unless you use that backend):

OPENROUTER_API_KEY=sk-or-...
PREFER_OPENROUTER=true

# Local CPU — set explicitly per deployment (optional):
# LLAMA_CPP_MODEL_PATH=/path/to/model.gguf
# LLAMA_CPP_THREADS=6
# TRANSFORMERS_JS_MODEL_ID=your/huggingface-model-id

# Optional: persist REST server activities to Mongo (npm run serve only)
# FUNCX_ACTIVIX_ENABLED=1
# MONGO_ACTIVIX_URI=mongodb://...   # or MONGO_LOGS_URI / MONGO_URI

See Optional MongoDB activity persistence (Activix) for collection names and database selection.

Package Version Role
@x12i/ai-profiles ^3.4.0 (direct) Single source of truth — profile registry, catalog, slug resolution, invoke routing, invoke cost
@x12i/catalox ^5.1.3 Firestore-backed function content

FuncX resolves profile/choice keys and catalog slugs through ai-profiles on every run() / callAI / client.ask() / REST /run.

Profiles + choice (@x12i/ai-profiles)

An AI profile is capability intent (cheap, balanced, deep, json, …). A choice is a concrete implementation within that profile (default, google_floor, deepseek_cheap, …). Stored config should use profile/choice keys — not bare model slugs.

Layer Example Where
Profile + choice balanced/default, cheap/deepseek_cheap Graph modelConfig, job config — pass directly to FuncX
OpenRouter slug deepseek/deepseek-v4-flash, google/gemini-2.5-flash-lite FuncX run(), client.ask(), REST options.model

Recommended — pass profile/choice and let FuncX resolve at invoke time:

import { run } from "@x12i/funcx/functions";

await run("classify", { text, categories }, { model: "cheap/default" });
await ai.ask("Summarize this.", { model: "balanced/default", maxTokens: 500 });

Or pass a catalog slug when you already know it:

await run("summarize", { text }, { model: "deepseek/deepseek-v4-flash" });

Host-side resolution (gateways, pre-flight validation) — optional; same package:

import { resolveAndNormalizeInvokeModel } from "@x12i/ai-profiles";

const { model } = await resolveAndNormalizeInvokeModel("cheap/deepseek_cheap", {
  catalogLane: "text",
});
// model → e.g. "deepseek/deepseek-v4-flash"

PREFER_OPENROUTER (default true, from @x12i/ai-profiles) controls OpenRouter vs vendor-direct routing. FuncX applies it when resolving model.

REST options.profile on /run is not ai-profiles — it selects a race evaluation winner (best | cheapest | fastest | balanced) for scope-specific model picks.

OpenRouter integration (v4.9)
Area Status Notes
ai-profiles as single SOT Done @x12i/ai-tools removed; catalog + cost via @x12i/ai-profiles ^3.4.0
profile/choice on model Done cheap/default resolves inside FuncX (resolveProfileKeys)
Explicit model per call By design No tier env defaults; pass profile/choice or slug
Invoke cost Done calculateInvokeCostFromRecord from ai-profiles
PREFER_OPENROUTER precedence Done PREFER_OPENROUTER wins over legacy USE_OPENROUTER when both set
BYOK Supported x-openrouter-key header on REST; OPENROUTER_API_KEY env fallback
REST options.profile Race keys only best | cheapest | fastest | balanced — not ai-profiles tiers
RunOptions.temperature / topP Done (4.9.9) Forwarded on run() / runStream() content path
Transport error.cause Done (4.9.9) OpenRouter fetch failures attach underlying cause on NxAiApiError
Sidekick fixtures export Done (4.9.10) @x12i/funcx/fixtures — golden envelopes + prompt hashes
post-audit-checklist markdown default Done (4.9.10) JSON only when args.outputMode = "json"
Embedder transport retry policy Done (4.9.12) See Retry policy for embedders below
RunOptions.transportRetries Done (4.9.13) Built-in exponential backoff on retriable transport failures
OpenRouter server tools Done (4.9.15) Runtime-enabled calls route through @x12i/ai-gateway; options.serverTools, options.openrouter, REST runtime, catalog meta.runtime.serverTools
Sidekick output schema CI Done (4.9.12) getFuncxOutputSchema validated against golden outputs for all 10 ids
Retry policy for embedders (CR-FUNCX-5)

When wrapping run() in your pipeline:

Error class Examples Retry?
Transport NxAiApiError with code: "OPENROUTER_HTTP_ERROR" and cause (ECONNRESET, ETIMEDOUT, fetch failed) Yes — idempotent run() hops only, with backoff
HTTP rate limit OPENROUTER_HTTP_ERROR with status: 429 Yes — respect Retry-After when present
Schema / validation ERR_SCHEMA_INVALID, malformed model JSON No — fix envelope, prompt, or schema
Unknown function Catalog / resolver misconfiguration No

Inspect err.cause on transport failures to distinguish flaky network from model output issues. Do not blindly retry non-idempotent side effects outside FuncX run().

Pass transportRetries: 2 on run() to retry transient OpenRouter failures automatically (up to 3 total attempts with backoff). Default 0 leaves retry policy to the embedder.

await run("pre-synthesize", envelope, { client, transportRetries: 2 });

Consumer prompt parity: render your disk templates with @x12i/funcx/fixtures golden envelopes, normalize via normalizePromptForParity + hashSidekickPrompt, and compare to loadSidekickFixtureManifest().sidekicks[].promptHash. FuncX CI enforces the same hashes on seed changes.

OpenRouter server tools

FuncX routes runtime-enabled OpenRouter execution through @x12i/ai-gateway. Existing plain OpenRouter calls continue to use the legacy HTTP-compatible path unless server tools, options.openrouter, or :online runtime behavior is requested. Server tools are opt-in per call, per REST request (runtime.serverTools), or per function catalog metadata (meta.runtime.serverTools).

Web search

await run("summarize", input, {
  model: "balanced/default",
  responseFormat: "markdown",
  serverTools: {
    webSearch: {
      mode: "required",
      allowedDomains: ["learn.microsoft.com"],
      maxResults: 5,
      maxTotalResults: 15,
    },
  },
});

Search + fetch

await ai.ask("Summarize the latest OpenRouter server tools docs.", {
  model: "balanced/default",
  serverTools: {
    webSearch: { mode: "required" },
    webFetch: { mode: "allowed", maxContentTokens: 50000 },
  },
});

Datetime

await ai.ask("What is due tomorrow?", {
  model: "cheap/default",
  serverTools: {
    datetime: { mode: "allowed", timezone: "Asia/Jerusalem" },
  },
});

Apply patch (Responses API; returns proposals only — FuncX never mutates files silently)

await run("runx.script.repair", input, {
  openrouter: { apiMode: "responses" },
  serverTools: {
    applyPatch: { mode: "required", behavior: "return_only" },
  },
});

Tool usage, citations, images, and patch proposals appear additively under usage (and under trace.openrouterRuntime when trace: true). JSON functions remain JSON-safe: citations and tool telemetry stay in usage/trace, not inside parsed results unless the output schema explicitly requests them.

Legacy :online model suffixes (e.g. openai/gpt-5.2:online) normalize to the base slug with implied webSearch.allowed and emit OPENROUTER_ONLINE_VARIANT_DEPRECATED.


REST API (optional, stateless)

Expose functions and the full lifecycle over HTTP. Request/response shapes are described in this README and in the API contract when present (e.g. docs/API_CONTRACT.md); server–contract sync: docs/CONTRACT_SYNC.md. If docs/ is not in your clone, the REST API section below is the endpoint reference.

npm run build && npm run serve
# PORT=3780 by default
Authentication
Header Purpose
x-api-key Authenticates to the server — validated against LIGHT_SKILLS_API_KEY env
x-openrouter-key BYOK — passed through to OpenRouter so each user can use their own key and billing

If neither LIGHT_SKILLS_API_KEY nor AIFUNCTIONS_API_KEY is set, all requests are allowed.

Run and health
GET  /health                  health check → { version, uptime, skills, hasOpenrouterKey, backends }
GET  /config/modes            server mode→model mapping → { weak, normal, strong, ultra } (each { model, description })
POST /run                     { skill, input, options } → { result, usage, valueEstimate? }
POST /skills/:name/run        { input, options }        → { result, usage }
POST /functions/:id/run       { input, options }        → { result, usage, requestId, draft?, trace?, valueEstimate? }
GET  /skills                  list functions + metadata (legacy alias route)
GET  /skills/:name            function detail (legacy alias route)
GET  /functions               list functions
GET  /functions/:id           function detail with status, version, last validation, currentInstructions, currentRules, currentRulesCount

Run request body may include options.scopeId and options.profile (best | cheapest | fastest | balanced) for scope-specific model selection; options.validateOutput: true returns { result, validation } against the library index schema; options.estimateValue: true with optional options.humanCostPerHour / options.humanCostProfile adds valueEstimate (see Cost vs value). Run responses include requestId (same as attribution traceId when provided). When the function is in draft status, the response includes draft: true. Pass options.trace: true in the run body to receive a trace object with the full prompt(s), model selection, and model used per call (for that request only; not stored). Run endpoints are rate-limited per key (see RATE_LIMIT_PER_MINUTE) and return X-RateLimit-Remaining and X-RateLimit-Reset headers.

Human value (dedicated routes; also callable via POST /run with skill: "humanValue.compute" etc.):

POST /human-value/derive-strategy
POST /human-value/compute
POST /human-value/resolve-for-function
POST /functions/:id/human-value/derive-strategy    # body { apply: true } persists meta.humanValueStrategy
GET  /functions/:id/human-value/strategy
PUT  /functions/:id/human-value/strategy
POST /content/human-value/emit-strategies
GET  /analytics/value?from=&to=&functionId=&projectId=&groupBy=functionId

Run mode may be weak, normal, strong, ultra, or profile modes best, cheapest, fastest, balanced. Profile modes require a race to have been run first; otherwise the server returns 422 NO_RACE_PROFILE.

Response formats and streaming

Use responseFormat to choose how the model should answer:

Format Use when
json (default) Structured tools, extraction, automation — output is parsed and schema-validated.
markdown Reports, explanations, chat-style prose — returns a markdown string (no JSON wrapper).
text Plain-text summaries or labels — returns a raw string.

npm library (@x12i/funcx/functions):

import { run, runStream } from "@x12i/funcx/functions";

const report = await run("my.report", { topic: "Q1" }, { responseFormat: "markdown" });

// Optional sampling overrides (v4.9.9+) — apply to content-backed and router LLM paths
await run("execution-plan", envelope, {
  model: "cheap/default",
  temperature: 0.2,
  topP: 0.95,
});

for await (const event of runStream("my.chat", { message: "Hi" }, { responseFormat: "markdown" })) {
  if (event.type === "delta") process.stdout.write(event.text);
}

run() rejects options.stream: true — use runStream() for streaming. Pass signal: AbortSignal to cancel. Content runStream() applies the same envelope → template variable merge as non-streaming runs.

Nothing streams by default. Every run is non-streaming unless the caller sets stream: true (or sends Accept: text/event-stream). Bundled seed meta.json sets runtime.supportsStreaming: false and runtime.streaming.defaultEnabled: false for all functions; to allow streaming for a specific function, set runtime.supportsStreaming: true on that function’s Catalox meta (streaming still requires an explicit stream: true from the client).

REST streaming — set options.stream: true or Accept: text/event-stream on POST /run, POST /skills/:name/run, or POST /functions/:id/run:

POST /functions/chat.answer/run
Accept: text/event-stream
Content-Type: application/json

{ "input": { "message": "Hello" }, "options": { "stream": true, "responseFormat": "markdown" } }

Events: start, delta, usage, final, error, done (Server-Sent Events).

Catalog migrations (one-time, existing Catalox envs):

# v5: multi-row fx/* blobs → one normalized item per function (required before v5 deploy)
npm run content:migrate:ai-functions:dry
npm run content:migrate:ai-functions:apply

# Optional legacy: add responseContracts + runtime to function meta (pre-v5 meta.json migration)
npm run content:migrate:response-contracts:dry
npm run content:migrate:response-contracts:apply
Agent platform — Memorix, Narrix, Reportix, Content Recipes, FuncX Catalog
Namespace Role
memorix Knowledge discovery/extraction, Q&A, prompt matching, entities
narrix Narrative composition (story + narrix → content)
reportix Data-backed report pages and data-to-story output
contentRecipes Content-structure composition (recipe → content)
funcxCatalog Registry introspection (list/find functions; no LLM on listers)

Memorix (memorix.knowledge + memorix.access):

  • memorix.exploreKnowledgeTypes / memorix.extractKnowledge — discover types then extract (single | multiple | all | auto)
  • memorix.answerQuestionsFromContent, memorix.classifyRelevantQuestions, memorix.classifyRelevantNarratives, memorix.extractPromptEntities

Narrix (composition only — not Q&A or reports): narrix.composeContent, narrix.planComposition, narrix.validateComposition, narrix.rewriteToNarrix

Reportix: reportix.classifyDataNarrativesreportix.tellDataStoryInNarrativereportix.composeQuestionAnswerReportPage

Content Recipes: contentRecipes.selectRecipe, planContent, composeContent, validateContent, rewriteWithRecipe, generateRecipeListItem, validateRecipe, composeFromNarrix

FuncX Catalog (deterministic listers + AI find):

  • funcxCatalog.listCategories, listAllFunctions, listFunctionsByCategorymode: brief|detailed, format: json|markdown
  • funcxCatalog.findFunctionneed → matching functionId
  • funcxCatalog.getFunctionfunctionId with detail: brief|full (default brief)

Seeds: content-seed/functions/{memorix,narrix,reportix,contentRecipes,funcxCatalog}.*.json. Emit:

npm run content:agent-platform:emit-seeds
npm run content:primitives:sync
npm run content:recipes:catalogs:sync   # optional: recipe native catalogs

pagenti.* has been removed; use memorix.* instead.

Functions lifecycle

Create a function, iterate on it, validate quality, then release it to a stable versioned endpoint.

POST /functions               create: { id, seedInstructions, scoreGate?, rules? }
POST /functions/:id:validate  run schema + semantic scoring → { passed, scoreNormalized, cases }
POST /functions/:id:release   promote to released (blocked if score < scoreGate). Body may include `scopeId` for scope-specific release.
POST /functions/:id:rollback  set current instructions/rules to a previous version (body: `{ version: gitRef }`; requires version APIs)
POST /functions/:id:optimize  rewrite instructions in-place
POST /functions/:id:push      push to remote git repo (requires SKILLS_LOCAL_PATH)
GET  /functions/:id/versions  instruction version history (git shas)
POST /functions/:id/versions/:version/run  run at a pinned version (ref = git sha from versions list)
GET  /functions/:id/scopes    query: scopeId required → { functionId, scopeId, releases }
POST /functions/:id/apply    apply evaluation result to scope (body: { scopeId?, evaluationSessionId, appliedBy? }) → { appliedProfileSet, scopeId, functionId }
GET  /functions/:id/test-cases
PUT  /functions/:id/test-cases  { testCases: [{ id, input, expectedOutput? }] }
POST /functions/:id/save-optimization  persist instructions, rules, examples from optimization wizard
POST /functions/generate-examples      { description, count?, mode? } → { examples, usage? }
Race / benchmark
POST /race/models              race models, temperatures, or tokens (async job) — body: skillName|prompt, testCases?, candidates|models, functionKey?, applyDefaults?, raceLabel?, notes?, type? (model|temperature|tokens), model?, temperatures?, tokenValues?
GET  /functions/:id/profiles   race winner profiles and defaults → { defaults, profiles: { best, cheapest, fastest, balanced } }
GET  /functions/:id/race-report  race history — query: last, since, raceId → { races }

Job result for a race includes ranked, raw, winners, usage. Run with mode: best|cheapest|fastest|balanced uses the stored profile for that function.

The cheapest winner is selected by pricing rate using the bundled pricing table (see below).

Optimization endpoints
POST /optimize/generate       generate instructions from test cases (async job)
POST /optimize/judge          score a response against rules → { pass, score, ruleResults }
POST /optimize/rules          generate rules from labeled examples or instructions
POST /optimize/rules-optimize optimize existing rules from examples with rationale (append/replace)
POST /optimize/fix            fix instructions from judge feedback → { fixedInstructions, changes, summary, usage, optional addedRuleBullets }
POST /optimize/compare        rank 2+ responses by quality → { ranking, bestId, candidates }
POST /optimize/instructions   one-shot instruction rewrite
POST /optimize/skill          rewrite one skill's instructions in-place
POST /optimize/batch          batch rewrite (async job)
Jobs (for async operations)
GET /jobs               list recent jobs
GET /jobs/:id           status, progress, result
GET /jobs/:id/logs      streaming log lines
Content workflows
POST /content/sync         sync instructions to content store (and optional push)
POST /content/index        enrich discovery metadata on normalized catalog items → { indexed, skills, errors }
POST /content/index/full   build and return full embedded library snapshot (body: prefix?, staticOnly?, writeDocsFallback?) → { fullSnapshot, ... }; writes .docs/library-index.full.fallback.json by default
POST /content/fixtures      validate examples vs io.output schemas
POST /content/layout-lint  enforce folder-based layout under functions/
Cost estimation (resolveInvokeBilling)

All LLM paths resolve token usage and USD cost through one orchestrator before responses are returned or persisted to Activix. Library calls expose CallAIResult.cost / usage.costUsd; HTTP and run() responses include a structured usage object (unless opted out).

Resolution order (first match wins; provider/router cost is never overwritten by catalog):

Step Condition Result
A Provider/router finite costUsd costStatus: "priced"
A′ Explicit costStatus: "priced" priced (cost optional)
A″ Explicit costStatus: "unpriced" unpriced — catalog not called
B Tokens + catalog authoritative (calculateInvokeCostFromRecord) priced + optional breakdown
C Tokens, still no price costStatus: "unpriced"
D Zero tokens omit cost fields

HTTP costEstimate maps costStatus: pricedstatus: "available"; unpricedstatus: "unavailable" with reasonCode: "BACKEND_NO_PRICING". Sources are provider-response or catalog only.

Activix (7.2+): with autoCost enabled, outer.cost is filled on completeRecord / failRecord from usage in outer.output. FuncX invoke billing uses @x12i/ai-profiles calculateInvokeCostFromRecord; Activix may still use its own catalog path transitively.

Config (funcx-specific env; legacy FUNCX_AI_TOOLS_* / AI_TOOLS_* names still honored):

Flag Default Env
calculateCost true FUNCX_AI_PROFILES_CALCULATE_COST=0 (or FUNCX_AI_TOOLS_CALCULATE_COST=0)
bundledOnly false AI_PROFILES_BUNDLED_ONLY=1 (or AI_TOOLS_BUNDLED_ONLY=1)
costIncludeBreakdown true configureAiProfilesEngine({ costIncludeBreakdown: false })

Model slugs and profile/choice keys are resolved via resolveModelSlug / requireCatalogModel (ai-profiles). Reasoning models use getModelReasoningProfile for token-budget headroom.

import {
  enrichAskResultCost,
  resolveInvokeBilling,
  configureAiProfilesEngine,
  resolveModelSlug,
  getModelReasoningProfile,
  configureActivix,
} from "@x12i/funcx";
includeUsage (default: true)

By default, run() wraps LLM function output as { result, usage } so callers get tokens and cost without extra wiring. Pass includeUsage: false for a clean function payload only (cost is still computed internally and sent to Activix when enabled).

import { run } from "@x12i/funcx/functions";

// Default — result + usage envelope
const out = await run("memorix.answerQuestionsFromContent", input);
// out.result — function output
// out.usage — { promptTokens, completionTokens, totalTokens, estimatedCost, costEstimate, ... }

// Clean — function output only
const clean = await run("memorix.answerQuestionsFromContent", input, { includeUsage: false });

HTTP run endpoints accept the same flag at options.includeUsage or top-level includeUsage:

POST /functions/memorix.answerQuestionsFromContent/run
{
  "input": { "...": "..." },
  "options": { "includeUsage": false }
}

When includeUsage is omitted or true, the response includes usage alongside result. When false, the response is { "result": …, "requestId": … } only.

Token budget (tokenBudget.* and maxTokens: "auto")

FuncX can derive and apply per-function max-tokens strategies stored in Catalox meta as maxTokensStrategy (versioned JSON: output archetype, padding, reasoning multiplier, ceilings).

Function Role
tokenBudget.deriveStrategy LLM (rare): design strategy from instructions / functionId
tokenBudget.adaptForModel Pure: adjust strategy to model maxCompletionTokens / reasoning
tokenBudget.computeMax Pure: strategy + input → maxTokens + breakdown
tokenBudget.resolveForFunction Orchestrator: meta strategy or archetype → final maxTokens
tokenBudget.auto Alias for resolveForFunction

Standalone (no FuncX function id):

import { run } from "@x12i/funcx/functions";

await run("tokenBudget.resolveForFunction", {
  instructions: "Summarize the document in 3 bullets.",
  input: { text: "..." },
  model: "deepseek/deepseek-v4-flash",
});

Auto max tokens on any LLM call:

await client.ask(prompt, { maxTokens: "auto", model: "deepseek/deepseek-v4-flash", system: instructions });

Batch-write strategies to Catalox meta: npm run content:token-budget:emit-strategies (optional --dry-run, --function-id <id>).

Combined token + human value batch: npm run content:catalog:emit-runtime-strategies.

Cost vs value (humanValue.* and estimateValue)

FuncX separates AI execution cost from estimated human effort equivalent:

Concept Meaning
Cost How much did the AI execution cost? (USD from usage / pricing)
Value How much human effort did this function likely save? (time → estimated human-cost equivalent)
Net value Estimated human-cost equivalent minus AI cost
ROI multiple Human-cost equivalent divided by AI cost (only when AI cost is known)

This is an operational estimate, not accounting truth. Prefer wording like estimated human-cost equivalent; avoid actual savings, guaranteed ROI, or revenue generated.

Strategies live in Catalox meta as humanValueStrategy (human-value-strategy.v1): a safe formula DSL (no arbitrary JavaScript). The LLM runs once via humanValue.deriveStrategy; runtime uses humanValue.compute (pure, no model).

Function Role
humanValue.deriveStrategy LLM (rare): design reusable strategy from function catalog context
humanValue.compute Pure: strategy + input/output/usage → minutes, cost equivalent, net value, ROI
humanValue.resolveForFunction Load meta strategy (or archetype fallback) and compute

Run with value estimate (HTTP or library):

import { run } from "@x12i/funcx/functions";

const out = await run("memorix.answerQuestionsFromContent", input, {
  estimateValue: true,
  humanCostPerHour: 80,
});
// out.result — function output
// out.valueEstimate — { humanMinutes, humanCostEquivalent, netValueUsd, roiMultiple, confidence }
POST /run
{ "skill": "classify", "input": { ... }, "options": { "estimateValue": true, "humanCostPerHour": 80 } }

Resolve value for a function (no LLM):

import { resolveHumanValueForFunction } from "@x12i/funcx/functions";

const value = await resolveHumanValueForFunction({
  functionId: "memorix.answerQuestionsFromContent",
  input,
  output,
  usage: { costUsd: 0.04, promptTokens: 1200, completionTokens: 300 },
  humanCostPerHour: 90,
});

Human cost profiles (override with humanCostPerHour or humanCostProfile: "analyst"):

Profile id Typical rate
general-knowledge-worker $50/hr
analyst $80/hr
senior-engineer $120/hr
legal-reviewer $150/hr

REST (also via generic POST /run):

  • POST /human-value/derive-strategy, POST /human-value/compute, POST /human-value/resolve-for-function
  • POST /functions/:id/human-value/derive-strategy (body { "apply": true } persists to meta)
  • GET|PUT /functions/:id/human-value/strategy
  • GET /analytics/value?from=&to=&functionId=&projectId=&groupBy=functionId
  • POST /content/human-value/emit-strategies

Batch: npm run content:human-value:emit-strategies, validate: npm run content:human-value:validate-strategies.

ai-skills: gateway invoke analysis (ai-skills/analyze-gateway-invoke-request)

Pre-invoke packet review before calling ai-gateway or running the target FuncX function. Full guide: .docs/GATEWAY-INVOKE-ANALYSIS.md.

FuncX key Alias (slash form)
ai-skills-analyze-gateway-invoke-request ai-skills/analyze-gateway-invoke-request

What runs: SDK static checks (optional) + FuncX packet consistency (automatic) + LLM simulation review. What does not run: the gateway, and the target functionId function.

Layer Field / mechanism
SDK deterministic deterministicFindings (your static pre-pass)
Packet consistency Auto: functionId vs actionRef vs runSkillRequest
LLM simulation This FuncX function → verdict, findings, markdownReport

Input: real sanitized invoke packet (not mock data). Canonical target id is functionId (FuncX id). Legacy skillKey is accepted with a deprecation warning.

npm run content:ai-skills:emit-seeds    # → aiSkills.seed.json
npm run content:primitives:sync
import { run } from "@x12i/funcx/functions";

await run("ai-skills/analyze-gateway-invoke-request", {
  functionId: "summarize",
  gatewayPayload: {
    actionType: "skill",
    actionRef: "summarize",
    workingMemory: { /* ... */ },
    smartInput: { /* ... */ },
    modelConfig: { model: "deepseek/deepseek-v4-flash" },
  },
  runSkillRequest: { functionId: "summarize", input: { text: "..." } },
  deterministicFindings: [], // SDK static pre-pass (optional)
});
Catalog judge rules

Catalog data.rules may be empty; runtime omits the rules section when there are no non-empty entries. Rules are appended to system instructions (callAI, askJson, buildFunctionPrompt) and passed through hand-written catalog runners.


Function-level cost & activity tracking

Every LLM call is automatically tagged with the function that originated it, so usage data can be attributed precisely — not just at the model or account level.

How it works:

  1. The server injects functionId automatically (e.g. extract.requirements, optimize.judge).
  2. Callers may optionally pass projectId, traceId, and tags in any POST body.
  3. The server embeds these as metadata in the outgoing provider request (user field for OpenRouter).
  4. Every response returns extended attribution fields in the usage object.

Optional request fields (any POST endpoint that calls an LLM):

Field Type Description
projectId string Logical project or tenant (e.g. "cognni-prod")
traceId string Correlation ID for distributed tracing. Auto-generated UUID if omitted.
tags object Free-form key-value metadata (string values)
options.includeUsage boolean When false, omit usage from the response (default true)

Example request:

POST /functions/extract.requirements/run
{
  "input": { "text": "..." },
  "projectId": "cognni-prod",
  "traceId": "req-983741",
  "tags": { "workflow": "classification", "environment": "production" }
}

Extended usage response:

{
  "promptTokens": 240,
  "completionTokens": 82,
  "totalTokens": 322,
  "model": "deepseek/deepseek-v4-flash",
  "latencyMs": 1430,
  "estimatedCost": 0.000147,
  "costEstimate": {
    "amountUsd": 0.000147,
    "status": "available",
    "confidence": "high",
    "source": "provider-response"
  },
  "functionId": "extract.requirements",
  "projectId": "cognni-prod",
  "traceId": "req-983741",
  "tags": { "workflow": "classification", "environment": "production" }
}

functionId is always present. projectId, traceId, and tags appear only when provided.

The library remains stateless when used as an npm dependency. The REST server is in-memory by default for GET /activity (ring buffer only). Optional Mongo persistence is described below.

Runtime logging (@x12i/logxer)

FuncX uses @x12i/logxer for structured runtime logs (diagnostic codes, evidence, correlation ids). Logging and the in-memory debug capture (runTime, AI activity ring buffer) are off in mode=prod unless you enable them in code or env.

Log level (single resolution — code wins over env):

Source Effect
configureFuncxLogging({ logLevel: "off" }) Silence all FUNCX log output
configureFuncxLogging({ logLevel: "debug" }) Emit debug and above (overrides env)
configureFuncxLogging({ logLevel: "warn" }) Emit warn and error only
FUNCX_LOGS_LEVEL Canonical env threshold when code does not set logLevel
FUNCX_LOG_LEVEL Legacy env alias (used only when FUNCX_LOGS_LEVEL is unset)
Default (no code, no env) warn

Allowed values: off (or none / silent), error, warn, info, debug, verbose.

Runtime capture enablement:

Source Effect
configureFuncxLogging({ enabled: false }) Disable runtime logging and in-memory capture (overrides mode=debug)
configureFuncxLogging({ enabled: true }) Enable runtime logging even when mode=prod
mode=debug (env) Enable when code does not set enabled
mode=prod (default) Disabled when code does not set enabled
import { configureFuncxLogging, getFuncxLogger, getRunTime } from "@x12i/funcx";

// Embed FuncX in a host app — silence FuncX logs regardless of env
configureFuncxLogging({ logLevel: "off" });

// Or keep capture but only show warnings/errors
configureFuncxLogging({ logLevel: "warn" });

// Enable structured debug logs in production for a support session
configureFuncxLogging({ enabled: true, logLevel: "debug", logFormat: "json" });

// Direct logger / runtime access (undefined when runtime logging is disabled)
const logger = getFuncxLogger();
logger?.infoCode("FUNCX_SERVER_STARTED", { port: 3780 });

// After configureFuncxLogging, use getRunTime() for the current capture state
configureFuncxLogging({ enabled: false });
// getRunTime() === undefined

Call configureFuncxLogging before importing modules that log (or call it anytime to hot-reload the logger; existing aiActivities are preserved). Diagnostic catalog codes live in metadata/log-diagnostics.json (shipped with the package).

Format: configureFuncxLogging({ logFormat: "json" }) or env FUNCX_LOG_FORMAT=json (default text).

Deep stacks (@x12i/logxer ≥ 4.5.0): When FuncX is embedded alongside other logxer packages, the host can control all package thresholds in one place:

import {
  applyPackageLogLevelsFromEnv,
  configurePackageLogLevels,
  type StackLoggingOptions,
} from "@x12i/logxer";
import { configureFuncxLogging, createFuncxLogger, FUNCX_LOG_ENV_PREFIX } from "@x12i/funcx";

// At startup (before heavy imports)
applyPackageLogLevelsFromEnv();
configurePackageLogLevels({
  default: "warn",
  levels: { [FUNCX_LOG_ENV_PREFIX]: "info" },
});

// Or pass-through when constructing FuncX surfaces
const logging: StackLoggingOptions = { packageLevels: { FUNCX: "debug" } };
configureFuncxLogging({ logging });

// Standalone logger factory (tests / custom wiring)
const log = createFuncxLogger({ logging });

Precedence for FuncX: configureFuncxLogging({ logLevel })logging / stackFUNCX_LOGS_LEVEL env → FUNCX_LOG_LEVEL legacy → host configurePackageLogLevels / LOGXER_PACKAGE_LEVELS → default warn.

Bulk env (host should call applyPackageLogLevelsFromEnv() at startup):

LOGXER_PACKAGE_LEVELS=FUNCX:debug,OTHER_PKG:off
LOGXER_PACKAGE_LOGS_DEFAULT=warn
Optional MongoDB activity persistence (Activix)

When Activix is enabled, the server initializes @x12i/activix 7.2+ in storageMode: database with autoCost (default on) so outer.cost is derived from usage in outer.output. FuncX billing enrichment uses @x12i/ai-profiles. Two collections are written (Mongo database from env — see ACTIVIX_DB_NAME, MONGO_AI_LOGS_DB, MONGO_LOGS_DB, MONGO_DB; default activitix):

Enablement (single resolution — no env/code collision):

Source Effect
configureActivix({ enabled: false }) Always off (overrides FUNCX_ACTIVIX_ENABLED=1)
configureActivix({ enabled: true }) On (requires Mongo URI from code or env; autoCost on by default)
configureActivix({ enabled: true, autoCost: false }) On without automatic outer.cost
configureActivix({ autoCost: { bundledOnly: true } }) On with offline catalog-only pricing
FUNCX_ACTIVIX_ENABLED=1 On when code config does not set enabled
Default Off
import { configureActivix } from "@x12i/funcx";

// Embed FuncX — disable Mongo persistence regardless of env
configureActivix({ enabled: false });

// Enable with custom URI and auto-cost (default when enabled)
configureActivix({
  enabled: true,
  mongoUri: process.env.MONGO_ACTIVIX_URI,
  autoCost: { bundledOnly: process.env.AI_TOOLS_BUNDLED_ONLY === "1" },
});
Collection What is logged
funcx-requests One row per high-level function execution (GET /activity mirror). runContext.sessionId = request traceId when present. Top-level metadata: functionId, projectId, traceId. outer.output.usage carries tokens/cost for Activix autoCost.
ai-actions One row per low-level OpenRouter ask / chat / stream. outer.output.response is the enriched AskResult; Activix autoCost reads usage/cost from it.

Document layout follows Activix v6+: root outer / optional inner, no nested structure. outer.cost is set by Activix on terminal writes — not duplicated in outer.metadata or top-level metadata.

Mongo connection string — first non-empty value wins:

  1. configureActivix({ mongoUri }) code override
  2. MONGO_ACTIVIX_URI
  3. MONGO_LOGS_URI
  4. MONGO_URI

If Activix is enabled but no URI is set, or Mongo is unreachable at startup, the process exits with an error (fail-fast). Runtime write failures are logged and do not fail API requests (fire-and-forget).

For Activix diagnostics only (not required for normal operation), see the @x12i/activix README (ENABLE_ACTIVIX_LOGXER, ACTIVIX_LOGS_LEVEL, etc.).


Analytics APIs

Proxy endpoints that fetch usage and cost data directly from the upstream provider. No data is stored by this server.

GET /models/available                 list models available via OpenRouter (x-openrouter-key or OPENROUTER_API_KEY)
GET /activity                         server-side activity log (query: from, to, functionId, projectId, model, limit) → { activities, summary }; entries may include valueEstimate when runs used estimateValue
GET /analytics/value                  aggregate human-value metrics from activity log (query: from, to, functionId, projectId, groupBy)
GET /analytics/openrouter/credits     account balance and total usage
GET /analytics/openrouter/generations generation records (query: dateMin, dateMax, model, userTag, limit)
GET /analytics/openai/usage            org usage buckets — requires OPENAI_ADMIN_KEY (query: startTime, endTime, groupBy, projectIds, models, limit)
GET /analytics/openai/costs            org cost buckets  — requires OPENAI_ADMIN_KEY (query: startTime, endTime, groupBy, projectIds, limit)

OpenRouter: uses x-openrouter-key header (BYOK) or OPENROUTER_API_KEY env.

OpenAI: requires OPENAI_ADMIN_KEY env var (an admin-scoped key from your OpenAI org settings). Standard project keys do not have access to organization-level analytics.

Filter generations by function or project:

GET /analytics/openrouter/generations?userTag=cognni-prod:extract.requirements&dateMin=2026-03-01

The userTag filter matches the attribution tag this package injects into the OpenRouter user field, so you can pull all generations for a specific function or project:function pair.


Server env vars
Var Default Description
PORT 3780 Server port
`LIGHT_SKILLS_API_K

Keywords