@vinhnt-sdk/llm 0.1.3
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.
- package/LICENSE +21 -0
- package/dist/adapter.d.ts +172 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +62 -0
- package/dist/adapter.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/model-caller.d.ts +83 -0
- package/dist/model-caller.d.ts.map +1 -0
- package/dist/model-caller.js +276 -0
- package/dist/model-caller.js.map +1 -0
- package/dist/registry.d.ts +84 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +143 -0
- package/dist/registry.js.map +1 -0
- package/dist/retry.d.ts +47 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +91 -0
- package/dist/retry.js.map +1 -0
- package/dist/token-meter.d.ts +67 -0
- package/dist/token-meter.d.ts.map +1 -0
- package/dist/token-meter.js +92 -0
- package/dist/token-meter.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nguyen Thanh Vinh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Adapter — the abstract contract every provider must implement.
|
|
3
|
+
*
|
|
4
|
+
* An adapter is the minimal interface for streaming model calls.
|
|
5
|
+
* The only required method is `stream()`. Everything else has defaults.
|
|
6
|
+
*
|
|
7
|
+
* This is the "Service Definition" in the capability seam pattern:
|
|
8
|
+
* Service Definition (LlmAdapter) → Provider (concrete) → Consumer (agent loop)
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import type { LlmAdapter, GenerateOptions, StreamChunk } from "@vinhnt-sdk/llm";
|
|
13
|
+
*
|
|
14
|
+
* class MyAdapter implements LlmAdapter {
|
|
15
|
+
* async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
|
16
|
+
* // Call your LLM API here
|
|
17
|
+
* yield { type: "text", content: "Hello" };
|
|
18
|
+
* yield { type: "finish", reason: "stop" };
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import type { ModelCapabilities } from "@vinhnt-sdk/schema";
|
|
24
|
+
/**
|
|
25
|
+
* Options for a model generation call.
|
|
26
|
+
* This is the adapter's view of the request — provider-owned fields
|
|
27
|
+
* (model, apiKey, baseUrl) are resolved before reaching the adapter.
|
|
28
|
+
*/
|
|
29
|
+
export interface GenerateOptions {
|
|
30
|
+
/** The provider/model identifier (e.g., "deepseek-chat", "gpt-4o"). */
|
|
31
|
+
readonly model: string;
|
|
32
|
+
/** Conversation messages. */
|
|
33
|
+
readonly messages: readonly {
|
|
34
|
+
readonly role: string;
|
|
35
|
+
readonly content: string | readonly unknown[];
|
|
36
|
+
readonly [key: string]: unknown;
|
|
37
|
+
}[];
|
|
38
|
+
/** Tool definitions (OpenAI function calling format). */
|
|
39
|
+
readonly tools?: readonly {
|
|
40
|
+
readonly type: "function";
|
|
41
|
+
readonly function: {
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly description: string;
|
|
44
|
+
readonly parameters?: unknown;
|
|
45
|
+
readonly strict?: boolean;
|
|
46
|
+
};
|
|
47
|
+
}[];
|
|
48
|
+
/** Tool choice control. */
|
|
49
|
+
readonly toolChoice?: "auto" | "required" | "none" | {
|
|
50
|
+
readonly type: "function";
|
|
51
|
+
readonly function: {
|
|
52
|
+
readonly name: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/** Max completion tokens. */
|
|
56
|
+
readonly maxTokens?: number;
|
|
57
|
+
/** Temperature (0-2). */
|
|
58
|
+
readonly temperature?: number;
|
|
59
|
+
/** Top-p sampling. */
|
|
60
|
+
readonly topP?: number;
|
|
61
|
+
/** Stop sequences. */
|
|
62
|
+
readonly stop?: readonly string[];
|
|
63
|
+
/** Reasoning effort for o-series models. */
|
|
64
|
+
readonly reasoningEffort?: string;
|
|
65
|
+
/** Stream options (include_usage, etc.). */
|
|
66
|
+
readonly streamOptions?: {
|
|
67
|
+
readonly includeUsage?: boolean;
|
|
68
|
+
};
|
|
69
|
+
/** Provider-specific options passthrough. */
|
|
70
|
+
readonly providerOptions?: Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Raw streaming chunk emitted by an adapter.
|
|
74
|
+
* This is the adapter → runtime protocol.
|
|
75
|
+
*/
|
|
76
|
+
export type StreamChunk = {
|
|
77
|
+
readonly type: "text";
|
|
78
|
+
readonly content: string;
|
|
79
|
+
} | {
|
|
80
|
+
readonly type: "reasoning";
|
|
81
|
+
readonly content: string;
|
|
82
|
+
} | {
|
|
83
|
+
readonly type: "tool-call";
|
|
84
|
+
readonly id: string;
|
|
85
|
+
readonly name: string;
|
|
86
|
+
readonly arguments: string;
|
|
87
|
+
} | {
|
|
88
|
+
readonly type: "usage";
|
|
89
|
+
readonly promptTokens: number;
|
|
90
|
+
readonly completionTokens: number;
|
|
91
|
+
readonly reasoningTokens?: number;
|
|
92
|
+
} | {
|
|
93
|
+
readonly type: "finish";
|
|
94
|
+
readonly reason: "stop" | "tool-calls" | "max-tokens" | "error" | "aborted";
|
|
95
|
+
} | {
|
|
96
|
+
readonly type: "error";
|
|
97
|
+
readonly error: string;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Provider retry policy — captured at registration time.
|
|
101
|
+
*/
|
|
102
|
+
export interface RetryPolicy {
|
|
103
|
+
/** Maximum number of retries. Default: 2. */
|
|
104
|
+
readonly maxRetries?: number;
|
|
105
|
+
/** Base delay in ms for exponential backoff. Default: 1000. */
|
|
106
|
+
readonly baseDelayMs?: number;
|
|
107
|
+
/** Maximum delay cap in ms. Default: 30000. */
|
|
108
|
+
readonly maxDelayMs?: number;
|
|
109
|
+
/** HTTP status codes that are retryable. */
|
|
110
|
+
readonly retryableStatuses?: readonly number[];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Provider metadata — returned by `providerInfo()`.
|
|
114
|
+
*/
|
|
115
|
+
export interface ProviderInfo {
|
|
116
|
+
/** Provider identifier (e.g., "deepseek", "openai", "anthropic"). */
|
|
117
|
+
readonly id: string;
|
|
118
|
+
/** Human-readable display name. */
|
|
119
|
+
readonly name: string;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolved model info — returned by `resolveModel()`.
|
|
123
|
+
*/
|
|
124
|
+
export interface ResolvedModelInfo {
|
|
125
|
+
/** Provider identifier. */
|
|
126
|
+
readonly provider: string;
|
|
127
|
+
/** Model identifier. */
|
|
128
|
+
readonly id: string;
|
|
129
|
+
/** Human-readable model name. */
|
|
130
|
+
readonly name: string;
|
|
131
|
+
/** Context window in tokens. */
|
|
132
|
+
readonly contextWindow?: number;
|
|
133
|
+
/** Model capabilities. */
|
|
134
|
+
readonly capabilities?: Partial<ModelCapabilities>;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Abstract LLM adapter — the Service Definition for model providers.
|
|
138
|
+
*
|
|
139
|
+
* Every provider implements this interface. The only required method is `stream()`.
|
|
140
|
+
* Everything else has sensible defaults.
|
|
141
|
+
*
|
|
142
|
+
* Adapters are stateless — all configuration is captured at registration time.
|
|
143
|
+
* The adapter receives only the `GenerateOptions` per call.
|
|
144
|
+
*/
|
|
145
|
+
export declare abstract class LlmAdapter {
|
|
146
|
+
/**
|
|
147
|
+
* Stream one model call as raw chunks (token-level deltas).
|
|
148
|
+
* This is the ONLY required method.
|
|
149
|
+
*/
|
|
150
|
+
abstract stream(options: GenerateOptions, signal?: AbortSignal): AsyncIterable<StreamChunk>;
|
|
151
|
+
/**
|
|
152
|
+
* Provider metadata — used for display and logging.
|
|
153
|
+
* Default: `{ id: "unknown", name: "Unknown Provider" }`.
|
|
154
|
+
*/
|
|
155
|
+
providerInfo(provider: string): ProviderInfo;
|
|
156
|
+
/**
|
|
157
|
+
* Provider-specific retry policy — captured at registration time.
|
|
158
|
+
* Default: undefined (use global defaults).
|
|
159
|
+
*/
|
|
160
|
+
providerRetryPolicy(provider: string): RetryPolicy | undefined;
|
|
161
|
+
/**
|
|
162
|
+
* List available models for a provider.
|
|
163
|
+
* Default: empty array.
|
|
164
|
+
*/
|
|
165
|
+
listModels(provider: string): Promise<readonly ResolvedModelInfo[]>;
|
|
166
|
+
/**
|
|
167
|
+
* Resolve a model identifier to full info.
|
|
168
|
+
* Default: returns the model id as-is.
|
|
169
|
+
*/
|
|
170
|
+
resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<ResolvedModelInfo>;
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Adapter — the abstract contract every provider must implement.
|
|
3
|
+
*
|
|
4
|
+
* An adapter is the minimal interface for streaming model calls.
|
|
5
|
+
* The only required method is `stream()`. Everything else has defaults.
|
|
6
|
+
*
|
|
7
|
+
* This is the "Service Definition" in the capability seam pattern:
|
|
8
|
+
* Service Definition (LlmAdapter) → Provider (concrete) → Consumer (agent loop)
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import type { LlmAdapter, GenerateOptions, StreamChunk } from "@vinhnt-sdk/llm";
|
|
13
|
+
*
|
|
14
|
+
* class MyAdapter implements LlmAdapter {
|
|
15
|
+
* async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
|
16
|
+
* // Call your LLM API here
|
|
17
|
+
* yield { type: "text", content: "Hello" };
|
|
18
|
+
* yield { type: "finish", reason: "stop" };
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Abstract LLM adapter — the Service Definition for model providers.
|
|
25
|
+
*
|
|
26
|
+
* Every provider implements this interface. The only required method is `stream()`.
|
|
27
|
+
* Everything else has sensible defaults.
|
|
28
|
+
*
|
|
29
|
+
* Adapters are stateless — all configuration is captured at registration time.
|
|
30
|
+
* The adapter receives only the `GenerateOptions` per call.
|
|
31
|
+
*/
|
|
32
|
+
export class LlmAdapter {
|
|
33
|
+
/**
|
|
34
|
+
* Provider metadata — used for display and logging.
|
|
35
|
+
* Default: `{ id: "unknown", name: "Unknown Provider" }`.
|
|
36
|
+
*/
|
|
37
|
+
providerInfo(provider) {
|
|
38
|
+
return { id: provider, name: provider };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Provider-specific retry policy — captured at registration time.
|
|
42
|
+
* Default: undefined (use global defaults).
|
|
43
|
+
*/
|
|
44
|
+
providerRetryPolicy(provider) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* List available models for a provider.
|
|
49
|
+
* Default: empty array.
|
|
50
|
+
*/
|
|
51
|
+
async listModels(provider) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a model identifier to full info.
|
|
56
|
+
* Default: returns the model id as-is.
|
|
57
|
+
*/
|
|
58
|
+
async resolveModel(provider, model, signal) {
|
|
59
|
+
return { provider, id: model, name: model };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAiGH;;;;;;;;GAQG;AACH,MAAM,OAAgB,UAAU;IAO9B;;;OAGG;IACH,YAAY,CAAC,QAAgB;QAC3B,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAC1C,CAAC;IAED;;;OAGG;IACH,mBAAmB,CAAC,QAAgB;QAClC,OAAO,SAAS,CAAC;IACnB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,UAAU,CAAC,QAAgB;QAC/B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,YAAY,CAAC,QAAgB,EAAE,KAAa,EAAE,MAAoB;QACtE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAC9C,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { type GenerateOptions, type StreamChunk, type RetryPolicy, type ProviderInfo, type ResolvedModelInfo, LlmAdapter, } from "./adapter.js";
|
|
2
|
+
export { type AdapterRegistrationHandle, LlmRegistry, AdapterRegistrationError, } from "./registry.js";
|
|
3
|
+
export { shouldRetry, calculateDelay, sleep, } from "./retry.js";
|
|
4
|
+
export { TokenMeter } from "./token-meter.js";
|
|
5
|
+
export { ModelCaller, type ModelCallerDeps, type ModelCallerPluginHooks, type ModelCallerLogger, } from "./model-caller.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,UAAU,GACX,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,KAAK,yBAAyB,EAC9B,WAAW,EACX,wBAAwB,GACzB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,WAAW,EACX,cAAc,EACd,KAAK,GACN,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,OAAO,EACL,WAAW,EACX,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,GACvB,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @vinhnt-sdk/llm
|
|
2
|
+
// LLM capability seam — adapter abstraction, registry, retry, token metering, model caller
|
|
3
|
+
export { LlmAdapter, } from "./adapter.js";
|
|
4
|
+
export { LlmRegistry, AdapterRegistrationError, } from "./registry.js";
|
|
5
|
+
export { shouldRetry, calculateDelay, sleep, } from "./retry.js";
|
|
6
|
+
export { TokenMeter } from "./token-meter.js";
|
|
7
|
+
// Re-export ModelCaller (previously in @vinhnt-sdk/model-caller)
|
|
8
|
+
export { ModelCaller, } from "./model-caller.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kBAAkB;AAClB,2FAA2F;AAE3F,OAAO,EAML,UAAU,GACX,MAAM,cAAc,CAAC;AAEtB,OAAO,EAEL,WAAW,EACX,wBAAwB,GACzB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,WAAW,EACX,cAAc,EACd,KAAK,GACN,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,iEAAiE;AACjE,OAAO,EACL,WAAW,GAIZ,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @vinhnt-sdk/model-caller
|
|
3
|
+
* Model caller kernel primitive: build requests, run non-streaming/streaming
|
|
4
|
+
* generation, fire model-call hooks, count tokens and emit token/cost events.
|
|
5
|
+
*/
|
|
6
|
+
import type { RunId, RequestContext, KnownRunEvent, ToolChoice, ResponseFormat, StreamOptions } from "@vinhnt-sdk/schema";
|
|
7
|
+
import type { ChatMessage, ModelProvider, ModelResponse, ModelRegistry } from "@vinhnt-sdk/schema";
|
|
8
|
+
import type { ToolDefinition } from "@vinhnt-sdk/tools";
|
|
9
|
+
/**
|
|
10
|
+
* Minimal structural hook surface used by the model caller.
|
|
11
|
+
*
|
|
12
|
+
* Hosts (e.g. core's `PluginManager`) only need to implement `fireHook` for
|
|
13
|
+
* the model-call hook names; no direct dependency on the full plugin contract.
|
|
14
|
+
*/
|
|
15
|
+
export interface ModelCallerPluginHooks {
|
|
16
|
+
fireHook(name: "onChatParams" | "onBeforeModelCall" | "onAfterModelCall" | "onTokenStreamed", data: Record<string, unknown>): Promise<{
|
|
17
|
+
modified: Record<string, unknown>;
|
|
18
|
+
} | null>;
|
|
19
|
+
}
|
|
20
|
+
/** Minimal structural logger used by the model caller (host Logger satisfies it). */
|
|
21
|
+
export interface ModelCallerLogger {
|
|
22
|
+
info(message: string, ...args: unknown[]): void;
|
|
23
|
+
}
|
|
24
|
+
/** Dependencies required by {@link ModelCaller}. */
|
|
25
|
+
export interface ModelCallerDeps {
|
|
26
|
+
defaultModel: ModelProvider;
|
|
27
|
+
readonly modelRegistry: ModelRegistry | undefined;
|
|
28
|
+
maxTokens: number;
|
|
29
|
+
thinkingBudget: number;
|
|
30
|
+
thinkingPrompt: string;
|
|
31
|
+
readonly pluginManager: ModelCallerPluginHooks | undefined;
|
|
32
|
+
readonly logger: ModelCallerLogger | undefined;
|
|
33
|
+
emitEvent(event: Omit<KnownRunEvent, "sequence">, persist?: boolean): Promise<void>;
|
|
34
|
+
modelForRun(runId: RunId): ModelProvider | undefined;
|
|
35
|
+
setModelForRun(runId: RunId, model: ModelProvider): void;
|
|
36
|
+
getAvailableTools(runId: RunId): readonly ToolDefinition[];
|
|
37
|
+
/** OpenAI: tool_choice — controls tool calling behavior. */
|
|
38
|
+
readonly toolChoice?: ToolChoice;
|
|
39
|
+
/** OpenAI: parallel_tool_calls — whether to allow parallel tool calls. */
|
|
40
|
+
readonly parallelToolCalls?: boolean;
|
|
41
|
+
/** OpenAI: response_format — controls output format. */
|
|
42
|
+
readonly responseFormat?: ResponseFormat;
|
|
43
|
+
/** OpenAI: stream_options — options for streaming. */
|
|
44
|
+
readonly streamOptions?: StreamOptions;
|
|
45
|
+
/** OpenAI: presence_penalty — penalizes tokens based on presence. */
|
|
46
|
+
readonly presencePenalty?: number;
|
|
47
|
+
/** OpenAI: frequency_penalty — penalizes tokens based on frequency. */
|
|
48
|
+
readonly frequencyPenalty?: number;
|
|
49
|
+
/** OpenAI: logit_bias — token-level logit biases. */
|
|
50
|
+
readonly logitBias?: Record<string, number>;
|
|
51
|
+
/** OpenAI: seed — for reproducible outputs. */
|
|
52
|
+
readonly seed?: number;
|
|
53
|
+
/** OpenAI: user — end-user identifier. */
|
|
54
|
+
readonly user?: string;
|
|
55
|
+
/** OpenAI: logprobs — return log probabilities. */
|
|
56
|
+
readonly logprobs?: boolean;
|
|
57
|
+
/** OpenAI: top_logprobs — number of top logprobs per token. */
|
|
58
|
+
readonly topLogprobs?: number;
|
|
59
|
+
/** OpenAI: max_completion_tokens — for o-series models. */
|
|
60
|
+
readonly maxCompletionTokens?: number;
|
|
61
|
+
/** OpenAI: reasoning_effort — controls reasoning token budget. */
|
|
62
|
+
readonly reasoningEffort?: string;
|
|
63
|
+
}
|
|
64
|
+
/** Runs model generation (streaming and non-streaming) with hooks, token counting and cost/token events. */
|
|
65
|
+
export declare class ModelCaller {
|
|
66
|
+
private readonly deps;
|
|
67
|
+
constructor(deps: ModelCallerDeps);
|
|
68
|
+
/** Swap the default model at runtime (config hot-reload). */
|
|
69
|
+
setDefaultModel(model: ModelProvider): void;
|
|
70
|
+
/** Swap runtime-tunable generation settings at runtime (config hot-reload). */
|
|
71
|
+
setRuntimeOptions(options: Partial<Pick<ModelCallerDeps, "maxTokens" | "thinkingBudget" | "thinkingPrompt">>): void;
|
|
72
|
+
getDefaultModel(): ModelProvider;
|
|
73
|
+
resolveAgentModel(agent: {
|
|
74
|
+
profile: {
|
|
75
|
+
model?: string;
|
|
76
|
+
};
|
|
77
|
+
}, runId?: RunId): ModelProvider;
|
|
78
|
+
getActiveModel(runId: RunId): ModelProvider;
|
|
79
|
+
callModelStream(messages: ChatMessage[], step: number, runId: RunId, ctx: RequestContext, signal: AbortSignal, agentMaxTokens?: number, disableTools?: boolean): Promise<ModelResponse>;
|
|
80
|
+
doThinkingStep(messages: ChatMessage[], step: number, runId: RunId, ctx: RequestContext, signal: AbortSignal): Promise<void>;
|
|
81
|
+
calculateCost(inputTokens: number, outputTokens: number, model?: ModelProvider): number | undefined;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=model-caller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-caller.d.ts","sourceRoot":"","sources":["../src/model-caller.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @vinhnt-sdk/model-caller
|
|
3
|
+
* Model caller kernel primitive: build requests, run non-streaming/streaming
|
|
4
|
+
* generation, fire model-call hooks, count tokens and emit token/cost events.
|
|
5
|
+
*/
|
|
6
|
+
import { VntError } from "@vinhnt-sdk/schema";
|
|
7
|
+
import { getTextContent } from "@vinhnt-sdk/schema";
|
|
8
|
+
function emitTC(runId, traceId, data) {
|
|
9
|
+
return { id: crypto.randomUUID(), runId, type: "token.counted", occurredAt: new Date().toISOString(), traceId, data };
|
|
10
|
+
}
|
|
11
|
+
function emitMC(runId, traceId, data) {
|
|
12
|
+
return { id: crypto.randomUUID(), runId, type: "model.cost", occurredAt: new Date().toISOString(), traceId, data };
|
|
13
|
+
}
|
|
14
|
+
/** Runs model generation (streaming and non-streaming) with hooks, token counting and cost/token events. */
|
|
15
|
+
export class ModelCaller {
|
|
16
|
+
deps;
|
|
17
|
+
constructor(deps) {
|
|
18
|
+
this.deps = deps;
|
|
19
|
+
}
|
|
20
|
+
/** Swap the default model at runtime (config hot-reload). */
|
|
21
|
+
setDefaultModel(model) {
|
|
22
|
+
this.deps.defaultModel = model;
|
|
23
|
+
}
|
|
24
|
+
/** Swap runtime-tunable generation settings at runtime (config hot-reload). */
|
|
25
|
+
setRuntimeOptions(options) {
|
|
26
|
+
if (options.maxTokens !== undefined)
|
|
27
|
+
this.deps.maxTokens = options.maxTokens;
|
|
28
|
+
if (options.thinkingBudget !== undefined)
|
|
29
|
+
this.deps.thinkingBudget = options.thinkingBudget;
|
|
30
|
+
if (options.thinkingPrompt !== undefined)
|
|
31
|
+
this.deps.thinkingPrompt = options.thinkingPrompt;
|
|
32
|
+
}
|
|
33
|
+
getDefaultModel() {
|
|
34
|
+
return this.deps.defaultModel;
|
|
35
|
+
}
|
|
36
|
+
resolveAgentModel(agent, runId) {
|
|
37
|
+
const preferred = agent?.profile?.model;
|
|
38
|
+
if (preferred && this.deps.modelRegistry) {
|
|
39
|
+
const provider = this.deps.modelRegistry.get(preferred);
|
|
40
|
+
if (provider) {
|
|
41
|
+
if (runId)
|
|
42
|
+
this.deps.setModelForRun(runId, provider);
|
|
43
|
+
return provider;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (runId)
|
|
47
|
+
this.deps.setModelForRun(runId, this.deps.defaultModel);
|
|
48
|
+
return this.deps.defaultModel;
|
|
49
|
+
}
|
|
50
|
+
getActiveModel(runId) {
|
|
51
|
+
return this.deps.modelForRun(runId) ?? this.deps.defaultModel;
|
|
52
|
+
}
|
|
53
|
+
async callModelStream(messages, step, runId, ctx, signal, agentMaxTokens, disableTools) {
|
|
54
|
+
const availableTools = disableTools ? [] : this.deps.getAvailableTools(runId);
|
|
55
|
+
const thinkingBudget = this.deps.thinkingBudget > 0 ? this.deps.thinkingBudget : undefined;
|
|
56
|
+
let request = {
|
|
57
|
+
messages, tools: availableTools, maxTokens: agentMaxTokens ?? this.deps.maxTokens,
|
|
58
|
+
...(thinkingBudget !== undefined ? { thinkingBudget } : {}),
|
|
59
|
+
...(this.deps.thinkingPrompt ? { thinkingPrompt: this.deps.thinkingPrompt } : {}),
|
|
60
|
+
// OpenAI fields passthrough
|
|
61
|
+
...(this.deps.toolChoice !== undefined ? { toolChoice: this.deps.toolChoice } : {}),
|
|
62
|
+
...(this.deps.parallelToolCalls !== undefined ? { parallelToolCalls: this.deps.parallelToolCalls } : {}),
|
|
63
|
+
...(this.deps.responseFormat !== undefined ? { responseFormat: this.deps.responseFormat } : {}),
|
|
64
|
+
...(this.deps.streamOptions !== undefined ? { streamOptions: this.deps.streamOptions } : {}),
|
|
65
|
+
...(this.deps.presencePenalty !== undefined ? { presencePenalty: this.deps.presencePenalty } : {}),
|
|
66
|
+
...(this.deps.frequencyPenalty !== undefined ? { frequencyPenalty: this.deps.frequencyPenalty } : {}),
|
|
67
|
+
...(this.deps.logitBias !== undefined ? { logitBias: this.deps.logitBias } : {}),
|
|
68
|
+
...(this.deps.seed !== undefined ? { seed: this.deps.seed } : {}),
|
|
69
|
+
...(this.deps.user !== undefined ? { user: this.deps.user } : {}),
|
|
70
|
+
...(this.deps.logprobs !== undefined ? { logprobs: this.deps.logprobs } : {}),
|
|
71
|
+
...(this.deps.topLogprobs !== undefined ? { topLogprobs: this.deps.topLogprobs } : {}),
|
|
72
|
+
...(this.deps.maxCompletionTokens !== undefined ? { maxCompletionTokens: this.deps.maxCompletionTokens } : {}),
|
|
73
|
+
...(this.deps.reasoningEffort !== undefined ? { reasoningEffort: this.deps.reasoningEffort } : {}),
|
|
74
|
+
};
|
|
75
|
+
const chatParamsResult = await this.deps.pluginManager?.fireHook("onChatParams", {
|
|
76
|
+
request: request,
|
|
77
|
+
});
|
|
78
|
+
if (chatParamsResult?.modified?.request) {
|
|
79
|
+
request = chatParamsResult.modified.request;
|
|
80
|
+
}
|
|
81
|
+
// P1-F: onBeforeModelCall — intercept/modify the request right before the model call.
|
|
82
|
+
const beforeCallResult = await this.deps.pluginManager?.fireHook("onBeforeModelCall", {
|
|
83
|
+
request: request,
|
|
84
|
+
});
|
|
85
|
+
if (beforeCallResult?.modified?.request) {
|
|
86
|
+
request = beforeCallResult.modified.request;
|
|
87
|
+
}
|
|
88
|
+
const model = this.getActiveModel(runId);
|
|
89
|
+
const modelName = model.model ?? "unknown";
|
|
90
|
+
const startTime = performance.now();
|
|
91
|
+
let inputTokens = 0;
|
|
92
|
+
let outputTokens = 0;
|
|
93
|
+
let reasoningTokens = 0;
|
|
94
|
+
const modelHasTokens = !!model.countTokens;
|
|
95
|
+
// RV-42: the input count here is only a local FALLBACK estimate — the
|
|
96
|
+
// authoritative `token.counted` is emitted exactly once per call, after the
|
|
97
|
+
// model call, from the provider-reported usage (or this estimate).
|
|
98
|
+
if (modelHasTokens) {
|
|
99
|
+
inputTokens = messages.reduce((sum, m) => sum + model.countTokens(getTextContent(m.content)), 0);
|
|
100
|
+
}
|
|
101
|
+
let content = "";
|
|
102
|
+
const toolCalls = [];
|
|
103
|
+
if (!model.stream) {
|
|
104
|
+
const res = await model.generate(request, signal);
|
|
105
|
+
// P1-F: onAfterModelCall — intercept/modify the response after the model call.
|
|
106
|
+
const afterCallResult = await this.deps.pluginManager?.fireHook("onAfterModelCall", {
|
|
107
|
+
response: res,
|
|
108
|
+
});
|
|
109
|
+
const effectiveRes = (afterCallResult?.modified?.response ?? res);
|
|
110
|
+
let source = "api";
|
|
111
|
+
const input = effectiveRes.usage?.inputTokens ?? effectiveRes.usage?.promptTokens ?? 0;
|
|
112
|
+
const output = effectiveRes.usage?.outputTokens ?? effectiveRes.usage?.completionTokens ?? 0;
|
|
113
|
+
if (input > 0 && output > 0) {
|
|
114
|
+
inputTokens = input;
|
|
115
|
+
outputTokens = output;
|
|
116
|
+
}
|
|
117
|
+
else if (modelHasTokens && effectiveRes.content) {
|
|
118
|
+
const localOut = model.countTokens(effectiveRes.content);
|
|
119
|
+
if (input > 0)
|
|
120
|
+
inputTokens = input;
|
|
121
|
+
outputTokens = localOut;
|
|
122
|
+
source = localOut === (output || -1) ? "api" : "local";
|
|
123
|
+
}
|
|
124
|
+
await this.deps.emitEvent(emitTC(runId, ctx.traceId, { inputTokens, outputTokens, step, source }));
|
|
125
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
126
|
+
const cost = this.calculateCost(inputTokens, outputTokens, model) ?? 0;
|
|
127
|
+
await this.deps.emitEvent(emitMC(runId, ctx.traceId, { inputTokens, outputTokens, cost, model: modelName, durationMs, step }));
|
|
128
|
+
const p = model?.pricing;
|
|
129
|
+
this.deps.logger?.info(`[llm] ${modelName}: ${inputTokens} in, ${outputTokens} out, $${cost.toFixed(6)}, ${durationMs}ms${p ? ` ($${p.input}/${p.output} per 1M)` : ""}`);
|
|
130
|
+
// RV-42: surface the authoritative usage on the response when the provider
|
|
131
|
+
// did not already report it, so the run loop can budget without countTokens.
|
|
132
|
+
return effectiveRes.usage
|
|
133
|
+
? effectiveRes
|
|
134
|
+
: inputTokens > 0 || outputTokens > 0
|
|
135
|
+
? { ...effectiveRes, usage: { promptTokens: inputTokens, completionTokens: outputTokens } }
|
|
136
|
+
: effectiveRes;
|
|
137
|
+
}
|
|
138
|
+
for await (const event of model.stream(request, signal)) {
|
|
139
|
+
if (signal?.aborted)
|
|
140
|
+
break;
|
|
141
|
+
switch (event.type) {
|
|
142
|
+
case "text":
|
|
143
|
+
content += event.content;
|
|
144
|
+
await this.deps.emitEvent({
|
|
145
|
+
id: crypto.randomUUID(), runId, type: "token.streamed",
|
|
146
|
+
occurredAt: new Date().toISOString(), traceId: ctx.traceId,
|
|
147
|
+
data: { content: event.content, step },
|
|
148
|
+
}, false);
|
|
149
|
+
await this.deps.pluginManager?.fireHook("onTokenStreamed", { content: event.content, step });
|
|
150
|
+
break;
|
|
151
|
+
case "thinking":
|
|
152
|
+
// RV-44: DeepSeek reasoner chain-of-thought — surface as
|
|
153
|
+
// thinking.content like doThinkingStep, never dropped.
|
|
154
|
+
await this.deps.emitEvent({
|
|
155
|
+
id: crypto.randomUUID(), runId, type: "thinking.content",
|
|
156
|
+
occurredAt: new Date().toISOString(), traceId: ctx.traceId,
|
|
157
|
+
data: { content: event.content, step },
|
|
158
|
+
}, false);
|
|
159
|
+
break;
|
|
160
|
+
case "tool_call":
|
|
161
|
+
toolCalls.push({ id: event.id, name: event.name, args: event.args });
|
|
162
|
+
break;
|
|
163
|
+
case "usage":
|
|
164
|
+
inputTokens = event.inputTokens;
|
|
165
|
+
outputTokens = event.outputTokens;
|
|
166
|
+
reasoningTokens = event.reasoningTokens ?? 0;
|
|
167
|
+
break;
|
|
168
|
+
case "done":
|
|
169
|
+
break;
|
|
170
|
+
case "error":
|
|
171
|
+
// Surface as a structured, non-retryable error so the circuit
|
|
172
|
+
// breaker never retries a truncated/malformed stream.
|
|
173
|
+
throw new VntError(event.error, { retryable: false });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// A cancelled stream must never commit partial content or partially
|
|
177
|
+
// assembled tool calls as a successful response.
|
|
178
|
+
if (signal?.aborted) {
|
|
179
|
+
throw new DOMException("Aborted", "AbortError");
|
|
180
|
+
}
|
|
181
|
+
let source = "api";
|
|
182
|
+
if (outputTokens === 0 && modelHasTokens && content) {
|
|
183
|
+
outputTokens = model.countTokens(content);
|
|
184
|
+
source = "local";
|
|
185
|
+
}
|
|
186
|
+
// P1-F: onAfterModelCall — intercept/modify the response after streaming completes.
|
|
187
|
+
// RV-42: attach the authoritative usage (provider-reported or local fallback).
|
|
188
|
+
const streamedResponse = {
|
|
189
|
+
...(toolCalls.length > 0 ? { content, toolCalls } : { content }),
|
|
190
|
+
...(inputTokens > 0 || outputTokens > 0
|
|
191
|
+
? { usage: { promptTokens: inputTokens, completionTokens: outputTokens, ...(reasoningTokens > 0 ? { reasoningTokens } : {}) } }
|
|
192
|
+
: {}),
|
|
193
|
+
};
|
|
194
|
+
const afterCallResult = await this.deps.pluginManager?.fireHook("onAfterModelCall", {
|
|
195
|
+
response: streamedResponse,
|
|
196
|
+
});
|
|
197
|
+
const effectiveRes = (afterCallResult?.modified?.response ?? streamedResponse);
|
|
198
|
+
if (inputTokens > 0 || outputTokens > 0) {
|
|
199
|
+
await this.deps.emitEvent(emitTC(runId, ctx.traceId, { inputTokens, outputTokens, step, source }));
|
|
200
|
+
}
|
|
201
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
202
|
+
const cost = this.calculateCost(inputTokens, outputTokens, model) ?? 0;
|
|
203
|
+
await this.deps.emitEvent(emitMC(runId, ctx.traceId, { inputTokens, outputTokens, cost, model: modelName, durationMs, step }));
|
|
204
|
+
const p = model?.pricing;
|
|
205
|
+
this.deps.logger?.info(`[llm] ${modelName}: ${inputTokens} in, ${outputTokens} out, $${cost.toFixed(6)}, ${durationMs}ms${p ? ` ($${p.input}/${p.output} per 1M)` : ""}`);
|
|
206
|
+
// RV-42: a hook may have replaced the response without usage — surface the
|
|
207
|
+
// authoritative usage so downstream token budgeting is not a no-op.
|
|
208
|
+
return effectiveRes.usage
|
|
209
|
+
? effectiveRes
|
|
210
|
+
: inputTokens > 0 || outputTokens > 0
|
|
211
|
+
? { ...effectiveRes, usage: { promptTokens: inputTokens, completionTokens: outputTokens } }
|
|
212
|
+
: effectiveRes;
|
|
213
|
+
}
|
|
214
|
+
async doThinkingStep(messages, step, runId, ctx, signal) {
|
|
215
|
+
await this.deps.emitEvent({
|
|
216
|
+
id: crypto.randomUUID(), runId, type: "thinking.started",
|
|
217
|
+
occurredAt: new Date().toISOString(), traceId: ctx.traceId,
|
|
218
|
+
data: { step },
|
|
219
|
+
});
|
|
220
|
+
let thinking = "";
|
|
221
|
+
const thinkModel = this.getActiveModel(runId);
|
|
222
|
+
if (thinkModel.stream) {
|
|
223
|
+
for await (const event of thinkModel.stream({ messages: [...messages, { role: "system", content: this.deps.thinkingPrompt }], tools: [] }, signal)) {
|
|
224
|
+
if (signal?.aborted)
|
|
225
|
+
break;
|
|
226
|
+
if (event.type === "text") {
|
|
227
|
+
thinking += event.content;
|
|
228
|
+
await this.deps.emitEvent({
|
|
229
|
+
id: crypto.randomUUID(), runId, type: "thinking.content",
|
|
230
|
+
occurredAt: new Date().toISOString(), traceId: ctx.traceId,
|
|
231
|
+
data: { content: event.content, step },
|
|
232
|
+
}, false);
|
|
233
|
+
}
|
|
234
|
+
else if (event.type === "error") {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
try {
|
|
241
|
+
const res = await thinkModel.generate({ messages: [...messages, { role: "system", content: this.deps.thinkingPrompt }], tools: [], maxTokens: this.deps.thinkingBudget }, signal);
|
|
242
|
+
thinking = res.content;
|
|
243
|
+
if (thinking) {
|
|
244
|
+
await this.deps.emitEvent({
|
|
245
|
+
id: crypto.randomUUID(), runId, type: "thinking.content",
|
|
246
|
+
occurredAt: new Date().toISOString(), traceId: ctx.traceId,
|
|
247
|
+
data: { content: thinking, step },
|
|
248
|
+
}, false);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (thinking.trim()) {
|
|
256
|
+
messages.push({
|
|
257
|
+
role: "system",
|
|
258
|
+
content: `[Thinking from previous pass]\n${thinking.trim()}`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
await this.deps.emitEvent({
|
|
262
|
+
id: crypto.randomUUID(), runId, type: "thinking.completed",
|
|
263
|
+
occurredAt: new Date().toISOString(), traceId: ctx.traceId,
|
|
264
|
+
data: { content: thinking, step },
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
calculateCost(inputTokens, outputTokens, model) {
|
|
268
|
+
const p = model?.pricing;
|
|
269
|
+
if (!p)
|
|
270
|
+
return undefined;
|
|
271
|
+
const inputCost = (inputTokens * p.input) / 1_000_000;
|
|
272
|
+
const outputCost = (outputTokens * p.output) / 1_000_000;
|
|
273
|
+
return Number((inputCost + outputCost).toFixed(6));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=model-caller.js.map
|