@metaobjectsdev/ai-runtime 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ import { type LlmRecorder, type LlmCallInput, type LlmCallRow, type RecordLlmCallResult } from "@metaobjectsdev/runtime-ts";
2
+ import { type Clock, type IdGen, type LlmClient, type LlmRequest, type LlmCompletion } from "./client.js";
3
+ import type { CostFn } from "./cost.js";
4
+ export interface RunLlmCallInput {
5
+ /** Discriminator / call identity. */
6
+ callType: string;
7
+ /** Already-rendered request sent to the client. */
8
+ request: LlmRequest;
9
+ /** Existing trace to attach to; a new trace id is generated when absent. */
10
+ traceId?: string;
11
+ parentSpanId?: string;
12
+ sessionId?: string;
13
+ }
14
+ export interface RunLlmCallDeps {
15
+ client: LlmClient;
16
+ cost?: CostFn;
17
+ clock?: Clock;
18
+ ids?: IdGen;
19
+ }
20
+ export interface RunLlmCallResult {
21
+ /** Ready-to-persist BASE recorder input (envelope + raw I/O + status). */
22
+ input: LlmCallInput;
23
+ /** Provider completion, or undefined if the client threw. */
24
+ completion?: LlmCompletion;
25
+ }
26
+ /**
27
+ * Perform the LLM CALL: generate ids, time it, call the client (never-throw),
28
+ * compute cost. Returns a ready base LlmCallInput + the completion. Does NOT
29
+ * persist and does NOT extract — callers do that (callLlm persists the base row;
30
+ * the generated call<Entity> additionally extracts + writes typed voRequest/voResponse).
31
+ */
32
+ export declare function runLlmCall(input: RunLlmCallInput, deps: RunLlmCallDeps): Promise<RunLlmCallResult>;
33
+ export interface CallLlmDeps extends RunLlmCallDeps {
34
+ recorder: LlmRecorder;
35
+ redact?: (row: LlmCallRow) => LlmCallRow;
36
+ }
37
+ /**
38
+ * Generic loop: CALL (runLlmCall) → persist the BASE row. No extract, no typed
39
+ * voRequest/voResponse (that's the generated call<Entity>). Finally-style: a
40
+ * client throw still persists an error row and never rethrows.
41
+ */
42
+ export declare function callLlm(input: RunLlmCallInput, deps: CallLlmDeps): Promise<RecordLlmCallResult>;
43
+ //# sourceMappingURL=call-loop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"call-loop.d.ts","sourceRoot":"","sources":["../src/call-loop.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAGL,KAAK,KAAK,EACV,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,MAAM,WAAW,eAAe;IAC9B,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,OAAO,EAAE,UAAU,CAAC;IACpB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,GAAG,CAAC,EAAE,KAAK,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,KAAK,EAAE,YAAY,CAAC;IACpB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,aAAa,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,gBAAgB,CAAC,CA8C3B;AAED,MAAM,WAAW,WAAY,SAAQ,cAAc;IACjD,QAAQ,EAAE,WAAW,CAAC;IACtB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,UAAU,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAC3B,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,WAAW,GAChB,OAAO,CAAC,mBAAmB,CAAC,CAQ9B"}
@@ -0,0 +1,71 @@
1
+ import { buildLlmCallRow, persistLlmCallRow, } from "@metaobjectsdev/runtime-ts";
2
+ import { systemClock, uuidIds, } from "./client.js";
3
+ /**
4
+ * Perform the LLM CALL: generate ids, time it, call the client (never-throw),
5
+ * compute cost. Returns a ready base LlmCallInput + the completion. Does NOT
6
+ * persist and does NOT extract — callers do that (callLlm persists the base row;
7
+ * the generated call<Entity> additionally extracts + writes typed voRequest/voResponse).
8
+ */
9
+ export async function runLlmCall(input, deps) {
10
+ const clock = deps.clock ?? systemClock;
11
+ const ids = deps.ids ?? uuidIds;
12
+ const spanId = ids.next();
13
+ const traceId = input.traceId ?? ids.next();
14
+ const t0 = clock.now();
15
+ const startedAt = new Date(t0).toISOString();
16
+ let completion;
17
+ let status = "ok";
18
+ let errorDetail = null;
19
+ try {
20
+ completion = await deps.client.complete(input.request);
21
+ }
22
+ catch (err) {
23
+ status = "error";
24
+ errorDetail = err instanceof Error ? err.message : String(err);
25
+ }
26
+ const latencyMs = clock.now() - t0;
27
+ // exactOptionalPropertyTypes: assign optionals only when defined.
28
+ const recInput = {
29
+ spanId,
30
+ traceId,
31
+ callType: input.callType,
32
+ startedAt,
33
+ latencyMs,
34
+ requestModel: input.request.model,
35
+ llmRequest: completion?.request ?? input.request,
36
+ llmResponseText: completion?.body ?? "",
37
+ status,
38
+ errorDetail,
39
+ };
40
+ if (input.request.system !== undefined)
41
+ recInput.system = input.request.system;
42
+ if (input.parentSpanId !== undefined)
43
+ recInput.parentSpanId = input.parentSpanId;
44
+ if (input.sessionId !== undefined)
45
+ recInput.sessionId = input.sessionId;
46
+ if (completion?.model !== undefined)
47
+ recInput.responseModel = completion.model;
48
+ if (completion?.usage?.inputTokens !== undefined)
49
+ recInput.inputTokens = completion.usage.inputTokens;
50
+ if (completion?.usage?.outputTokens !== undefined)
51
+ recInput.outputTokens = completion.usage.outputTokens;
52
+ if (completion?.finishReason !== undefined)
53
+ recInput.finishReason = completion.finishReason;
54
+ if (completion !== undefined && deps.cost !== undefined) {
55
+ const c = deps.cost(completion.model ?? input.request.model, completion.usage);
56
+ if (c !== null)
57
+ recInput.costMinor = c;
58
+ }
59
+ return completion !== undefined ? { input: recInput, completion } : { input: recInput };
60
+ }
61
+ /**
62
+ * Generic loop: CALL (runLlmCall) → persist the BASE row. No extract, no typed
63
+ * voRequest/voResponse (that's the generated call<Entity>). Finally-style: a
64
+ * client throw still persists an error row and never rethrows.
65
+ */
66
+ export async function callLlm(input, deps) {
67
+ const { input: recInput } = await runLlmCall(input, deps);
68
+ await persistLlmCallRow(deps.recorder, buildLlmCallRow(recInput), deps.redact ? { redact: deps.redact } : undefined);
69
+ return { status: recInput.status, errorDetail: recInput.errorDetail };
70
+ }
71
+ //# sourceMappingURL=call-loop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"call-loop.js","sourceRoot":"","sources":["../src/call-loop.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ export interface LlmRequest {
2
+ /** The rendered prompt text (the GENERATE step's output). */
3
+ prompt: string;
4
+ /** gen_ai.request.model */
5
+ model: string;
6
+ /** Optional system prompt text (gen_ai.system / system message). */
7
+ system?: string;
8
+ /** Provider params: temperature, max_tokens, top_p, ... */
9
+ params?: Record<string, unknown>;
10
+ }
11
+ export interface LlmUsage {
12
+ /** gen_ai.usage.input_tokens */
13
+ inputTokens?: number;
14
+ /** gen_ai.usage.output_tokens */
15
+ outputTokens?: number;
16
+ }
17
+ export interface LlmCompletion {
18
+ /** Raw completion text — fed to extract/record. */
19
+ body: string;
20
+ usage?: LlmUsage;
21
+ /** gen_ai.response.model (may differ from request.model). */
22
+ model?: string;
23
+ /** Full wire request → llmRequest column (falls back to LlmRequest). */
24
+ request?: unknown;
25
+ /** gen_ai.response.finish_reasons */
26
+ finishReason?: string;
27
+ }
28
+ /** The single-method, provider-neutral CALL seam. */
29
+ export interface LlmClient {
30
+ complete(req: LlmRequest): Promise<LlmCompletion>;
31
+ }
32
+ /** Injectable wall clock (ms since epoch). Injected so tests are deterministic. */
33
+ export interface Clock {
34
+ now(): number;
35
+ }
36
+ /** Injectable id generator (span/trace ids). Injected so tests are deterministic. */
37
+ export interface IdGen {
38
+ next(): string;
39
+ }
40
+ export declare const systemClock: Clock;
41
+ export declare const uuidIds: IdGen;
42
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,UAAU;IACzB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,QAAQ;IACvB,gCAAgC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qDAAqD;AACrD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CACnD;AAED,mFAAmF;AACnF,MAAM,WAAW,KAAK;IACpB,GAAG,IAAI,MAAM,CAAC;CACf;AAED,qFAAqF;AACrF,MAAM,WAAW,KAAK;IACpB,IAAI,IAAI,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,WAAW,EAAE,KAEzB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,KAErB,CAAC"}
package/dist/client.js ADDED
@@ -0,0 +1,9 @@
1
+ // The CALL seam. Named LlmClient to avoid collision with render's Provider
2
+ // (which resolves prompt-TEXT references, not LLM calls).
3
+ export const systemClock = {
4
+ now: () => Date.now(),
5
+ };
6
+ export const uuidIds = {
7
+ next: () => crypto.randomUUID(),
8
+ };
9
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,0DAA0D;AA+C1D,MAAM,CAAC,MAAM,WAAW,GAAU;IAChC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;CACtB,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAAU;IAC5B,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE;CAChC,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { LlmRecorder, LlmCallRow } from "@metaobjectsdev/runtime-ts";
2
+ export interface CompositeRecorderOpts {
3
+ /** Called once per sink that rejects. Default: swallow. Telemetry must never
4
+ * break the call path, so record() always resolves. */
5
+ onError?: (error: unknown, index: number) => void;
6
+ }
7
+ /** Fans a row out to several sinks; a sink that rejects is isolated. */
8
+ export declare class CompositeRecorder implements LlmRecorder {
9
+ private readonly recorders;
10
+ private readonly onError;
11
+ constructor(recorders: readonly LlmRecorder[], opts?: CompositeRecorderOpts);
12
+ record(call: LlmCallRow): Promise<void>;
13
+ }
14
+ //# sourceMappingURL=composite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../src/composite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAE1E,MAAM,WAAW,qBAAqB;IACpC;2DACuD;IACvD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnD;AAED,wEAAwE;AACxE,qBAAa,iBAAkB,YAAW,WAAW;IACnD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IACnD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0C;gBAEtD,SAAS,EAAE,SAAS,WAAW,EAAE,EAAE,IAAI,CAAC,EAAE,qBAAqB;IAKrE,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;CAQ9C"}
@@ -0,0 +1,17 @@
1
+ /** Fans a row out to several sinks; a sink that rejects is isolated. */
2
+ export class CompositeRecorder {
3
+ recorders;
4
+ onError;
5
+ constructor(recorders, opts) {
6
+ this.recorders = recorders;
7
+ this.onError = opts?.onError ?? (() => { });
8
+ }
9
+ async record(call) {
10
+ const results = await Promise.allSettled(this.recorders.map((r) => r.record(call)));
11
+ results.forEach((res, i) => {
12
+ if (res.status === "rejected")
13
+ this.onError(res.reason, i);
14
+ });
15
+ }
16
+ }
17
+ //# sourceMappingURL=composite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composite.js","sourceRoot":"","sources":["../src/composite.ts"],"names":[],"mappings":"AAQA,wEAAwE;AACxE,MAAM,OAAO,iBAAiB;IACX,SAAS,CAAyB;IAClC,OAAO,CAA0C;IAElE,YAAY,SAAiC,EAAE,IAA4B;QACzE,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAgB;QAC3B,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CACtC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAC1C,CAAC;QACF,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;YACzB,IAAI,GAAG,CAAC,MAAM,KAAK,UAAU;gBAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAC7D,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
package/dist/cost.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { LlmUsage } from "./client.js";
2
+ /**
3
+ * Maps (model, usage) to a cost in integer USD minor units (cents), per the
4
+ * field.currency wire contract. Returns null when unknown — never throws. The
5
+ * library ships NO rate table (ADR-0024); adopters supply their own CostFn
6
+ * (from their LLM library's usage + their own rates) via `deps.cost`.
7
+ */
8
+ export type CostFn = (model: string, usage: LlmUsage | undefined) => number | null;
9
+ //# sourceMappingURL=cost.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cost.d.ts","sourceRoot":"","sources":["../src/cost.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,SAAS,KAAK,MAAM,GAAG,IAAI,CAAC"}
package/dist/cost.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cost.js","sourceRoot":"","sources":["../src/cost.ts"],"names":[],"mappings":""}
@@ -0,0 +1,6 @@
1
+ export declare const AI_RUNTIME_PACKAGE = "@metaobjectsdev/ai-runtime";
2
+ export { systemClock, uuidIds, type LlmClient, type LlmRequest, type LlmCompletion, type LlmUsage, type Clock, type IdGen, } from "./client.js";
3
+ export type { CostFn } from "./cost.js";
4
+ export { callLlm, runLlmCall, type CallLlmDeps, type RunLlmCallInput, type RunLlmCallDeps, type RunLlmCallResult, } from "./call-loop.js";
5
+ export { CompositeRecorder, type CompositeRecorderOpts } from "./composite.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,kBAAkB,+BAA+B,CAAC;AAE/D,OAAO,EACL,WAAW,EACX,OAAO,EACP,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,KAAK,EACV,KAAK,KAAK,GACX,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,EACL,OAAO,EACP,UAAU,EACV,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,KAAK,qBAAqB,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // @metaobjectsdev/ai-runtime — LLM call loop + typed-trace recorder adapters.
2
+ export const AI_RUNTIME_PACKAGE = "@metaobjectsdev/ai-runtime";
3
+ export { systemClock, uuidIds, } from "./client.js";
4
+ export { callLlm, runLlmCall, } from "./call-loop.js";
5
+ export { CompositeRecorder } from "./composite.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,MAAM,CAAC,MAAM,kBAAkB,GAAG,4BAA4B,CAAC;AAE/D,OAAO,EACL,WAAW,EACX,OAAO,GAOR,MAAM,aAAa,CAAC;AAIrB,OAAO,EACL,OAAO,EACP,UAAU,GAKX,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAA8B,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { LlmRecorder, LlmCallRow } from "@metaobjectsdev/runtime-ts";
2
+ /** The shape we post to Langfuse — a generation/observation. */
3
+ export interface LangfuseTracePayload {
4
+ id: string;
5
+ traceId: string;
6
+ name: string;
7
+ model?: string;
8
+ input?: unknown;
9
+ output?: unknown;
10
+ usage?: {
11
+ input?: number;
12
+ output?: number;
13
+ };
14
+ metadata?: Record<string, unknown>;
15
+ }
16
+ /** Injected sink — implemented over the real langfuse SDK by the adopter, or a
17
+ * fake in tests. Keeps the langfuse SDK an optional dep (never imported here). */
18
+ export interface LangfuseSink {
19
+ trace(payload: LangfuseTracePayload): Promise<void> | void;
20
+ }
21
+ export interface LangfuseRecorderOpts {
22
+ sink: LangfuseSink;
23
+ /** Called when the sink rejects. Default: swallow (telemetry never breaks the call). */
24
+ onError?: (error: unknown) => void;
25
+ }
26
+ export declare class LangfuseRecorder implements LlmRecorder {
27
+ private readonly sink;
28
+ private readonly onError;
29
+ constructor(opts: LangfuseRecorderOpts);
30
+ record(call: LlmCallRow): Promise<void>;
31
+ }
32
+ //# sourceMappingURL=langfuse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"langfuse.d.ts","sourceRoot":"","sources":["../src/langfuse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAE1E,gEAAgE;AAChE,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;kFACkF;AAClF,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC5D;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,YAAY,CAAC;IACnB,wFAAwF;IACxF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAOD,qBAAa,gBAAiB,YAAW,WAAW;IAClD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;gBAEvC,IAAI,EAAE,oBAAoB;IAKhC,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;CAwC9C"}
@@ -0,0 +1,52 @@
1
+ const numOrUndef = (v) => typeof v === "number" ? v : undefined;
2
+ const strOrUndef = (v) => typeof v === "string" ? v : undefined;
3
+ export class LangfuseRecorder {
4
+ sink;
5
+ onError;
6
+ constructor(opts) {
7
+ this.sink = opts.sink;
8
+ this.onError = opts.onError ?? (() => { });
9
+ }
10
+ async record(call) {
11
+ // Build the payload incrementally to satisfy exactOptionalPropertyTypes:
12
+ // never assign `T | undefined` to an optional `T?` property.
13
+ const payload = {
14
+ id: String(call["spanId"] ?? ""),
15
+ traceId: String(call["traceId"] ?? ""),
16
+ name: String(call["callType"] ?? "llm-call"),
17
+ metadata: {
18
+ status: call["status"],
19
+ finishReason: call["finishReason"],
20
+ latencyMs: call["latencyMs"],
21
+ costMinor: call["costMinor"],
22
+ },
23
+ };
24
+ // Optional scalar fields — only set when defined.
25
+ const model = strOrUndef(call["requestModel"]);
26
+ if (model !== undefined)
27
+ payload.model = model;
28
+ if (call["llmRequest"] !== undefined)
29
+ payload.input = call["llmRequest"];
30
+ const output = call["voResponse"] ?? call["llmResponse"];
31
+ if (output !== undefined)
32
+ payload.output = output;
33
+ // usage — only populate keys that have a value.
34
+ const inTok = numOrUndef(call["inputTokens"]);
35
+ const outTok = numOrUndef(call["outputTokens"]);
36
+ if (inTok !== undefined || outTok !== undefined) {
37
+ const usage = {};
38
+ if (inTok !== undefined)
39
+ usage.input = inTok;
40
+ if (outTok !== undefined)
41
+ usage.output = outTok;
42
+ payload.usage = usage;
43
+ }
44
+ try {
45
+ await this.sink.trace(payload);
46
+ }
47
+ catch (err) {
48
+ this.onError(err);
49
+ }
50
+ }
51
+ }
52
+ //# sourceMappingURL=langfuse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"langfuse.js","sourceRoot":"","sources":["../src/langfuse.ts"],"names":[],"mappings":"AA0BA,MAAM,UAAU,GAAG,CAAC,CAAU,EAAsB,EAAE,CACpD,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACxC,MAAM,UAAU,GAAG,CAAC,CAAU,EAAsB,EAAE,CACpD,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAExC,MAAM,OAAO,gBAAgB;IACV,IAAI,CAAe;IACnB,OAAO,CAA2B;IAEnD,YAAY,IAA0B;QACpC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAgB;QAC3B,yEAAyE;QACzE,6DAA6D;QAC7D,MAAM,OAAO,GAAyB;YACpC,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YAChC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;YACtC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC;YAC5C,QAAQ,EAAE;gBACR,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC;gBACtB,YAAY,EAAE,IAAI,CAAC,cAAc,CAAC;gBAClC,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC;gBAC5B,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC;aAC7B;SACF,CAAC;QAEF,kDAAkD;QAClD,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;QAC/C,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;QAE/C,IAAI,IAAI,CAAC,YAAY,CAAC,KAAK,SAAS;YAAE,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC;QAEzE,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,IAAI,CAAC,aAAa,CAAC,CAAC;QACzD,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC;QAElD,gDAAgD;QAChD,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;QAChD,IAAI,KAAK,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAChD,MAAM,KAAK,GAAwC,EAAE,CAAC;YACtD,IAAI,KAAK,KAAK,SAAS;gBAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;YAC7C,IAAI,MAAM,KAAK,SAAS;gBAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC;YAChD,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;QACxB,CAAC;QAED,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;CACF"}
package/dist/otel.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { LlmRecorder, LlmCallRow } from "@metaobjectsdev/runtime-ts";
2
+ /** Minimal structural subset of an OTel span. Avoids a hard @opentelemetry/api dep. */
3
+ export interface OtelSpan {
4
+ setAttributes(attrs: Record<string, unknown>): void;
5
+ end(): void;
6
+ }
7
+ /** Minimal structural subset of an OTel tracer. */
8
+ export interface OtelTracer {
9
+ startSpan(name: string): OtelSpan;
10
+ }
11
+ export interface OtelRecorderOpts {
12
+ tracer: OtelTracer;
13
+ onError?: (error: unknown) => void;
14
+ }
15
+ /** Maps a trace row → a span with gen_ai.* attributes. The internal→gen_ai.*
16
+ * mapping lives here (stable internal names, canonicalize at the edge). */
17
+ export declare class OtelRecorder implements LlmRecorder {
18
+ private readonly tracer;
19
+ private readonly onError;
20
+ constructor(opts: OtelRecorderOpts);
21
+ record(call: LlmCallRow): Promise<void>;
22
+ }
23
+ //# sourceMappingURL=otel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otel.d.ts","sourceRoot":"","sources":["../src/otel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAE1E,uFAAuF;AACvF,MAAM,WAAW,QAAQ;IACvB,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACpD,GAAG,IAAI,IAAI,CAAC;CACb;AAED,mDAAmD;AACnD,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC;CACnC;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAED;2EAC2E;AAC3E,qBAAa,YAAa,YAAW,WAAW;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;gBAEvC,IAAI,EAAE,gBAAgB;IAK5B,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;CAsB9C"}
package/dist/otel.js ADDED
@@ -0,0 +1,35 @@
1
+ /** Maps a trace row → a span with gen_ai.* attributes. The internal→gen_ai.*
2
+ * mapping lives here (stable internal names, canonicalize at the edge). */
3
+ export class OtelRecorder {
4
+ tracer;
5
+ onError;
6
+ constructor(opts) {
7
+ this.tracer = opts.tracer;
8
+ this.onError = opts.onError ?? (() => { });
9
+ }
10
+ async record(call) {
11
+ try {
12
+ const span = this.tracer.startSpan(String(call.callType ?? "llm-call"));
13
+ const attrs = {};
14
+ const put = (key, v) => {
15
+ if (v !== undefined && v !== null)
16
+ attrs[key] = v;
17
+ };
18
+ put("gen_ai.system", call.system);
19
+ put("gen_ai.request.model", call.requestModel);
20
+ put("gen_ai.response.model", call.responseModel);
21
+ put("gen_ai.usage.input_tokens", call.inputTokens);
22
+ put("gen_ai.usage.output_tokens", call.outputTokens);
23
+ put("gen_ai.response.finish_reasons", call.finishReason);
24
+ put("metaobjects.trace_id", call.traceId);
25
+ put("metaobjects.span_id", call.spanId);
26
+ put("metaobjects.status", call.status);
27
+ span.setAttributes(attrs);
28
+ span.end();
29
+ }
30
+ catch (err) {
31
+ this.onError(err);
32
+ }
33
+ }
34
+ }
35
+ //# sourceMappingURL=otel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otel.js","sourceRoot":"","sources":["../src/otel.ts"],"names":[],"mappings":"AAkBA;2EAC2E;AAC3E,MAAM,OAAO,YAAY;IACN,MAAM,CAAa;IACnB,OAAO,CAA2B;IAEnD,YAAY,IAAsB;QAChC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAgB;QAC3B,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,IAAI,UAAU,CAAC,CAAC,CAAC;YACxE,MAAM,KAAK,GAA4B,EAAE,CAAC;YAC1C,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,CAAU,EAAE,EAAE;gBACtC,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI;oBAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACpD,CAAC,CAAC;YACF,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YAClC,GAAG,CAAC,sBAAsB,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;YAC/C,GAAG,CAAC,uBAAuB,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;YACjD,GAAG,CAAC,2BAA2B,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;YACnD,GAAG,CAAC,4BAA4B,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;YACrD,GAAG,CAAC,gCAAgC,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;YACzD,GAAG,CAAC,sBAAsB,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;YAC1C,GAAG,CAAC,qBAAqB,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YACxC,GAAG,CAAC,oBAAoB,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YACvC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC1B,IAAI,CAAC,GAAG,EAAE,CAAC;QACb,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@metaobjectsdev/ai-runtime",
3
+ "version": "0.10.0",
4
+ "description": "LLM call loop + typed-trace recorder adapters for MetaObjects: provider-neutral LlmClient seam, callLlm bridge, Composite/Langfuse/OTel recorders.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "bun": "./src/index.ts",
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./langfuse": {
15
+ "bun": "./src/langfuse.ts",
16
+ "types": "./dist/langfuse.d.ts",
17
+ "default": "./dist/langfuse.js"
18
+ },
19
+ "./otel": {
20
+ "bun": "./src/otel.ts",
21
+ "types": "./dist/otel.d.ts",
22
+ "default": "./dist/otel.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsc -p .",
33
+ "typecheck": "tsc -p tsconfig.typecheck.json"
34
+ },
35
+ "license": "Apache-2.0",
36
+ "author": "Doug Mealing <doug@dougmealing.com>",
37
+ "homepage": "https://metaobjects.dev",
38
+ "bugs": {
39
+ "url": "https://github.com/metaobjectsdev/metaobjects/issues"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/metaobjectsdev/metaobjects.git",
44
+ "directory": "server/typescript/packages/ai-runtime"
45
+ },
46
+ "keywords": [
47
+ "metaobjects",
48
+ "llm",
49
+ "ai",
50
+ "trace",
51
+ "langfuse",
52
+ "opentelemetry"
53
+ ],
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "dependencies": {
58
+ "@metaobjectsdev/metadata": "0.10.0",
59
+ "@metaobjectsdev/render": "0.10.0",
60
+ "@metaobjectsdev/runtime-ts": "0.10.0"
61
+ },
62
+ "peerDependencies": {
63
+ "langfuse": ">=3.0.0",
64
+ "@opentelemetry/api": ">=1.0.0"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "langfuse": {
68
+ "optional": true
69
+ },
70
+ "@opentelemetry/api": {
71
+ "optional": true
72
+ }
73
+ },
74
+ "devDependencies": {
75
+ "bun-types": "latest",
76
+ "typescript": "^5.6.0"
77
+ }
78
+ }
@@ -0,0 +1,123 @@
1
+ import {
2
+ buildLlmCallRow,
3
+ persistLlmCallRow,
4
+ type LlmRecorder,
5
+ type LlmCallInput,
6
+ type LlmCallRow,
7
+ type RecordLlmCallResult,
8
+ } from "@metaobjectsdev/runtime-ts";
9
+ import {
10
+ systemClock,
11
+ uuidIds,
12
+ type Clock,
13
+ type IdGen,
14
+ type LlmClient,
15
+ type LlmRequest,
16
+ type LlmCompletion,
17
+ } from "./client.js";
18
+ import type { CostFn } from "./cost.js";
19
+
20
+ export interface RunLlmCallInput {
21
+ /** Discriminator / call identity. */
22
+ callType: string;
23
+ /** Already-rendered request sent to the client. */
24
+ request: LlmRequest;
25
+ /** Existing trace to attach to; a new trace id is generated when absent. */
26
+ traceId?: string;
27
+ parentSpanId?: string;
28
+ sessionId?: string;
29
+ }
30
+
31
+ export interface RunLlmCallDeps {
32
+ client: LlmClient;
33
+ cost?: CostFn;
34
+ clock?: Clock;
35
+ ids?: IdGen;
36
+ }
37
+
38
+ export interface RunLlmCallResult {
39
+ /** Ready-to-persist BASE recorder input (envelope + raw I/O + status). */
40
+ input: LlmCallInput;
41
+ /** Provider completion, or undefined if the client threw. */
42
+ completion?: LlmCompletion;
43
+ }
44
+
45
+ /**
46
+ * Perform the LLM CALL: generate ids, time it, call the client (never-throw),
47
+ * compute cost. Returns a ready base LlmCallInput + the completion. Does NOT
48
+ * persist and does NOT extract — callers do that (callLlm persists the base row;
49
+ * the generated call<Entity> additionally extracts + writes typed voRequest/voResponse).
50
+ */
51
+ export async function runLlmCall(
52
+ input: RunLlmCallInput,
53
+ deps: RunLlmCallDeps,
54
+ ): Promise<RunLlmCallResult> {
55
+ const clock = deps.clock ?? systemClock;
56
+ const ids = deps.ids ?? uuidIds;
57
+
58
+ const spanId = ids.next();
59
+ const traceId = input.traceId ?? ids.next();
60
+ const t0 = clock.now();
61
+ const startedAt = new Date(t0).toISOString();
62
+
63
+ let completion: LlmCompletion | undefined;
64
+ let status: "ok" | "error" = "ok";
65
+ let errorDetail: string | null = null;
66
+ try {
67
+ completion = await deps.client.complete(input.request);
68
+ } catch (err) {
69
+ status = "error";
70
+ errorDetail = err instanceof Error ? err.message : String(err);
71
+ }
72
+ const latencyMs = clock.now() - t0;
73
+
74
+ // exactOptionalPropertyTypes: assign optionals only when defined.
75
+ const recInput: LlmCallInput = {
76
+ spanId,
77
+ traceId,
78
+ callType: input.callType,
79
+ startedAt,
80
+ latencyMs,
81
+ requestModel: input.request.model,
82
+ llmRequest: completion?.request ?? input.request,
83
+ llmResponseText: completion?.body ?? "",
84
+ status,
85
+ errorDetail,
86
+ };
87
+ if (input.request.system !== undefined) recInput.system = input.request.system;
88
+ if (input.parentSpanId !== undefined) recInput.parentSpanId = input.parentSpanId;
89
+ if (input.sessionId !== undefined) recInput.sessionId = input.sessionId;
90
+ if (completion?.model !== undefined) recInput.responseModel = completion.model;
91
+ if (completion?.usage?.inputTokens !== undefined) recInput.inputTokens = completion.usage.inputTokens;
92
+ if (completion?.usage?.outputTokens !== undefined) recInput.outputTokens = completion.usage.outputTokens;
93
+ if (completion?.finishReason !== undefined) recInput.finishReason = completion.finishReason;
94
+ if (completion !== undefined && deps.cost !== undefined) {
95
+ const c = deps.cost(completion.model ?? input.request.model, completion.usage);
96
+ if (c !== null) recInput.costMinor = c;
97
+ }
98
+
99
+ return completion !== undefined ? { input: recInput, completion } : { input: recInput };
100
+ }
101
+
102
+ export interface CallLlmDeps extends RunLlmCallDeps {
103
+ recorder: LlmRecorder;
104
+ redact?: (row: LlmCallRow) => LlmCallRow;
105
+ }
106
+
107
+ /**
108
+ * Generic loop: CALL (runLlmCall) → persist the BASE row. No extract, no typed
109
+ * voRequest/voResponse (that's the generated call<Entity>). Finally-style: a
110
+ * client throw still persists an error row and never rethrows.
111
+ */
112
+ export async function callLlm(
113
+ input: RunLlmCallInput,
114
+ deps: CallLlmDeps,
115
+ ): Promise<RecordLlmCallResult> {
116
+ const { input: recInput } = await runLlmCall(input, deps);
117
+ await persistLlmCallRow(
118
+ deps.recorder,
119
+ buildLlmCallRow(recInput),
120
+ deps.redact ? { redact: deps.redact } : undefined,
121
+ );
122
+ return { status: recInput.status, errorDetail: recInput.errorDetail };
123
+ }
package/src/client.ts ADDED
@@ -0,0 +1,55 @@
1
+ // The CALL seam. Named LlmClient to avoid collision with render's Provider
2
+ // (which resolves prompt-TEXT references, not LLM calls).
3
+
4
+ export interface LlmRequest {
5
+ /** The rendered prompt text (the GENERATE step's output). */
6
+ prompt: string;
7
+ /** gen_ai.request.model */
8
+ model: string;
9
+ /** Optional system prompt text (gen_ai.system / system message). */
10
+ system?: string;
11
+ /** Provider params: temperature, max_tokens, top_p, ... */
12
+ params?: Record<string, unknown>;
13
+ }
14
+
15
+ export interface LlmUsage {
16
+ /** gen_ai.usage.input_tokens */
17
+ inputTokens?: number;
18
+ /** gen_ai.usage.output_tokens */
19
+ outputTokens?: number;
20
+ }
21
+
22
+ export interface LlmCompletion {
23
+ /** Raw completion text — fed to extract/record. */
24
+ body: string;
25
+ usage?: LlmUsage;
26
+ /** gen_ai.response.model (may differ from request.model). */
27
+ model?: string;
28
+ /** Full wire request → llmRequest column (falls back to LlmRequest). */
29
+ request?: unknown;
30
+ /** gen_ai.response.finish_reasons */
31
+ finishReason?: string;
32
+ }
33
+
34
+ /** The single-method, provider-neutral CALL seam. */
35
+ export interface LlmClient {
36
+ complete(req: LlmRequest): Promise<LlmCompletion>;
37
+ }
38
+
39
+ /** Injectable wall clock (ms since epoch). Injected so tests are deterministic. */
40
+ export interface Clock {
41
+ now(): number;
42
+ }
43
+
44
+ /** Injectable id generator (span/trace ids). Injected so tests are deterministic. */
45
+ export interface IdGen {
46
+ next(): string;
47
+ }
48
+
49
+ export const systemClock: Clock = {
50
+ now: () => Date.now(),
51
+ };
52
+
53
+ export const uuidIds: IdGen = {
54
+ next: () => crypto.randomUUID(),
55
+ };
@@ -0,0 +1,27 @@
1
+ import type { LlmRecorder, LlmCallRow } from "@metaobjectsdev/runtime-ts";
2
+
3
+ export interface CompositeRecorderOpts {
4
+ /** Called once per sink that rejects. Default: swallow. Telemetry must never
5
+ * break the call path, so record() always resolves. */
6
+ onError?: (error: unknown, index: number) => void;
7
+ }
8
+
9
+ /** Fans a row out to several sinks; a sink that rejects is isolated. */
10
+ export class CompositeRecorder implements LlmRecorder {
11
+ private readonly recorders: readonly LlmRecorder[];
12
+ private readonly onError: (error: unknown, index: number) => void;
13
+
14
+ constructor(recorders: readonly LlmRecorder[], opts?: CompositeRecorderOpts) {
15
+ this.recorders = recorders;
16
+ this.onError = opts?.onError ?? (() => {});
17
+ }
18
+
19
+ async record(call: LlmCallRow): Promise<void> {
20
+ const results = await Promise.allSettled(
21
+ this.recorders.map((r) => r.record(call)),
22
+ );
23
+ results.forEach((res, i) => {
24
+ if (res.status === "rejected") this.onError(res.reason, i);
25
+ });
26
+ }
27
+ }
package/src/cost.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { LlmUsage } from "./client.js";
2
+
3
+ /**
4
+ * Maps (model, usage) to a cost in integer USD minor units (cents), per the
5
+ * field.currency wire contract. Returns null when unknown — never throws. The
6
+ * library ships NO rate table (ADR-0024); adopters supply their own CostFn
7
+ * (from their LLM library's usage + their own rates) via `deps.cost`.
8
+ */
9
+ export type CostFn = (model: string, usage: LlmUsage | undefined) => number | null;
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ // @metaobjectsdev/ai-runtime — LLM call loop + typed-trace recorder adapters.
2
+ export const AI_RUNTIME_PACKAGE = "@metaobjectsdev/ai-runtime";
3
+
4
+ export {
5
+ systemClock,
6
+ uuidIds,
7
+ type LlmClient,
8
+ type LlmRequest,
9
+ type LlmCompletion,
10
+ type LlmUsage,
11
+ type Clock,
12
+ type IdGen,
13
+ } from "./client.js";
14
+
15
+ export type { CostFn } from "./cost.js";
16
+
17
+ export {
18
+ callLlm,
19
+ runLlmCall,
20
+ type CallLlmDeps,
21
+ type RunLlmCallInput,
22
+ type RunLlmCallDeps,
23
+ type RunLlmCallResult,
24
+ } from "./call-loop.js";
25
+
26
+ export { CompositeRecorder, type CompositeRecorderOpts } from "./composite.js";
@@ -0,0 +1,81 @@
1
+ import type { LlmRecorder, LlmCallRow } from "@metaobjectsdev/runtime-ts";
2
+
3
+ /** The shape we post to Langfuse — a generation/observation. */
4
+ export interface LangfuseTracePayload {
5
+ id: string;
6
+ traceId: string;
7
+ name: string;
8
+ model?: string;
9
+ input?: unknown;
10
+ output?: unknown;
11
+ usage?: { input?: number; output?: number };
12
+ metadata?: Record<string, unknown>;
13
+ }
14
+
15
+ /** Injected sink — implemented over the real langfuse SDK by the adopter, or a
16
+ * fake in tests. Keeps the langfuse SDK an optional dep (never imported here). */
17
+ export interface LangfuseSink {
18
+ trace(payload: LangfuseTracePayload): Promise<void> | void;
19
+ }
20
+
21
+ export interface LangfuseRecorderOpts {
22
+ sink: LangfuseSink;
23
+ /** Called when the sink rejects. Default: swallow (telemetry never breaks the call). */
24
+ onError?: (error: unknown) => void;
25
+ }
26
+
27
+ const numOrUndef = (v: unknown): number | undefined =>
28
+ typeof v === "number" ? v : undefined;
29
+ const strOrUndef = (v: unknown): string | undefined =>
30
+ typeof v === "string" ? v : undefined;
31
+
32
+ export class LangfuseRecorder implements LlmRecorder {
33
+ private readonly sink: LangfuseSink;
34
+ private readonly onError: (error: unknown) => void;
35
+
36
+ constructor(opts: LangfuseRecorderOpts) {
37
+ this.sink = opts.sink;
38
+ this.onError = opts.onError ?? (() => {});
39
+ }
40
+
41
+ async record(call: LlmCallRow): Promise<void> {
42
+ // Build the payload incrementally to satisfy exactOptionalPropertyTypes:
43
+ // never assign `T | undefined` to an optional `T?` property.
44
+ const payload: LangfuseTracePayload = {
45
+ id: String(call["spanId"] ?? ""),
46
+ traceId: String(call["traceId"] ?? ""),
47
+ name: String(call["callType"] ?? "llm-call"),
48
+ metadata: {
49
+ status: call["status"],
50
+ finishReason: call["finishReason"],
51
+ latencyMs: call["latencyMs"],
52
+ costMinor: call["costMinor"],
53
+ },
54
+ };
55
+
56
+ // Optional scalar fields — only set when defined.
57
+ const model = strOrUndef(call["requestModel"]);
58
+ if (model !== undefined) payload.model = model;
59
+
60
+ if (call["llmRequest"] !== undefined) payload.input = call["llmRequest"];
61
+
62
+ const output = call["voResponse"] ?? call["llmResponse"];
63
+ if (output !== undefined) payload.output = output;
64
+
65
+ // usage — only populate keys that have a value.
66
+ const inTok = numOrUndef(call["inputTokens"]);
67
+ const outTok = numOrUndef(call["outputTokens"]);
68
+ if (inTok !== undefined || outTok !== undefined) {
69
+ const usage: { input?: number; output?: number } = {};
70
+ if (inTok !== undefined) usage.input = inTok;
71
+ if (outTok !== undefined) usage.output = outTok;
72
+ payload.usage = usage;
73
+ }
74
+
75
+ try {
76
+ await this.sink.trace(payload);
77
+ } catch (err) {
78
+ this.onError(err);
79
+ }
80
+ }
81
+ }
package/src/otel.ts ADDED
@@ -0,0 +1,52 @@
1
+ import type { LlmRecorder, LlmCallRow } from "@metaobjectsdev/runtime-ts";
2
+
3
+ /** Minimal structural subset of an OTel span. Avoids a hard @opentelemetry/api dep. */
4
+ export interface OtelSpan {
5
+ setAttributes(attrs: Record<string, unknown>): void;
6
+ end(): void;
7
+ }
8
+
9
+ /** Minimal structural subset of an OTel tracer. */
10
+ export interface OtelTracer {
11
+ startSpan(name: string): OtelSpan;
12
+ }
13
+
14
+ export interface OtelRecorderOpts {
15
+ tracer: OtelTracer;
16
+ onError?: (error: unknown) => void;
17
+ }
18
+
19
+ /** Maps a trace row → a span with gen_ai.* attributes. The internal→gen_ai.*
20
+ * mapping lives here (stable internal names, canonicalize at the edge). */
21
+ export class OtelRecorder implements LlmRecorder {
22
+ private readonly tracer: OtelTracer;
23
+ private readonly onError: (error: unknown) => void;
24
+
25
+ constructor(opts: OtelRecorderOpts) {
26
+ this.tracer = opts.tracer;
27
+ this.onError = opts.onError ?? (() => {});
28
+ }
29
+
30
+ async record(call: LlmCallRow): Promise<void> {
31
+ try {
32
+ const span = this.tracer.startSpan(String(call.callType ?? "llm-call"));
33
+ const attrs: Record<string, unknown> = {};
34
+ const put = (key: string, v: unknown) => {
35
+ if (v !== undefined && v !== null) attrs[key] = v;
36
+ };
37
+ put("gen_ai.system", call.system);
38
+ put("gen_ai.request.model", call.requestModel);
39
+ put("gen_ai.response.model", call.responseModel);
40
+ put("gen_ai.usage.input_tokens", call.inputTokens);
41
+ put("gen_ai.usage.output_tokens", call.outputTokens);
42
+ put("gen_ai.response.finish_reasons", call.finishReason);
43
+ put("metaobjects.trace_id", call.traceId);
44
+ put("metaobjects.span_id", call.spanId);
45
+ put("metaobjects.status", call.status);
46
+ span.setAttributes(attrs);
47
+ span.end();
48
+ } catch (err) {
49
+ this.onError(err);
50
+ }
51
+ }
52
+ }