@sisu-ai/mw-usage-tracker 10.0.0 → 11.0.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,7 @@
1
+ import { type Middleware } from './compose.js';
2
+ import type { Ctx } from './types.js';
3
+ export declare class Agent<C extends Ctx = Ctx> {
4
+ private stack;
5
+ use(mw: Middleware<C>): this;
6
+ handler(): (ctx: C, nextOuter?: (() => Promise<void>) | undefined) => Promise<void>;
7
+ }
@@ -0,0 +1,8 @@
1
+ import { compose } from './compose.js';
2
+ export class Agent {
3
+ constructor() {
4
+ this.stack = [];
5
+ }
6
+ use(mw) { this.stack.push(mw); return this; }
7
+ handler() { return compose(this.stack); }
8
+ }
@@ -0,0 +1,3 @@
1
+ import type { Ctx } from "./types.js";
2
+ export type Middleware<C extends Ctx = Ctx> = (ctx: C, next: () => Promise<void>) => void | Promise<void>;
3
+ export declare function compose<C extends Ctx>(stack: Middleware<C>[]): (ctx: C, nextOuter?: () => Promise<void>) => Promise<void>;
@@ -0,0 +1,21 @@
1
+ export function compose(stack) {
2
+ if (!Array.isArray(stack)) {
3
+ throw new TypeError("Middleware stack must be an array");
4
+ }
5
+ if (stack.some((fn) => typeof fn !== "function")) {
6
+ throw new TypeError("Middleware must be composed of functions");
7
+ }
8
+ return (ctx, nextOuter) => {
9
+ let index = -1;
10
+ async function dispatch(i) {
11
+ if (i <= index)
12
+ throw new Error("next() called multiple times");
13
+ index = i;
14
+ const fn = stack[i] ?? nextOuter;
15
+ if (!fn)
16
+ return;
17
+ await fn(ctx, () => dispatch(i + 1));
18
+ }
19
+ return dispatch(0);
20
+ };
21
+ }
@@ -0,0 +1,17 @@
1
+ import type { EmbeddingsProvider } from "./types.js";
2
+ export interface CreateEmbeddingsClientOptions {
3
+ baseUrl: string;
4
+ model: string;
5
+ apiKey?: string;
6
+ path?: string;
7
+ headers?: Record<string, string>;
8
+ authHeader?: string;
9
+ authScheme?: string;
10
+ clientName?: string;
11
+ buildBody?: (args: {
12
+ input: string[];
13
+ model: string;
14
+ }) => Record<string, unknown>;
15
+ parseResponse?: (raw: string) => number[][];
16
+ }
17
+ export declare function createEmbeddingsClient(options: CreateEmbeddingsClientOptions): EmbeddingsProvider;
@@ -0,0 +1,75 @@
1
+ export function createEmbeddingsClient(options) {
2
+ const clientName = options.clientName ?? "createEmbeddingsClient";
3
+ if (!options.baseUrl) {
4
+ throw new Error(`[${clientName}] baseUrl is required`);
5
+ }
6
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
7
+ const path = options.path ?? "/v1/embeddings";
8
+ const authHeader = options.authHeader ?? "Authorization";
9
+ const authScheme = options.authScheme ?? "Bearer ";
10
+ return {
11
+ async embed(input, opts) {
12
+ if (!Array.isArray(input) || input.length === 0) {
13
+ throw new Error(`[${clientName}] input must contain at least one string`);
14
+ }
15
+ if (opts?.signal?.aborted) {
16
+ throw new Error(`[${clientName}] embedding request aborted`);
17
+ }
18
+ const model = opts?.model ?? options.model;
19
+ if (!model) {
20
+ throw new Error(`[${clientName}] model is required`);
21
+ }
22
+ const response = await fetch(`${baseUrl}${path}`, {
23
+ method: "POST",
24
+ headers: {
25
+ "Content-Type": "application/json",
26
+ Accept: "application/json",
27
+ ...(options.apiKey
28
+ ? { [authHeader]: `${authScheme}${options.apiKey}` }
29
+ : {}),
30
+ ...(options.headers ?? {}),
31
+ },
32
+ body: JSON.stringify(options.buildBody?.({ input, model }) ?? {
33
+ model,
34
+ input,
35
+ }),
36
+ signal: opts?.signal,
37
+ });
38
+ const raw = await response.text();
39
+ if (!response.ok) {
40
+ throw new Error(`[${clientName}] API error: ${response.status} ${response.statusText} - ${extractErrorDetails(raw)}`);
41
+ }
42
+ let embeddings;
43
+ try {
44
+ embeddings = options.parseResponse?.(raw) ?? parseOpenAICompatibleResponse(raw);
45
+ }
46
+ catch (error) {
47
+ const message = error instanceof Error ? error.message : "unknown parse error";
48
+ throw new Error(`[${clientName}] Failed to parse embeddings response: ${message}`);
49
+ }
50
+ if (embeddings.length !== input.length) {
51
+ throw new Error(`[${clientName}] Expected ${input.length} embeddings, received ${embeddings.length}`);
52
+ }
53
+ return embeddings;
54
+ },
55
+ };
56
+ }
57
+ function parseOpenAICompatibleResponse(raw) {
58
+ const parsed = JSON.parse(raw);
59
+ return (parsed.data ?? []).map((entry) => entry.embedding ?? []);
60
+ }
61
+ function extractErrorDetails(raw) {
62
+ try {
63
+ const parsed = JSON.parse(raw);
64
+ if (typeof parsed.error === "string")
65
+ return parsed.error;
66
+ if (parsed.error?.message)
67
+ return parsed.error.message;
68
+ if (parsed.message)
69
+ return parsed.message;
70
+ }
71
+ catch {
72
+ return raw;
73
+ }
74
+ return raw;
75
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Base error class for all Sisu errors.
3
+ * Provides structured error information with error codes and context.
4
+ */
5
+ export declare class SisuError extends Error {
6
+ readonly code: string;
7
+ readonly context?: unknown | undefined;
8
+ constructor(message: string, code: string, context?: unknown | undefined);
9
+ /**
10
+ * Convert error to JSON for logging and tracing
11
+ */
12
+ toJSON(): {
13
+ name: string;
14
+ message: string;
15
+ code: string;
16
+ context?: unknown;
17
+ stack?: string;
18
+ };
19
+ }
20
+ /**
21
+ * Error thrown when middleware execution fails.
22
+ * Includes the middleware index to help identify which middleware failed.
23
+ */
24
+ export declare class MiddlewareError extends SisuError {
25
+ readonly middlewareIndex: number;
26
+ constructor(message: string, middlewareIndex: number, cause?: Error);
27
+ }
28
+ /**
29
+ * Error thrown when tool execution fails.
30
+ * Includes tool name and arguments for debugging.
31
+ */
32
+ export declare class ToolExecutionError extends SisuError {
33
+ readonly toolName: string;
34
+ readonly args: unknown;
35
+ constructor(message: string, toolName: string, args: unknown, cause?: Error);
36
+ }
37
+ /**
38
+ * Error thrown when adapter/LLM operations fail.
39
+ * Includes model name and provider-specific details.
40
+ */
41
+ export declare class AdapterError extends SisuError {
42
+ readonly modelName: string;
43
+ readonly provider?: string | undefined;
44
+ constructor(message: string, modelName: string, provider?: string | undefined, cause?: Error);
45
+ }
46
+ /**
47
+ * Error thrown when validation fails (e.g., schema validation for tools).
48
+ * Includes validation errors and the data that failed validation.
49
+ */
50
+ export declare class ValidationError extends SisuError {
51
+ readonly validationErrors: unknown;
52
+ readonly data?: unknown | undefined;
53
+ constructor(message: string, validationErrors: unknown, data?: unknown | undefined, cause?: Error);
54
+ }
55
+ /**
56
+ * Error thrown when a timeout occurs.
57
+ * Includes timeout duration and operation details.
58
+ */
59
+ export declare class TimeoutError extends SisuError {
60
+ readonly timeoutMs: number;
61
+ readonly operation?: string | undefined;
62
+ constructor(message: string, timeoutMs: number, operation?: string | undefined, cause?: Error);
63
+ }
64
+ /**
65
+ * Error thrown when an operation is cancelled (e.g., via AbortSignal).
66
+ * Includes the reason for cancellation if available.
67
+ */
68
+ export declare class CancellationError extends SisuError {
69
+ readonly reason?: string | undefined;
70
+ constructor(message: string, reason?: string | undefined, cause?: Error);
71
+ }
72
+ /**
73
+ * Error thrown when a configuration is invalid.
74
+ * Includes the invalid configuration and expected format.
75
+ */
76
+ export declare class ConfigurationError extends SisuError {
77
+ readonly config?: unknown | undefined;
78
+ readonly expected?: string | undefined;
79
+ constructor(message: string, config?: unknown | undefined, expected?: string | undefined, cause?: Error);
80
+ }
81
+ /**
82
+ * Helper to check if an error is a SisuError
83
+ */
84
+ export declare function isSisuError(error: unknown): error is SisuError;
85
+ /**
86
+ * Helper to extract error details for logging
87
+ */
88
+ export declare function getErrorDetails(error: unknown): {
89
+ name: string;
90
+ message: string;
91
+ code?: string;
92
+ context?: unknown;
93
+ stack?: string;
94
+ };
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Base error class for all Sisu errors.
3
+ * Provides structured error information with error codes and context.
4
+ */
5
+ export class SisuError extends Error {
6
+ constructor(message, code, context) {
7
+ super(message);
8
+ this.code = code;
9
+ this.context = context;
10
+ this.name = 'SisuError';
11
+ // Maintain proper stack trace for where our error was thrown (only available on V8)
12
+ if (Error.captureStackTrace) {
13
+ Error.captureStackTrace(this, this.constructor);
14
+ }
15
+ }
16
+ /**
17
+ * Convert error to JSON for logging and tracing
18
+ */
19
+ toJSON() {
20
+ return {
21
+ name: this.name,
22
+ message: this.message,
23
+ code: this.code,
24
+ context: this.context,
25
+ stack: this.stack,
26
+ };
27
+ }
28
+ }
29
+ /**
30
+ * Error thrown when middleware execution fails.
31
+ * Includes the middleware index to help identify which middleware failed.
32
+ */
33
+ export class MiddlewareError extends SisuError {
34
+ constructor(message, middlewareIndex, cause) {
35
+ super(message, 'MIDDLEWARE_ERROR', { middlewareIndex, cause });
36
+ this.middlewareIndex = middlewareIndex;
37
+ this.name = 'MiddlewareError';
38
+ if (Error.captureStackTrace) {
39
+ Error.captureStackTrace(this, this.constructor);
40
+ }
41
+ }
42
+ }
43
+ /**
44
+ * Error thrown when tool execution fails.
45
+ * Includes tool name and arguments for debugging.
46
+ */
47
+ export class ToolExecutionError extends SisuError {
48
+ constructor(message, toolName, args, cause) {
49
+ super(message, 'TOOL_EXECUTION_ERROR', { toolName, args, cause });
50
+ this.toolName = toolName;
51
+ this.args = args;
52
+ this.name = 'ToolExecutionError';
53
+ if (Error.captureStackTrace) {
54
+ Error.captureStackTrace(this, this.constructor);
55
+ }
56
+ }
57
+ }
58
+ /**
59
+ * Error thrown when adapter/LLM operations fail.
60
+ * Includes model name and provider-specific details.
61
+ */
62
+ export class AdapterError extends SisuError {
63
+ constructor(message, modelName, provider, cause) {
64
+ super(message, 'ADAPTER_ERROR', { modelName, provider, cause });
65
+ this.modelName = modelName;
66
+ this.provider = provider;
67
+ this.name = 'AdapterError';
68
+ if (Error.captureStackTrace) {
69
+ Error.captureStackTrace(this, this.constructor);
70
+ }
71
+ }
72
+ }
73
+ /**
74
+ * Error thrown when validation fails (e.g., schema validation for tools).
75
+ * Includes validation errors and the data that failed validation.
76
+ */
77
+ export class ValidationError extends SisuError {
78
+ constructor(message, validationErrors, data, cause) {
79
+ super(message, 'VALIDATION_ERROR', { validationErrors, data, cause });
80
+ this.validationErrors = validationErrors;
81
+ this.data = data;
82
+ this.name = 'ValidationError';
83
+ if (Error.captureStackTrace) {
84
+ Error.captureStackTrace(this, this.constructor);
85
+ }
86
+ }
87
+ }
88
+ /**
89
+ * Error thrown when a timeout occurs.
90
+ * Includes timeout duration and operation details.
91
+ */
92
+ export class TimeoutError extends SisuError {
93
+ constructor(message, timeoutMs, operation, cause) {
94
+ super(message, 'TIMEOUT_ERROR', { timeoutMs, operation, cause });
95
+ this.timeoutMs = timeoutMs;
96
+ this.operation = operation;
97
+ this.name = 'TimeoutError';
98
+ if (Error.captureStackTrace) {
99
+ Error.captureStackTrace(this, this.constructor);
100
+ }
101
+ }
102
+ }
103
+ /**
104
+ * Error thrown when an operation is cancelled (e.g., via AbortSignal).
105
+ * Includes the reason for cancellation if available.
106
+ */
107
+ export class CancellationError extends SisuError {
108
+ constructor(message, reason, cause) {
109
+ super(message, 'CANCELLATION_ERROR', { reason, cause });
110
+ this.reason = reason;
111
+ this.name = 'CancellationError';
112
+ if (Error.captureStackTrace) {
113
+ Error.captureStackTrace(this, this.constructor);
114
+ }
115
+ }
116
+ }
117
+ /**
118
+ * Error thrown when a configuration is invalid.
119
+ * Includes the invalid configuration and expected format.
120
+ */
121
+ export class ConfigurationError extends SisuError {
122
+ constructor(message, config, expected, cause) {
123
+ super(message, 'CONFIGURATION_ERROR', { config, expected, cause });
124
+ this.config = config;
125
+ this.expected = expected;
126
+ this.name = 'ConfigurationError';
127
+ if (Error.captureStackTrace) {
128
+ Error.captureStackTrace(this, this.constructor);
129
+ }
130
+ }
131
+ }
132
+ /**
133
+ * Helper to check if an error is a SisuError
134
+ */
135
+ export function isSisuError(error) {
136
+ return error instanceof SisuError;
137
+ }
138
+ /**
139
+ * Helper to extract error details for logging
140
+ */
141
+ export function getErrorDetails(error) {
142
+ if (isSisuError(error)) {
143
+ return error.toJSON();
144
+ }
145
+ if (error instanceof Error) {
146
+ return {
147
+ name: error.name,
148
+ message: error.message,
149
+ stack: error.stack,
150
+ };
151
+ }
152
+ return {
153
+ name: 'UnknownError',
154
+ message: String(error),
155
+ };
156
+ }
@@ -0,0 +1,6 @@
1
+ export * from './types.js';
2
+ export * from './embeddings.js';
3
+ export * from './compose.js';
4
+ export * from './Agent.js';
5
+ export * from './util.js';
6
+ export * from './errors.js';
@@ -0,0 +1,6 @@
1
+ export * from './types.js';
2
+ export * from './embeddings.js';
3
+ export * from './compose.js';
4
+ export * from './Agent.js';
5
+ export * from './util.js';
6
+ export * from './errors.js';
@@ -0,0 +1,190 @@
1
+ export type Role = "user" | "assistant" | "system" | "tool";
2
+ /** Tool call envelope normalized across providers */
3
+ export interface ToolCall {
4
+ id: string;
5
+ name: string;
6
+ arguments: unknown;
7
+ }
8
+ /** Messages are discriminated by role for precision */
9
+ export type Message = SystemMessage | UserMessage | AssistantMessage | ToolMessage;
10
+ export interface SystemMessage {
11
+ role: "system";
12
+ content: string;
13
+ name?: string;
14
+ }
15
+ export interface UserMessage {
16
+ role: "user";
17
+ content: string;
18
+ name?: string;
19
+ }
20
+ export interface AssistantMessage {
21
+ role: "assistant";
22
+ content: string;
23
+ name?: string;
24
+ /** When the model wants to call tools, it returns one or more tool calls */
25
+ tool_calls?: ToolCall[];
26
+ /**
27
+ * Reasoning details from thinking/reasoning models (e.g., o1, o3, ChatGPT 5.1).
28
+ * This field must be preserved when passing the message back to the model
29
+ * for multi-turn conversations to maintain reasoning context.
30
+ * @internal The structure is provider-specific and should be treated as opaque.
31
+ */
32
+ reasoning_details?: unknown;
33
+ }
34
+ export interface ToolMessage {
35
+ role: "tool";
36
+ /** Tool JSON/string result to be fed back to the model */
37
+ content: string;
38
+ /** Link back to the specific assistant tool call */
39
+ tool_call_id: string;
40
+ /** (optional) echo the tool name for debugging/trace */
41
+ name?: string;
42
+ }
43
+ /** LLM call options */
44
+ export type ToolChoice = "auto" | "none" | "required" | {
45
+ name: string;
46
+ };
47
+ export interface GenerateOptions {
48
+ temperature?: number;
49
+ maxTokens?: number;
50
+ toolChoice?: ToolChoice;
51
+ signal?: globalThis.AbortSignal;
52
+ tools?: Tool[];
53
+ parallelToolCalls?: boolean;
54
+ stream?: boolean;
55
+ /**
56
+ * Enable extended reasoning/thinking for models that support it (e.g., o1, o3, ChatGPT 5.1).
57
+ * - `true` or `false`: Simple enable/disable
58
+ * - `{ enabled: true }`: OpenAI-style object notation
59
+ * - Custom object: Provider-specific options
60
+ *
61
+ * @example
62
+ * // Enable reasoning
63
+ * { reasoning: true }
64
+ *
65
+ * @example
66
+ * // OpenAI format
67
+ * { reasoning: { enabled: true } }
68
+ */
69
+ reasoning?: boolean | {
70
+ enabled: boolean;
71
+ } | Record<string, unknown>;
72
+ }
73
+ /** Streaming events */
74
+ export type ModelEvent = {
75
+ type: "token";
76
+ token: string;
77
+ } | {
78
+ type: "tool_call";
79
+ call: ToolCall;
80
+ } | {
81
+ type: "assistant_message";
82
+ message: AssistantMessage;
83
+ } | {
84
+ type: "usage";
85
+ usage: Usage;
86
+ };
87
+ export interface Usage {
88
+ promptTokens?: number;
89
+ completionTokens?: number;
90
+ totalTokens?: number;
91
+ costUSD?: number;
92
+ }
93
+ /** Final response */
94
+ export interface ModelResponse {
95
+ message: AssistantMessage;
96
+ usage?: Usage;
97
+ }
98
+ export interface EmbedOptions {
99
+ model?: string;
100
+ signal?: globalThis.AbortSignal;
101
+ }
102
+ export interface EmbeddingsProvider {
103
+ embed(input: string[], opts?: EmbedOptions): Promise<number[][]>;
104
+ }
105
+ /** Adapter contract */
106
+ export interface LLM {
107
+ name: string;
108
+ capabilities: {
109
+ functionCall?: boolean;
110
+ streaming?: boolean;
111
+ };
112
+ generate(messages: Message[], opts?: GenerateOptions): Promise<ModelResponse>;
113
+ generate(messages: Message[], opts?: GenerateOptions): AsyncIterable<ModelEvent>;
114
+ generate(messages: Message[], opts?: GenerateOptions): Promise<ModelResponse | AsyncIterable<ModelEvent>>;
115
+ }
116
+ /** Logger, Memory, TokenStream: unchanged */
117
+ export interface Logger {
118
+ debug(...args: unknown[]): void;
119
+ info(...args: unknown[]): void;
120
+ warn(...args: unknown[]): void;
121
+ error(...args: unknown[]): void;
122
+ span?(name: string, attrs?: Record<string, unknown>): void;
123
+ }
124
+ export interface Memory {
125
+ get<T = unknown>(key: string): Promise<T | undefined>;
126
+ set(key: string, val: unknown): Promise<void>;
127
+ retrieval?(index: string): {
128
+ search: (q: string, topK?: number) => Promise<Array<{
129
+ text: string;
130
+ score?: number;
131
+ }>>;
132
+ };
133
+ }
134
+ export interface TokenStream {
135
+ write(token: string): void;
136
+ end(): void;
137
+ }
138
+ /**
139
+ * Restricted context for tool execution.
140
+ * Tools have access to a sandboxed subset of Ctx to prevent:
141
+ * - Tools calling other tools (no tools registry access)
142
+ * - Tools manipulating conversation history (no messages access)
143
+ * - Tools accessing middleware state (no state access)
144
+ * - Tools interfering with user I/O (no input/stream access)
145
+ *
146
+ * Tools CAN:
147
+ * - Use the model for meta-operations (e.g., summarizeText)
148
+ * - Access persistent memory
149
+ * - Respect cancellation signals
150
+ * - Log their operations
151
+ * - Access injected dependencies (for testing/configuration)
152
+ */
153
+ export interface ToolContext {
154
+ readonly memory: Memory;
155
+ readonly signal: globalThis.AbortSignal;
156
+ readonly log: Logger;
157
+ readonly model: LLM;
158
+ /** Optional dependency injection container for testing or runtime configuration */
159
+ readonly deps?: Record<string, unknown>;
160
+ }
161
+ export interface Tool<TArgs = unknown, TResult = unknown> {
162
+ name: string;
163
+ description?: string;
164
+ schema: unknown;
165
+ handler(args: TArgs, ctx: ToolContext): Promise<TResult>;
166
+ }
167
+ /** Registry */
168
+ export interface ToolRegistry {
169
+ list(): Tool[];
170
+ get(name: string): Tool | undefined;
171
+ register(tool: Tool): void;
172
+ }
173
+ /** Context */
174
+ export interface Ctx {
175
+ input?: string;
176
+ messages: Message[];
177
+ model: LLM;
178
+ tools: ToolRegistry;
179
+ memory: Memory;
180
+ stream: TokenStream;
181
+ /**
182
+ * Extensible state object for middleware to share data.
183
+ *
184
+ * Well-known keys used by SISU middleware:
185
+ * - `toolAliases` (Map<string, string>): Map of tool names to API aliases set by registerTools
186
+ */
187
+ state: Record<string, unknown>;
188
+ signal: globalThis.AbortSignal;
189
+ log: Logger;
190
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,88 @@
1
+ import { Middleware } from "./compose.js";
2
+ import type { Ctx, Logger, Memory, TokenStream, Tool, ToolRegistry, LLM } from "./types.js";
3
+ type Level = "debug" | "info" | "warn" | "error";
4
+ export declare function createConsoleLogger(opts?: {
5
+ level?: Level;
6
+ timestamps?: boolean;
7
+ }): Logger;
8
+ export declare const consoleLogger: Logger;
9
+ export interface TraceEvent {
10
+ level: Level | "span";
11
+ ts: string;
12
+ args: unknown[];
13
+ }
14
+ export declare function createTracingLogger(base?: Logger): {
15
+ logger: Logger;
16
+ getTrace: () => TraceEvent[];
17
+ reset: () => void;
18
+ };
19
+ export interface RedactOptions {
20
+ keys?: string[];
21
+ mask?: string;
22
+ patterns?: RegExp[];
23
+ }
24
+ export declare function redactSensitive(input: unknown, opts?: RedactOptions): unknown;
25
+ export declare function createRedactingLogger(base: Logger, opts?: RedactOptions): Logger;
26
+ export declare class InMemoryKV implements Memory {
27
+ private m;
28
+ get<T = unknown>(key: string): Promise<T | undefined>;
29
+ set(key: string, val: unknown): Promise<void>;
30
+ retrieval(index: string): {
31
+ search: (q: string, topK?: number) => Promise<{
32
+ text: string;
33
+ score: number;
34
+ }[]>;
35
+ };
36
+ }
37
+ export declare class NullStream implements TokenStream {
38
+ write(_t: string): void;
39
+ end(): void;
40
+ }
41
+ export declare const stdoutStream: TokenStream;
42
+ export declare function bufferStream(): {
43
+ stream: {
44
+ write: (t: string) => void;
45
+ end: () => void;
46
+ };
47
+ getText: () => string;
48
+ };
49
+ export declare function teeStream(...streams: TokenStream[]): TokenStream;
50
+ export declare const streamOnce: Middleware;
51
+ export declare class SimpleTools implements ToolRegistry {
52
+ private tools;
53
+ list(): Tool<unknown, unknown>[];
54
+ get(name: string): Tool<unknown, unknown> | undefined;
55
+ register(tool: Tool): void;
56
+ }
57
+ export interface CreateCtxOptions {
58
+ model: LLM;
59
+ input?: string;
60
+ systemPrompt?: string;
61
+ logLevel?: Level;
62
+ timestamps?: boolean;
63
+ signal?: globalThis.AbortSignal;
64
+ tools?: Tool[] | ToolRegistry;
65
+ memory?: Memory;
66
+ stream?: TokenStream;
67
+ state?: Record<string, unknown>;
68
+ }
69
+ /**
70
+ * Factory function to create a Ctx object with sensible defaults.
71
+ * Reduces boilerplate by providing defaults for all optional fields.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const ctx = createCtx({
76
+ * model: openAIAdapter({ model: 'gpt-4o-mini' }),
77
+ * input: 'Hello',
78
+ * systemPrompt: 'You are a helpful assistant',
79
+ * logLevel: 'debug'
80
+ * });
81
+ * ```
82
+ */
83
+ export declare function createCtx(options: CreateCtxOptions): Ctx;
84
+ export type FlagMap = Record<string, string | boolean>;
85
+ export declare function parseFlags(argv?: string[]): FlagMap;
86
+ export declare function configFromFlagsAndEnv(envVars: string[], flags?: FlagMap, env?: typeof process.env): Record<string, string | undefined>;
87
+ export declare function firstConfigValue(names: string[], flags?: FlagMap, env?: typeof process.env): string | undefined;
88
+ export {};
@@ -0,0 +1,371 @@
1
+ const order = {
2
+ debug: 10,
3
+ info: 20,
4
+ warn: 30,
5
+ error: 40,
6
+ };
7
+ function nowTs() {
8
+ const d = new Date();
9
+ const pad = (n, s = 2) => String(n).padStart(s, "0");
10
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
11
+ }
12
+ export function createConsoleLogger(opts = {}) {
13
+ const envLevel = process.env.LOG_LEVEL?.toLowerCase();
14
+ const level = opts.level ?? envLevel ?? "info";
15
+ const showTs = opts.timestamps ?? true;
16
+ const enabled = (lvl) => order[lvl] >= order[level];
17
+ const prefix = (lvl) => (showTs ? `[${nowTs()}] ` : "") + `[${lvl}]`;
18
+ return {
19
+ debug: (...a) => {
20
+ if (enabled("debug"))
21
+ console.debug(prefix("debug"), ...a);
22
+ },
23
+ info: (...a) => {
24
+ if (enabled("info"))
25
+ console.info(prefix("info"), ...a);
26
+ },
27
+ warn: (...a) => {
28
+ if (enabled("warn"))
29
+ console.warn(prefix("warn"), ...a);
30
+ },
31
+ error: (...a) => {
32
+ if (enabled("error"))
33
+ console.error(prefix("error"), ...a);
34
+ },
35
+ span: (name, attrs) => {
36
+ if (enabled("info"))
37
+ console.info((showTs ? `[${nowTs()}] ` : "") + "[span]", name, attrs ?? {});
38
+ },
39
+ };
40
+ }
41
+ // Backward-compatible always-on logger
42
+ export const consoleLogger = createConsoleLogger({
43
+ level: process.env.LOG_LEVEL ?? "debug",
44
+ });
45
+ export function createTracingLogger(base = createConsoleLogger()) {
46
+ const events = [];
47
+ const push = (level, ...args) => {
48
+ events.push({ level, ts: new Date().toISOString(), args });
49
+ };
50
+ const logger = {
51
+ debug: (...a) => {
52
+ push("debug", ...a);
53
+ base.debug?.(...a);
54
+ },
55
+ info: (...a) => {
56
+ push("info", ...a);
57
+ base.info?.(...a);
58
+ },
59
+ warn: (...a) => {
60
+ push("warn", ...a);
61
+ base.warn?.(...a);
62
+ },
63
+ error: (...a) => {
64
+ push("error", ...a);
65
+ base.error?.(...a);
66
+ },
67
+ span: (name, attrs) => {
68
+ push("span", name, attrs ?? {});
69
+ base.span?.(name, attrs);
70
+ },
71
+ };
72
+ return {
73
+ logger,
74
+ getTrace: () => events.slice(),
75
+ reset: () => {
76
+ events.length = 0;
77
+ },
78
+ };
79
+ }
80
+ const DEFAULT_SENSITIVE_KEYS = [
81
+ "api_key",
82
+ "apikey",
83
+ "apiKey",
84
+ "authorization",
85
+ "auth",
86
+ "token",
87
+ "access_token",
88
+ "refresh_token",
89
+ "password",
90
+ "passwd",
91
+ "secret",
92
+ "x-api-key",
93
+ "openai_api_key",
94
+ ];
95
+ // Default patterns for detecting common sensitive data formats
96
+ const DEFAULT_PATTERNS = [
97
+ /sk-[a-zA-Z0-9]{32,}/, // OpenAI-style keys
98
+ /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/, // JWT tokens
99
+ /ghp_[a-zA-Z0-9]{36}/, // GitHub Personal Access Token
100
+ /gho_[a-zA-Z0-9]{36}/, // GitHub OAuth Access Token
101
+ /github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/, // GitHub fine-grained PAT
102
+ /glpat-[a-zA-Z0-9_-]{20}/, // GitLab Personal Access Token
103
+ /AIza[0-9A-Za-z_-]{35}/, // Google API Key
104
+ /ya29\.[0-9A-Za-z_-]+/, // Google OAuth Access Token
105
+ /AKIA[0-9A-Z]{16}/, // AWS Access Key ID
106
+ /xox[baprs]-[0-9a-zA-Z-]{10,}/, // Slack tokens
107
+ ];
108
+ function matchesPattern(value, patterns) {
109
+ for (const pattern of patterns) {
110
+ if (pattern.test(value))
111
+ return true;
112
+ }
113
+ return false;
114
+ }
115
+ function redactObject(input, keysSet, patterns, mask) {
116
+ if (input === null || input === undefined)
117
+ return input;
118
+ // Preserve Error objects with useful fields
119
+ if (input instanceof Error) {
120
+ return { name: input.name, message: input.message, stack: input.stack };
121
+ }
122
+ if (Array.isArray(input)) {
123
+ return input.map((v) => redactObject(v, keysSet, patterns, mask));
124
+ }
125
+ if (typeof input === "object") {
126
+ const out = {};
127
+ for (const [k, v] of Object.entries(input)) {
128
+ out[k] = keysSet.has(k.toLowerCase())
129
+ ? mask
130
+ : redactObject(v, keysSet, patterns, mask);
131
+ }
132
+ return out;
133
+ }
134
+ // Check string values against patterns
135
+ if (typeof input === "string" && matchesPattern(input, patterns)) {
136
+ return mask;
137
+ }
138
+ return input;
139
+ }
140
+ function redactArgs(args, keysSet, patterns, mask) {
141
+ return args.map((arg) => redactObject(arg, keysSet, patterns, mask));
142
+ }
143
+ export function redactSensitive(input, opts = {}) {
144
+ const envKeys = (process.env.LOG_REDACT_KEYS || "")
145
+ .split(",")
146
+ .map((s) => s.trim())
147
+ .filter(Boolean);
148
+ const keys = (opts.keys && opts.keys.length ? opts.keys : DEFAULT_SENSITIVE_KEYS).concat(envKeys);
149
+ const keysSet = new Set(keys.map((k) => k.toLowerCase()));
150
+ const patterns = opts.patterns ?? DEFAULT_PATTERNS;
151
+ const mask = opts.mask ?? "***REDACTED***";
152
+ return redactObject(input, keysSet, patterns, mask);
153
+ }
154
+ export function createRedactingLogger(base, opts = {}) {
155
+ const envKeys = (process.env.LOG_REDACT_KEYS || "")
156
+ .split(",")
157
+ .map((s) => s.trim())
158
+ .filter(Boolean);
159
+ const keys = (opts.keys && opts.keys.length ? opts.keys : DEFAULT_SENSITIVE_KEYS).concat(envKeys);
160
+ const keysSet = new Set(keys.map((k) => k.toLowerCase()));
161
+ const patterns = opts.patterns ?? DEFAULT_PATTERNS;
162
+ const mask = opts.mask ?? "***REDACTED***";
163
+ return {
164
+ debug: (...a) => base.debug(...redactArgs(a, keysSet, patterns, mask)),
165
+ info: (...a) => base.info(...redactArgs(a, keysSet, patterns, mask)),
166
+ warn: (...a) => base.warn(...redactArgs(a, keysSet, patterns, mask)),
167
+ error: (...a) => base.error(...redactArgs(a, keysSet, patterns, mask)),
168
+ span: (name, attrs) => base.span?.(name, redactObject(attrs, keysSet, patterns, mask)),
169
+ };
170
+ }
171
+ export class InMemoryKV {
172
+ constructor() {
173
+ this.m = new Map();
174
+ }
175
+ async get(key) {
176
+ return this.m.get(key);
177
+ }
178
+ async set(key, val) {
179
+ this.m.set(key, val);
180
+ }
181
+ retrieval(index) {
182
+ const docs = this.m.get(`retrieval:${index}`) ?? [];
183
+ return {
184
+ search: async (q, topK = 4) => {
185
+ const scored = docs.map((t) => ({
186
+ text: t,
187
+ score: t.toLowerCase().includes(q.toLowerCase()) ? 1 : 0,
188
+ }));
189
+ return scored.sort((a, b) => b.score - a.score).slice(0, topK);
190
+ },
191
+ };
192
+ }
193
+ }
194
+ export class NullStream {
195
+ write(_t) { }
196
+ end() { }
197
+ }
198
+ export const stdoutStream = {
199
+ write: (t) => {
200
+ process.stdout.write(t);
201
+ },
202
+ end: () => {
203
+ process.stdout.write("\n");
204
+ },
205
+ };
206
+ export function bufferStream() {
207
+ let buf = "";
208
+ return {
209
+ stream: {
210
+ write: (t) => {
211
+ buf += t;
212
+ },
213
+ end: () => { },
214
+ },
215
+ getText: () => buf,
216
+ };
217
+ }
218
+ export function teeStream(...streams) {
219
+ return {
220
+ write: (t) => {
221
+ for (const s of streams)
222
+ s.write(t);
223
+ },
224
+ end: () => {
225
+ for (const s of streams)
226
+ s.end();
227
+ },
228
+ };
229
+ }
230
+ export const streamOnce = async (c) => {
231
+ const out = await c.model.generate(c.messages, {
232
+ stream: true,
233
+ toolChoice: "none",
234
+ signal: c.signal,
235
+ });
236
+ const stream = out;
237
+ if (stream &&
238
+ typeof stream[Symbol.asyncIterator] ===
239
+ "function") {
240
+ for await (const ev of stream) {
241
+ if (ev && typeof ev === "object") {
242
+ const event = ev;
243
+ if (event.type === "token" && typeof event.token === "string") {
244
+ c.stream.write(event.token);
245
+ }
246
+ else if (event.type === "assistant_message" && event.message) {
247
+ c.messages.push(event.message);
248
+ }
249
+ }
250
+ }
251
+ c.stream.end();
252
+ }
253
+ else if (out?.message) {
254
+ c.messages.push(out.message);
255
+ c.stream.write(out.message.content);
256
+ c.stream.end();
257
+ }
258
+ };
259
+ export class SimpleTools {
260
+ constructor() {
261
+ this.tools = new Map();
262
+ }
263
+ list() {
264
+ return Array.from(this.tools.values());
265
+ }
266
+ get(name) {
267
+ return this.tools.get(name);
268
+ }
269
+ register(tool) {
270
+ this.tools.set(tool.name, tool);
271
+ }
272
+ }
273
+ /**
274
+ * Factory function to create a Ctx object with sensible defaults.
275
+ * Reduces boilerplate by providing defaults for all optional fields.
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * const ctx = createCtx({
280
+ * model: openAIAdapter({ model: 'gpt-4o-mini' }),
281
+ * input: 'Hello',
282
+ * systemPrompt: 'You are a helpful assistant',
283
+ * logLevel: 'debug'
284
+ * });
285
+ * ```
286
+ */
287
+ export function createCtx(options) {
288
+ const messages = [];
289
+ if (options.systemPrompt) {
290
+ messages.push({ role: "system", content: options.systemPrompt });
291
+ }
292
+ // Handle tools - accept either array or registry
293
+ let toolRegistry;
294
+ if (options.tools) {
295
+ if (Array.isArray(options.tools)) {
296
+ toolRegistry = new SimpleTools();
297
+ options.tools.forEach((t) => toolRegistry.register(t));
298
+ }
299
+ else {
300
+ toolRegistry = options.tools;
301
+ }
302
+ }
303
+ else {
304
+ toolRegistry = new SimpleTools();
305
+ }
306
+ return {
307
+ input: options.input,
308
+ messages,
309
+ model: options.model,
310
+ tools: toolRegistry,
311
+ memory: options.memory ?? new InMemoryKV(),
312
+ stream: options.stream ?? new NullStream(),
313
+ state: options.state ?? {},
314
+ signal: options.signal ?? new globalThis.AbortController().signal,
315
+ log: createConsoleLogger({
316
+ level: options.logLevel ?? "info",
317
+ timestamps: options.timestamps,
318
+ }),
319
+ };
320
+ }
321
+ export function parseFlags(argv = process.argv) {
322
+ const out = {};
323
+ for (let i = 2; i < argv.length; i++) {
324
+ const a = argv[i];
325
+ if (!a || !a.startsWith("--"))
326
+ continue;
327
+ const k = a.slice(2);
328
+ const eq = k.indexOf("=");
329
+ if (eq >= 0) {
330
+ const key = k.slice(0, eq);
331
+ const val = k.slice(eq + 1);
332
+ out[key] = val;
333
+ continue;
334
+ }
335
+ const next = argv[i + 1];
336
+ if (next && !next.startsWith("--")) {
337
+ out[k] = next;
338
+ i++;
339
+ }
340
+ else {
341
+ out[k] = true;
342
+ }
343
+ }
344
+ return out;
345
+ }
346
+ function kebabForEnv(envName) {
347
+ return envName.toLowerCase().replace(/_/g, "-");
348
+ }
349
+ // Given a list of env var names (e.g., ['OPENAI_API_KEY','API_KEY']), returns values with precedence: CLI flag > env
350
+ export function configFromFlagsAndEnv(envVars, flags = parseFlags(), env = process.env) {
351
+ const out = {};
352
+ for (const name of envVars) {
353
+ const flag = kebabForEnv(name);
354
+ const cliVal = flags[flag] ?? undefined;
355
+ out[name] = cliVal ?? env[name];
356
+ }
357
+ return out;
358
+ }
359
+ // Helper to pick the first defined value out of a set of env names, respecting CLI-over-env precedence
360
+ export function firstConfigValue(names, flags = parseFlags(), env = process.env) {
361
+ for (const n of names) {
362
+ const flag = kebabForEnv(n);
363
+ const cliVal = flags[flag] ?? undefined;
364
+ if (cliVal !== undefined)
365
+ return cliVal;
366
+ const envVal = env[n];
367
+ if (envVal !== undefined)
368
+ return envVal;
369
+ }
370
+ return undefined;
371
+ }
@@ -0,0 +1,22 @@
1
+ import type { Middleware } from "@sisu-ai/core";
2
+ export type PriceTable = Record<string, {
3
+ inputPer1M?: number;
4
+ outputPer1M?: number;
5
+ inputPer1K?: number;
6
+ outputPer1K?: number;
7
+ imagePerImage?: number;
8
+ imagePer1K?: number;
9
+ imageInputPer1K?: number;
10
+ imageTokenPerImage?: number;
11
+ }>;
12
+ export interface UsageTotals {
13
+ promptTokens: number;
14
+ completionTokens: number;
15
+ totalTokens: number;
16
+ costUSD?: number;
17
+ imageTokens?: number;
18
+ imageCount?: number;
19
+ }
20
+ export declare function usageTracker(prices: PriceTable, opts?: {
21
+ logPerCall?: boolean;
22
+ }): Middleware;
@@ -0,0 +1,165 @@
1
+ export function usageTracker(prices, opts = {}) {
2
+ return async (ctx, next) => {
3
+ // Wrap model.generate to intercept responses
4
+ const orig = ctx.model.generate.bind(ctx.model);
5
+ const price = prices[ctx.model.name] ?? prices["*"];
6
+ const state = ctx.state;
7
+ const totals = {
8
+ promptTokens: Number(state.usage?.promptTokens ?? 0),
9
+ completionTokens: Number(state.usage?.completionTokens ?? 0),
10
+ totalTokens: Number(state.usage?.totalTokens ?? 0),
11
+ costUSD: Number(state.usage?.costUSD ?? 0),
12
+ };
13
+ function applyUsage(out, imageCount, imageTokens) {
14
+ const u = out.usage;
15
+ if (u) {
16
+ const p = Number(u.promptTokens ?? 0);
17
+ const c = Number(u.completionTokens ?? 0);
18
+ const t = Number(u.totalTokens ?? p + c);
19
+ totals.promptTokens += p;
20
+ totals.completionTokens += c;
21
+ totals.totalTokens += t;
22
+ if (price) {
23
+ const inPer1K = price.inputPer1K ??
24
+ (price.inputPer1M != null ? price.inputPer1M / 1000 : undefined) ??
25
+ 0;
26
+ const outPer1K = price.outputPer1K ??
27
+ (price.outputPer1M != null
28
+ ? price.outputPer1M / 1000
29
+ : undefined) ??
30
+ 0;
31
+ const textPromptTokens = Math.max(0, p - (price.imageInputPer1K ? imageTokens : 0));
32
+ const tokenCost = (textPromptTokens / 1000) * inPer1K + (c / 1000) * outPer1K;
33
+ const perImage = price.imagePerImage != null
34
+ ? price.imagePerImage
35
+ : price.imagePer1K != null
36
+ ? price.imagePer1K / 1000
37
+ : undefined;
38
+ const imageCost = perImage != null
39
+ ? imageCount * perImage
40
+ : price.imageInputPer1K
41
+ ? (imageTokens / 1000) * price.imageInputPer1K
42
+ : 0;
43
+ const cost = tokenCost + imageCost;
44
+ totals.costUSD = Number((totals.costUSD ?? 0) + cost);
45
+ if (imageCount > 0 && price.imageInputPer1K) {
46
+ totals.imageTokens = Number((totals.imageTokens ?? 0) + imageTokens);
47
+ totals.imageCount = Number((totals.imageCount ?? 0) + imageCount);
48
+ }
49
+ }
50
+ if (opts.logPerCall)
51
+ ctx.log.info?.("[usage] call", {
52
+ promptTokens: p,
53
+ completionTokens: c,
54
+ totalTokens: t,
55
+ imageTokens: imageCount > 0 && price?.imageInputPer1K
56
+ ? imageTokens
57
+ : undefined,
58
+ imageCount: imageCount > 0 ? imageCount : undefined,
59
+ estCostUSD: price
60
+ ? (() => {
61
+ const inPer1K = price.inputPer1K ??
62
+ (price.inputPer1M != null ? price.inputPer1M / 1000 : 0);
63
+ const outPer1K = price.outputPer1K ??
64
+ (price.outputPer1M != null ? price.outputPer1M / 1000 : 0);
65
+ const textPromptTokens = Math.max(0, p - (price?.imageInputPer1K ? imageTokens : 0));
66
+ const perImage = price?.imagePerImage != null
67
+ ? price.imagePerImage
68
+ : price?.imagePer1K != null
69
+ ? price.imagePer1K / 1000
70
+ : undefined;
71
+ const tokenCost = (textPromptTokens / 1000) * inPer1K + (c / 1000) * outPer1K;
72
+ const imageCost = perImage != null
73
+ ? imageCount * perImage
74
+ : price?.imageInputPer1K
75
+ ? (imageTokens / 1000) * price.imageInputPer1K
76
+ : 0;
77
+ return roundUSD(tokenCost + imageCost);
78
+ })()
79
+ : undefined,
80
+ });
81
+ }
82
+ }
83
+ const isAsyncIterable = (val) => !!val &&
84
+ typeof val[Symbol.asyncIterator] ===
85
+ "function";
86
+ function withUsage(...args) {
87
+ const reqMessages = args?.[0];
88
+ const imageCount = countImageInputs(reqMessages);
89
+ const imageTokens = imageCount * Number(price?.imageTokenPerImage ?? 1000);
90
+ const out = orig(...args);
91
+ if (isAsyncIterable(out)) {
92
+ const iter = async function* () {
93
+ let final;
94
+ for await (const ev of out) {
95
+ if (ev.type === "assistant_message") {
96
+ final = { message: ev.message };
97
+ }
98
+ else if (ev.type === "usage") {
99
+ final = {
100
+ message: { role: "assistant", content: "" },
101
+ usage: ev.usage,
102
+ };
103
+ }
104
+ yield ev;
105
+ }
106
+ if (final)
107
+ applyUsage(final, imageCount, imageTokens);
108
+ };
109
+ return iter();
110
+ }
111
+ return (async () => {
112
+ const resolved = await out;
113
+ applyUsage(resolved, imageCount, imageTokens);
114
+ return resolved;
115
+ })();
116
+ }
117
+ state._origGenerate = orig;
118
+ ctx.model.generate =
119
+ withUsage;
120
+ await next();
121
+ // Restore
122
+ ctx.model.generate = orig;
123
+ if (!state.usage)
124
+ state.usage = {};
125
+ state.usage.promptTokens = totals.promptTokens;
126
+ state.usage.completionTokens = totals.completionTokens;
127
+ state.usage.totalTokens = totals.totalTokens;
128
+ if (price)
129
+ state.usage.costUSD = roundUSD(totals.costUSD ?? 0);
130
+ if (totals.imageTokens)
131
+ state.usage.imageTokens = totals.imageTokens;
132
+ if (totals.imageCount)
133
+ state.usage.imageCount = totals.imageCount;
134
+ ctx.log.info?.("[usage] totals", state.usage);
135
+ };
136
+ }
137
+ function roundUSD(n) {
138
+ return Math.round(n * 1e6) / 1e6;
139
+ }
140
+ function countImageInputs(msgs) {
141
+ if (!Array.isArray(msgs))
142
+ return 0;
143
+ let count = 0;
144
+ for (const m of msgs) {
145
+ const c = m?.content;
146
+ if (Array.isArray(c)) {
147
+ for (const part of c) {
148
+ const partType = part?.type;
149
+ if (part &&
150
+ typeof part === "object" &&
151
+ (partType === "image_url" || partType === "image"))
152
+ count += 1;
153
+ }
154
+ }
155
+ // Convenience shapes supported by adapters (count regardless of content presence)
156
+ const anyMsg = m;
157
+ if (Array.isArray(anyMsg.images))
158
+ count += anyMsg.images.length;
159
+ if (typeof anyMsg.image_url === "string")
160
+ count += 1;
161
+ if (typeof anyMsg.image === "string")
162
+ count += 1;
163
+ }
164
+ return count;
165
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisu-ai/mw-usage-tracker",
3
- "version": "10.0.0",
3
+ "version": "11.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,7 +12,7 @@
12
12
  "access": "public"
13
13
  },
14
14
  "peerDependencies": {
15
- "@sisu-ai/core": "^2.4.0"
15
+ "@sisu-ai/core": "^2.5.0"
16
16
  },
17
17
  "repository": {
18
18
  "type": "git",