@proteinjs/conversation 2.7.2 → 3.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.
- package/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/src/CodegenConversation.js +1 -1
- package/dist/src/CodegenConversation.js.map +1 -1
- package/dist/src/Conversation.d.ts +173 -99
- package/dist/src/Conversation.d.ts.map +1 -1
- package/dist/src/Conversation.js +903 -502
- package/dist/src/Conversation.js.map +1 -1
- package/dist/src/OpenAi.d.ts +20 -0
- package/dist/src/OpenAi.d.ts.map +1 -1
- package/dist/src/OpenAi.js +16 -0
- package/dist/src/OpenAi.js.map +1 -1
- package/dist/src/OpenAiStreamProcessor.d.ts +9 -3
- package/dist/src/OpenAiStreamProcessor.d.ts.map +1 -1
- package/dist/src/OpenAiStreamProcessor.js +5 -3
- package/dist/src/OpenAiStreamProcessor.js.map +1 -1
- package/dist/src/UsageData.d.ts.map +1 -1
- package/dist/src/UsageData.js +22 -0
- package/dist/src/UsageData.js.map +1 -1
- package/dist/src/code_template/Code.d.ts.map +1 -1
- package/dist/src/code_template/Code.js +8 -2
- package/dist/src/code_template/Code.js.map +1 -1
- package/dist/src/resolveModel.d.ts +17 -0
- package/dist/src/resolveModel.d.ts.map +1 -0
- package/dist/src/resolveModel.js +121 -0
- package/dist/src/resolveModel.js.map +1 -0
- package/dist/test/conversation/conversation.generateObject.test.d.ts +2 -0
- package/dist/test/conversation/conversation.generateObject.test.d.ts.map +1 -0
- package/dist/test/conversation/conversation.generateObject.test.js +153 -0
- package/dist/test/conversation/conversation.generateObject.test.js.map +1 -0
- package/dist/test/conversation/conversation.generateResponse.test.d.ts +2 -0
- package/dist/test/conversation/conversation.generateResponse.test.d.ts.map +1 -0
- package/dist/test/conversation/conversation.generateResponse.test.js +167 -0
- package/dist/test/conversation/conversation.generateResponse.test.js.map +1 -0
- package/dist/test/conversation/conversation.generateStream.test.d.ts +2 -0
- package/dist/test/conversation/conversation.generateStream.test.d.ts.map +1 -0
- package/dist/test/conversation/conversation.generateStream.test.js +255 -0
- package/dist/test/conversation/conversation.generateStream.test.js.map +1 -0
- package/index.ts +5 -0
- package/package.json +7 -2
- package/src/CodegenConversation.ts +1 -1
- package/src/Conversation.ts +938 -496
- package/src/OpenAi.ts +20 -0
- package/src/OpenAiStreamProcessor.ts +9 -3
- package/src/UsageData.ts +25 -0
- package/src/code_template/Code.ts +5 -1
- package/src/resolveModel.ts +130 -0
- package/test/conversation/conversation.generateObject.test.ts +132 -0
- package/test/conversation/conversation.generateResponse.test.ts +132 -0
- package/test/conversation/conversation.generateStream.test.ts +173 -0
package/src/Conversation.ts
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { Function } from './Function';
|
|
1
|
+
import type { LanguageModel, ToolSet, LanguageModelUsage, ReasoningOutput } from 'ai';
|
|
2
|
+
import type { LanguageModelV3Source } from '@ai-sdk/provider';
|
|
3
|
+
import { streamText, generateObject as aiGenerateObject, jsonSchema, stepCountIs } from 'ai';
|
|
4
|
+
import type { RepairTextFunction } from 'ai';
|
|
6
5
|
import { Logger, LogLevel } from '@proteinjs/logger';
|
|
7
|
-
import { Fs } from '@proteinjs/util-node';
|
|
8
|
-
import { MessageModerator } from './history/MessageModerator';
|
|
9
6
|
import { ConversationModule } from './ConversationModule';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
7
|
+
import { Function } from './Function';
|
|
8
|
+
import { MessageModerator } from './history/MessageModerator';
|
|
9
|
+
import { MessageHistory } from './history/MessageHistory';
|
|
10
|
+
import { UsageData, UsageDataAccumulator, TokenUsage } from './UsageData';
|
|
11
|
+
import { resolveModel, inferProvider } from './resolveModel';
|
|
12
|
+
import type { ToolInvocationProgressEvent, ToolInvocationResult } from './OpenAi';
|
|
13
|
+
import type { OpenAiResponses, OpenAiServiceTier } from './OpenAiResponses';
|
|
14
|
+
import type { ChatCompletionMessageParam } from 'openai/resources/chat';
|
|
15
|
+
import { TiktokenModel } from 'tiktoken';
|
|
16
|
+
|
|
17
|
+
// Re-export for convenience
|
|
18
|
+
export type { ToolInvocationProgressEvent, ToolInvocationResult } from './OpenAi';
|
|
19
|
+
|
|
20
|
+
// ────────────────────────────────────────────────────────────────
|
|
21
|
+
// Public types
|
|
22
|
+
// ────────────────────────────────────────────────────────────────
|
|
15
23
|
|
|
16
24
|
export type ConversationParams = {
|
|
17
25
|
name: string;
|
|
18
26
|
modules?: ConversationModule[];
|
|
19
27
|
logLevel?: LogLevel;
|
|
28
|
+
defaultModel?: LanguageModel | string;
|
|
20
29
|
limits?: {
|
|
21
30
|
enforceLimits?: boolean;
|
|
22
31
|
maxMessagesInHistory?: number;
|
|
@@ -24,47 +33,122 @@ export type ConversationParams = {
|
|
|
24
33
|
};
|
|
25
34
|
};
|
|
26
35
|
|
|
27
|
-
/**
|
|
28
|
-
export type
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
/** Message format accepted by Conversation methods. */
|
|
37
|
+
export type ConversationMessage =
|
|
38
|
+
| string
|
|
39
|
+
| {
|
|
40
|
+
role: 'system' | 'user' | 'assistant' | 'developer' | 'tool' | 'function' | (string & {});
|
|
41
|
+
content?: string | null | unknown;
|
|
42
|
+
};
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
/** @deprecated Use `GenerateObjectResult` instead. */
|
|
45
|
+
export type GenerateObjectOutcome<T> = GenerateObjectResult<T>;
|
|
34
46
|
|
|
47
|
+
export type ReasoningEffort = 'none' | 'low' | 'medium' | 'high' | 'max' | 'xhigh';
|
|
48
|
+
|
|
49
|
+
export type GenerateStreamParams = {
|
|
50
|
+
messages: ConversationMessage[];
|
|
51
|
+
model?: LanguageModel | string;
|
|
52
|
+
reasoningEffort?: ReasoningEffort;
|
|
53
|
+
webSearch?: boolean;
|
|
54
|
+
tools?: Function[];
|
|
55
|
+
onToolInvocation?: (evt: ToolInvocationProgressEvent) => void;
|
|
56
|
+
onUsageData?: (usageData: UsageData) => Promise<void>;
|
|
35
57
|
abortSignal?: AbortSignal;
|
|
58
|
+
maxToolCalls?: number;
|
|
36
59
|
|
|
37
|
-
|
|
38
|
-
|
|
60
|
+
// OpenAI-specific
|
|
61
|
+
backgroundMode?: boolean;
|
|
62
|
+
serviceTier?: OpenAiServiceTier;
|
|
63
|
+
maxBackgroundWaitMs?: number;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** A single part emitted by the interleaved full stream. */
|
|
67
|
+
export type StreamPart =
|
|
68
|
+
| { type: 'text-delta'; textDelta: string }
|
|
69
|
+
| { type: 'reasoning-delta'; textDelta: string }
|
|
70
|
+
| { type: 'source'; source: StreamSource };
|
|
71
|
+
|
|
72
|
+
/** The result of generateStream. All properties are available immediately for streaming consumption. */
|
|
73
|
+
export type StreamResult = {
|
|
74
|
+
/** Async iterable of text content chunks. */
|
|
75
|
+
textStream: AsyncIterable<string>;
|
|
76
|
+
/** Async iterable of reasoning/thinking chunks (empty if model doesn't support reasoning). */
|
|
77
|
+
reasoningStream: AsyncIterable<string>;
|
|
78
|
+
/**
|
|
79
|
+
* Interleaved stream of all parts (text, reasoning, sources) for real-time
|
|
80
|
+
* consumption. Prefer this over consuming `textStream` and `reasoningStream`
|
|
81
|
+
* separately, since those may share the same underlying data source and
|
|
82
|
+
* cannot be consumed concurrently.
|
|
83
|
+
*/
|
|
84
|
+
fullStream: AsyncIterable<StreamPart>;
|
|
85
|
+
/** Resolves to the full text when generation completes. */
|
|
86
|
+
text: Promise<string>;
|
|
87
|
+
/** Resolves to the full reasoning text when generation completes. */
|
|
88
|
+
reasoning: Promise<string>;
|
|
89
|
+
/** Resolves to source citations (web search results, etc.). */
|
|
90
|
+
sources: Promise<StreamSource[]>;
|
|
91
|
+
/** Resolves to usage data when generation completes. */
|
|
92
|
+
usage: Promise<UsageData>;
|
|
93
|
+
/** Resolves to tool invocation results. */
|
|
94
|
+
toolInvocations: Promise<ToolInvocationResult[]>;
|
|
95
|
+
};
|
|
39
96
|
|
|
40
|
-
|
|
97
|
+
export type StreamSource = {
|
|
98
|
+
url?: string;
|
|
99
|
+
title?: string;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type GenerateObjectParams<T> = {
|
|
103
|
+
messages: ConversationMessage[];
|
|
104
|
+
model?: LanguageModel | string;
|
|
105
|
+
schema: any; // Zod schema or JSON Schema
|
|
106
|
+
reasoningEffort?: ReasoningEffort;
|
|
41
107
|
temperature?: number;
|
|
42
108
|
topP?: number;
|
|
43
109
|
maxTokens?: number;
|
|
44
|
-
|
|
45
|
-
/** Usage callback */
|
|
110
|
+
abortSignal?: AbortSignal;
|
|
46
111
|
onUsageData?: (usageData: UsageData) => Promise<void>;
|
|
47
|
-
|
|
48
|
-
/** Append final JSON to history as assistant text; default true */
|
|
49
112
|
recordInHistory?: boolean;
|
|
50
113
|
|
|
51
|
-
|
|
52
|
-
|
|
114
|
+
// OpenAI-specific
|
|
115
|
+
backgroundMode?: boolean;
|
|
116
|
+
serviceTier?: OpenAiServiceTier;
|
|
117
|
+
maxBackgroundWaitMs?: number;
|
|
53
118
|
};
|
|
54
119
|
|
|
55
|
-
export type
|
|
56
|
-
object: T;
|
|
57
|
-
|
|
120
|
+
export type GenerateObjectResult<T> = {
|
|
121
|
+
object: T;
|
|
122
|
+
usage: UsageData;
|
|
123
|
+
reasoning?: string;
|
|
124
|
+
toolInvocations: ToolInvocationResult[];
|
|
58
125
|
};
|
|
59
126
|
|
|
127
|
+
export type GenerateResponseResult = {
|
|
128
|
+
text: string;
|
|
129
|
+
reasoning?: string;
|
|
130
|
+
sources: StreamSource[];
|
|
131
|
+
usage: UsageData;
|
|
132
|
+
toolInvocations: ToolInvocationResult[];
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// ────────────────────────────────────────────────────────────────
|
|
136
|
+
// Default constants
|
|
137
|
+
// ────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
const DEFAULT_MODEL = 'gpt-4o' as TiktokenModel;
|
|
140
|
+
const DEFAULT_TOKEN_LIMIT = 50_000;
|
|
141
|
+
|
|
142
|
+
// ────────────────────────────────────────────────────────────────
|
|
143
|
+
// Conversation class
|
|
144
|
+
// ────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
60
146
|
export class Conversation {
|
|
61
|
-
private tokenLimit
|
|
62
|
-
private history;
|
|
63
|
-
private systemMessages:
|
|
147
|
+
private tokenLimit: number;
|
|
148
|
+
private history: MessageHistory;
|
|
149
|
+
private systemMessages: ConversationMessage[] = [];
|
|
64
150
|
private functions: Function[] = [];
|
|
65
151
|
private messageModerators: MessageModerator[] = [];
|
|
66
|
-
private generatedCode = false;
|
|
67
|
-
private generatedList = false;
|
|
68
152
|
private logger: Logger;
|
|
69
153
|
private params: ConversationParams;
|
|
70
154
|
private modulesProcessed = false;
|
|
@@ -72,41 +156,312 @@ export class Conversation {
|
|
|
72
156
|
|
|
73
157
|
constructor(params: ConversationParams) {
|
|
74
158
|
this.params = params;
|
|
159
|
+
this.tokenLimit = params.limits?.tokenLimit ?? DEFAULT_TOKEN_LIMIT;
|
|
75
160
|
this.history = new MessageHistory({
|
|
76
161
|
maxMessages: params.limits?.maxMessagesInHistory,
|
|
77
162
|
enforceMessageLimit: params.limits?.enforceLimits,
|
|
78
163
|
});
|
|
79
164
|
this.logger = new Logger({ name: params.name, logLevel: params.logLevel });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ────────────────────────────────────────────────────────────
|
|
168
|
+
// Public API
|
|
169
|
+
// ────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Stream a text response from the model.
|
|
173
|
+
*
|
|
174
|
+
* Returns a `StreamResult` with async iterables for text and reasoning chunks,
|
|
175
|
+
* plus promises that resolve when generation completes.
|
|
176
|
+
*
|
|
177
|
+
* For OpenAI models with high reasoning effort or pro models, this may
|
|
178
|
+
* fall back to background/polling mode via `OpenAiResponses` and return
|
|
179
|
+
* the full result as a single-chunk stream.
|
|
180
|
+
*/
|
|
181
|
+
async generateStream(params: GenerateStreamParams): Promise<StreamResult> {
|
|
182
|
+
await this.ensureModulesProcessed();
|
|
183
|
+
|
|
184
|
+
const model = this.resolveModelInstance(params.model);
|
|
185
|
+
const modelString = this.getModelString(params.model);
|
|
186
|
+
const provider = inferProvider(params.model ?? this.params.defaultModel ?? DEFAULT_MODEL);
|
|
187
|
+
|
|
188
|
+
this.logger.info({
|
|
189
|
+
message: `generateStream`,
|
|
190
|
+
obj: { model: modelString, provider, reasoningEffort: params.reasoningEffort, webSearch: params.webSearch },
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Check if we should use background/polling mode (OpenAI-specific)
|
|
194
|
+
if (provider === 'openai' && this.shouldUseBackgroundMode(modelString, params)) {
|
|
195
|
+
return this.generateStreamViaPolling(params, modelString);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Build messages for the AI SDK
|
|
199
|
+
let messages = this.buildAiSdkMessages(params.messages);
|
|
200
|
+
|
|
201
|
+
// Google requires all system messages at the beginning of the conversation.
|
|
202
|
+
// Reorder so system messages come first, preserving relative order within
|
|
203
|
+
// each group.
|
|
204
|
+
if (provider === 'google') {
|
|
205
|
+
const system = messages.filter((m) => m.role === 'system');
|
|
206
|
+
const nonSystem = messages.filter((m) => m.role !== 'system');
|
|
207
|
+
messages = [...system, ...nonSystem];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Build tools from module functions + any extra tools
|
|
211
|
+
const allFunctions = [...this.functions, ...(params.tools ?? [])];
|
|
212
|
+
const tools = this.buildAiSdkTools(allFunctions);
|
|
213
|
+
|
|
214
|
+
// Build provider options
|
|
215
|
+
const providerOptions = this.buildProviderOptions(provider, params, modelString);
|
|
216
|
+
|
|
217
|
+
const result = streamText({
|
|
218
|
+
model,
|
|
219
|
+
messages,
|
|
220
|
+
tools: Object.keys(tools).length > 0 ? tools : undefined,
|
|
221
|
+
stopWhen: stepCountIs(params.maxToolCalls ?? 50),
|
|
222
|
+
abortSignal: params.abortSignal,
|
|
223
|
+
providerOptions,
|
|
224
|
+
...(params.webSearch && provider === 'openai' ? { toolChoice: 'auto' as const } : {}),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Build the StreamResult
|
|
228
|
+
const usagePromise = this.buildUsagePromise(result, modelString, params);
|
|
229
|
+
const toolInvocationsPromise = this.buildToolInvocationsPromise(result);
|
|
230
|
+
|
|
231
|
+
// Wrap ALL AI SDK promises with catch handlers to prevent unhandled rejections
|
|
232
|
+
// when the stream is aborted mid-generation. The AI SDK's internal transform
|
|
233
|
+
// stream throws NoOutputGeneratedError on flush if aborted before any output,
|
|
234
|
+
// rejecting finishReason, totalUsage, steps, text, etc. Any uncaught rejection
|
|
235
|
+
// crashes the Node process.
|
|
236
|
+
const safeText = Promise.resolve(result.text).catch(() => '');
|
|
237
|
+
const safeReasoning = Promise.resolve(result.reasoning)
|
|
238
|
+
.then((parts: ReasoningOutput[]) =>
|
|
239
|
+
parts
|
|
240
|
+
? parts
|
|
241
|
+
.filter((part) => part.type === 'reasoning')
|
|
242
|
+
.map((part) => part.text)
|
|
243
|
+
.join('')
|
|
244
|
+
: ''
|
|
245
|
+
)
|
|
246
|
+
.catch(() => '');
|
|
247
|
+
const safeSources = Promise.resolve(result.sources)
|
|
248
|
+
.then((s: LanguageModelV3Source[]) =>
|
|
249
|
+
(s ?? []).map((source) => ({
|
|
250
|
+
url: source.sourceType === 'url' ? source.url : undefined,
|
|
251
|
+
title: source.sourceType === 'url' ? source.title : undefined,
|
|
252
|
+
}))
|
|
253
|
+
)
|
|
254
|
+
.catch(() => [] as StreamSource[]);
|
|
255
|
+
const safeUsage = usagePromise.catch(
|
|
256
|
+
() =>
|
|
257
|
+
({
|
|
258
|
+
model: modelString,
|
|
259
|
+
initialRequestTokenUsage: {
|
|
260
|
+
inputTokens: 0,
|
|
261
|
+
cachedInputTokens: 0,
|
|
262
|
+
reasoningTokens: 0,
|
|
263
|
+
outputTokens: 0,
|
|
264
|
+
totalTokens: 0,
|
|
265
|
+
},
|
|
266
|
+
initialRequestCostUsd: { inputUsd: 0, cachedInputUsd: 0, reasoningUsd: 0, outputUsd: 0, totalUsd: 0 },
|
|
267
|
+
totalTokenUsage: {
|
|
268
|
+
inputTokens: 0,
|
|
269
|
+
cachedInputTokens: 0,
|
|
270
|
+
reasoningTokens: 0,
|
|
271
|
+
outputTokens: 0,
|
|
272
|
+
totalTokens: 0,
|
|
273
|
+
},
|
|
274
|
+
totalCostUsd: { inputUsd: 0, cachedInputUsd: 0, reasoningUsd: 0, outputUsd: 0, totalUsd: 0 },
|
|
275
|
+
totalRequestsToAssistant: 0,
|
|
276
|
+
callsPerTool: {},
|
|
277
|
+
totalToolCalls: 0,
|
|
278
|
+
}) as UsageData
|
|
279
|
+
);
|
|
280
|
+
const safeToolInvocations = toolInvocationsPromise.catch(() => [] as ToolInvocationResult[]);
|
|
281
|
+
|
|
282
|
+
// Catch remaining AI SDK promises that are rejected by NoOutputGeneratedError
|
|
283
|
+
// on flush when the stream is aborted before any output. The AI SDK's internal
|
|
284
|
+
// flush rejects _finishReason, _rawFinishReason, _totalUsage, and _steps.
|
|
285
|
+
// We already catch totalUsage and steps above; these catch the rest so the
|
|
286
|
+
// unhandled rejections don't crash the Node process.
|
|
287
|
+
Promise.resolve(result.finishReason).catch(() => {});
|
|
288
|
+
Promise.resolve((result as any).rawFinishReason).catch(() => {});
|
|
289
|
+
Promise.resolve(result.response).catch(() => {});
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
textStream: result.textStream,
|
|
293
|
+
reasoningStream: (async function* () {
|
|
294
|
+
// Reasoning is available via the promise after generation completes.
|
|
295
|
+
// For real-time streaming, use fullStream instead.
|
|
296
|
+
})(),
|
|
297
|
+
fullStream: this.mapFullStream(result.fullStream),
|
|
298
|
+
text: safeText,
|
|
299
|
+
reasoning: safeReasoning,
|
|
300
|
+
sources: safeSources,
|
|
301
|
+
usage: safeUsage,
|
|
302
|
+
toolInvocations: safeToolInvocations,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Generate a strongly-typed structured object from the model.
|
|
308
|
+
*
|
|
309
|
+
* This is promise-based (not streaming-first) to guarantee the
|
|
310
|
+
* type contract. Reasoning is available on the result after completion.
|
|
311
|
+
*
|
|
312
|
+
* For OpenAI models with high reasoning or pro models, this uses
|
|
313
|
+
* `OpenAiResponses` with background/polling mode.
|
|
314
|
+
*/
|
|
315
|
+
async generateObject<T>(params: GenerateObjectParams<T>): Promise<GenerateObjectResult<T>> {
|
|
316
|
+
await this.ensureModulesProcessed();
|
|
317
|
+
|
|
318
|
+
const model = this.resolveModelInstance(params.model);
|
|
319
|
+
const modelString = this.getModelString(params.model);
|
|
320
|
+
const provider = inferProvider(params.model ?? this.params.defaultModel ?? DEFAULT_MODEL);
|
|
321
|
+
|
|
322
|
+
// Check if we should use background/polling mode (OpenAI-specific)
|
|
323
|
+
if (provider === 'openai' && this.shouldUseBackgroundMode(modelString, params)) {
|
|
324
|
+
return this.generateObjectViaPolling(params, modelString);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let messages = this.buildAiSdkMessages(params.messages);
|
|
328
|
+
|
|
329
|
+
// Google requires all system messages at the beginning
|
|
330
|
+
if (provider === 'google') {
|
|
331
|
+
const system = messages.filter((m) => m.role === 'system');
|
|
332
|
+
const nonSystem = messages.filter((m) => m.role !== 'system');
|
|
333
|
+
messages = [...system, ...nonSystem];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Schema normalization
|
|
337
|
+
const isZod = this.isZodSchema(params.schema);
|
|
338
|
+
const normalizedSchema = isZod ? params.schema : jsonSchema(this.strictifyJsonSchema(params.schema));
|
|
339
|
+
|
|
340
|
+
const result = await aiGenerateObject({
|
|
341
|
+
model,
|
|
342
|
+
messages,
|
|
343
|
+
schema: normalizedSchema,
|
|
344
|
+
abortSignal: params.abortSignal,
|
|
345
|
+
maxOutputTokens: params.maxTokens,
|
|
346
|
+
temperature: params.temperature,
|
|
347
|
+
topP: params.topP,
|
|
348
|
+
providerOptions: this.buildProviderOptions(provider, params, modelString),
|
|
349
|
+
experimental_repairText: (async ({ text }) => {
|
|
350
|
+
const cleaned = String(text ?? '')
|
|
351
|
+
.trim()
|
|
352
|
+
.replace(/^```(?:json)?/i, '')
|
|
353
|
+
.replace(/```$/, '');
|
|
354
|
+
try {
|
|
355
|
+
JSON.parse(cleaned);
|
|
356
|
+
return cleaned;
|
|
357
|
+
} catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}) as RepairTextFunction,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Record in history
|
|
364
|
+
if (params.recordInHistory !== false) {
|
|
365
|
+
try {
|
|
366
|
+
const toRecord = typeof result?.object === 'object' ? JSON.stringify(result.object) : '';
|
|
367
|
+
if (toRecord) {
|
|
368
|
+
this.addAssistantMessagesToHistory([toRecord]);
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
/* ignore */
|
|
372
|
+
}
|
|
373
|
+
}
|
|
80
374
|
|
|
81
|
-
|
|
82
|
-
|
|
375
|
+
const usage = this.processAiSdkUsage(result, modelString);
|
|
376
|
+
|
|
377
|
+
if (params.onUsageData) {
|
|
378
|
+
await params.onUsageData(usage);
|
|
83
379
|
}
|
|
84
380
|
|
|
85
|
-
if
|
|
86
|
-
|
|
381
|
+
// Extract reasoning if available
|
|
382
|
+
const reasoning = this.extractReasoningFromResult(result);
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
object: (result?.object ?? {}) as T,
|
|
386
|
+
usage,
|
|
387
|
+
reasoning: reasoning || undefined,
|
|
388
|
+
toolInvocations: [],
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Non-streaming convenience: generates a text response and waits for completion.
|
|
394
|
+
*/
|
|
395
|
+
async generateResponse(params: GenerateStreamParams): Promise<GenerateResponseResult> {
|
|
396
|
+
const stream = await this.generateStream(params);
|
|
397
|
+
const [text, reasoning, sources, usage, toolInvocations] = await Promise.all([
|
|
398
|
+
stream.text,
|
|
399
|
+
stream.reasoning,
|
|
400
|
+
stream.sources,
|
|
401
|
+
stream.usage,
|
|
402
|
+
stream.toolInvocations,
|
|
403
|
+
]);
|
|
404
|
+
return { text, reasoning: reasoning || undefined, sources, usage, toolInvocations };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ────────────────────────────────────────────────────────────
|
|
408
|
+
// History management (public, for callers like ThoughtConversation)
|
|
409
|
+
// ────────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
addSystemMessagesToHistory(messages: string[], unshift = false) {
|
|
412
|
+
const formatted: ConversationMessage[] = messages.map((m) => ({ role: 'system' as const, content: m }));
|
|
413
|
+
this.addMessagesToHistory(formatted, unshift);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
addAssistantMessagesToHistory(messages: string[], unshift = false) {
|
|
417
|
+
const formatted: ConversationMessage[] = messages.map((m) => ({ role: 'assistant' as const, content: m }));
|
|
418
|
+
this.addMessagesToHistory(formatted, unshift);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
addUserMessagesToHistory(messages: string[], unshift = false) {
|
|
422
|
+
const formatted: ConversationMessage[] = messages.map((m) => ({ role: 'user' as const, content: m }));
|
|
423
|
+
this.addMessagesToHistory(formatted, unshift);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
addMessagesToHistory(messages: ConversationMessage[], unshift = false) {
|
|
427
|
+
// Convert to the format MessageHistory expects (ChatCompletionMessageParam-like)
|
|
428
|
+
const historyMessages = messages.map((m) => {
|
|
429
|
+
if (typeof m === 'string') {
|
|
430
|
+
return { role: 'user' as const, content: m };
|
|
431
|
+
}
|
|
432
|
+
return m;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const systemMsgs = historyMessages.filter((m) => m.role === 'system');
|
|
436
|
+
|
|
437
|
+
if (unshift) {
|
|
438
|
+
this.history.getMessages().unshift(...(historyMessages as any[]));
|
|
439
|
+
this.history.prune();
|
|
440
|
+
this.systemMessages.unshift(...systemMsgs);
|
|
441
|
+
} else {
|
|
442
|
+
this.history.push(historyMessages as any[]);
|
|
443
|
+
this.systemMessages.push(...systemMsgs);
|
|
87
444
|
}
|
|
88
445
|
}
|
|
89
446
|
|
|
447
|
+
// ────────────────────────────────────────────────────────────
|
|
448
|
+
// Module system
|
|
449
|
+
// ────────────────────────────────────────────────────────────
|
|
450
|
+
|
|
90
451
|
private async ensureModulesProcessed(): Promise<void> {
|
|
91
|
-
// If modules are already processed, return immediately
|
|
92
452
|
if (this.modulesProcessed) {
|
|
93
453
|
return;
|
|
94
454
|
}
|
|
95
|
-
|
|
96
|
-
// If modules are currently being processed, wait for that to complete
|
|
97
455
|
if (this.processingModulesPromise) {
|
|
98
456
|
return this.processingModulesPromise;
|
|
99
457
|
}
|
|
100
458
|
|
|
101
|
-
// Start processing modules and keep a reference to the promise
|
|
102
459
|
this.processingModulesPromise = this.processModules();
|
|
103
|
-
|
|
104
460
|
try {
|
|
105
461
|
await this.processingModulesPromise;
|
|
106
462
|
this.modulesProcessed = true;
|
|
107
463
|
} catch (error) {
|
|
108
464
|
this.logger.error({ message: 'Error processing modules', obj: { error } });
|
|
109
|
-
// Reset the promise so we can try again
|
|
110
465
|
this.processingModulesPromise = null;
|
|
111
466
|
throw error;
|
|
112
467
|
}
|
|
@@ -118,334 +473,613 @@ export class Conversation {
|
|
|
118
473
|
}
|
|
119
474
|
|
|
120
475
|
for (const module of this.params.modules) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
476
|
+
const moduleName = module.getName();
|
|
477
|
+
|
|
478
|
+
// System messages
|
|
479
|
+
const rawSystem = await Promise.resolve(module.getSystemMessages());
|
|
480
|
+
const sysArr = Array.isArray(rawSystem) ? rawSystem : rawSystem ? [rawSystem] : [];
|
|
481
|
+
const trimmed = sysArr.map((s) => String(s ?? '').trim()).filter(Boolean);
|
|
482
|
+
|
|
483
|
+
if (trimmed.length > 0) {
|
|
484
|
+
const formatted = trimmed.join('. ');
|
|
485
|
+
this.addSystemMessagesToHistory([
|
|
486
|
+
`The following are instructions from the ${moduleName} module:\n${formatted}`,
|
|
487
|
+
]);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Functions
|
|
491
|
+
const moduleFunctions = module.getFunctions();
|
|
492
|
+
this.functions.push(...moduleFunctions);
|
|
493
|
+
|
|
494
|
+
// Function instructions
|
|
495
|
+
let functionInstructions = `The following are instructions from functions in the ${moduleName} module:`;
|
|
496
|
+
let hasInstructions = false;
|
|
497
|
+
for (const f of moduleFunctions) {
|
|
498
|
+
if (f.instructions && f.instructions.length > 0) {
|
|
499
|
+
hasInstructions = true;
|
|
500
|
+
const paragraph = f.instructions.join('. ');
|
|
501
|
+
functionInstructions += ` ${f.definition.name}: ${paragraph}.`;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (hasInstructions) {
|
|
505
|
+
this.addSystemMessagesToHistory([functionInstructions]);
|
|
506
|
+
}
|
|
124
507
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
508
|
+
// Message moderators
|
|
509
|
+
this.messageModerators.push(...module.getMessageModerators());
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ────────────────────────────────────────────────────────────
|
|
514
|
+
// AI SDK message building
|
|
515
|
+
// ────────────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
private buildAiSdkMessages(
|
|
518
|
+
callMessages: ConversationMessage[]
|
|
519
|
+
): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
|
|
520
|
+
const result: Array<{ role: 'system' | 'user' | 'assistant'; content: string }> = [];
|
|
521
|
+
|
|
522
|
+
// Add history messages
|
|
523
|
+
for (const msg of this.history.getMessages()) {
|
|
524
|
+
const m = msg as any;
|
|
525
|
+
const rawRole = String(m.role ?? 'user');
|
|
526
|
+
// Map non-standard roles to the closest AI SDK role
|
|
527
|
+
const role = (rawRole === 'system' ? 'system' : rawRole === 'assistant' ? 'assistant' : 'user') as
|
|
528
|
+
| 'system'
|
|
529
|
+
| 'user'
|
|
530
|
+
| 'assistant';
|
|
531
|
+
const content = typeof m.content === 'string' ? m.content : this.extractTextFromContent(m.content);
|
|
532
|
+
if (content.trim()) {
|
|
533
|
+
result.push({ role, content });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Add call messages
|
|
538
|
+
for (const msg of callMessages) {
|
|
539
|
+
if (typeof msg === 'string') {
|
|
540
|
+
result.push({ role: 'user', content: msg });
|
|
128
541
|
} else {
|
|
129
|
-
|
|
542
|
+
const rawRole = String(msg.role ?? 'user');
|
|
543
|
+
const role = (rawRole === 'system' ? 'system' : rawRole === 'assistant' ? 'assistant' : 'user') as
|
|
544
|
+
| 'system'
|
|
545
|
+
| 'user'
|
|
546
|
+
| 'assistant';
|
|
547
|
+
result.push({ role, content: typeof msg.content === 'string' ? msg.content : '' });
|
|
130
548
|
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return result;
|
|
552
|
+
}
|
|
131
553
|
|
|
132
|
-
|
|
554
|
+
private extractTextFromContent(content: any): string {
|
|
555
|
+
if (typeof content === 'string') {
|
|
556
|
+
return content;
|
|
557
|
+
}
|
|
558
|
+
if (Array.isArray(content)) {
|
|
559
|
+
return content
|
|
560
|
+
.map((p: any) => {
|
|
561
|
+
if (typeof p === 'string') {
|
|
562
|
+
return p;
|
|
563
|
+
}
|
|
564
|
+
if (p?.type === 'text') {
|
|
565
|
+
return p.text;
|
|
566
|
+
}
|
|
567
|
+
return '';
|
|
568
|
+
})
|
|
569
|
+
.join('\n');
|
|
570
|
+
}
|
|
571
|
+
return '';
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ────────────────────────────────────────────────────────────
|
|
575
|
+
// AI SDK tool building
|
|
576
|
+
// ────────────────────────────────────────────────────────────
|
|
577
|
+
|
|
578
|
+
private buildAiSdkTools(functions: Function[]): ToolSet {
|
|
579
|
+
const tools: ToolSet = {};
|
|
580
|
+
|
|
581
|
+
for (const f of functions) {
|
|
582
|
+
const def = f.definition;
|
|
583
|
+
if (!def?.name) {
|
|
133
584
|
continue;
|
|
134
585
|
}
|
|
135
586
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
:
|
|
587
|
+
tools[def.name] = {
|
|
588
|
+
description: def.description,
|
|
589
|
+
inputSchema: jsonSchema(this.normalizeToolParameters(def.parameters)),
|
|
590
|
+
execute: async (args: any) => {
|
|
591
|
+
const result = await f.call(args);
|
|
592
|
+
if (typeof result === 'undefined') {
|
|
593
|
+
return { result: 'Function executed successfully' };
|
|
594
|
+
}
|
|
595
|
+
return result;
|
|
596
|
+
},
|
|
597
|
+
} as any;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return tools;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Normalize tool parameter schemas to ensure they are valid JSON Schema
|
|
605
|
+
* with `type: "object"`. Handles missing, null, or invalid schemas
|
|
606
|
+
* (e.g. `type: "None"` which some functions produce).
|
|
607
|
+
*/
|
|
608
|
+
private normalizeToolParameters(parameters: any): Record<string, any> {
|
|
609
|
+
const emptySchema = { type: 'object', properties: {} };
|
|
139
610
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
]);
|
|
143
|
-
this.addFunctions(module.getName(), module.getFunctions());
|
|
144
|
-
this.addMessageModerators(module.getMessageModerators());
|
|
611
|
+
if (!parameters || typeof parameters !== 'object') {
|
|
612
|
+
return emptySchema;
|
|
145
613
|
}
|
|
614
|
+
|
|
615
|
+
// If type is missing, not a string, or not a valid JSON Schema type, default to object
|
|
616
|
+
const validTypes = ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'];
|
|
617
|
+
if (
|
|
618
|
+
!parameters.type ||
|
|
619
|
+
typeof parameters.type !== 'string' ||
|
|
620
|
+
!validTypes.includes(parameters.type.toLowerCase())
|
|
621
|
+
) {
|
|
622
|
+
return { ...emptySchema, ...parameters, type: 'object' };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return parameters;
|
|
146
626
|
}
|
|
147
627
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
628
|
+
// ────────────────────────────────────────────────────────────
|
|
629
|
+
// Provider options
|
|
630
|
+
// ────────────────────────────────────────────────────────────
|
|
631
|
+
|
|
632
|
+
private buildProviderOptions(
|
|
633
|
+
provider: string,
|
|
634
|
+
params: { reasoningEffort?: ReasoningEffort; webSearch?: boolean; serviceTier?: OpenAiServiceTier },
|
|
635
|
+
modelString?: string
|
|
636
|
+
): Record<string, any> {
|
|
637
|
+
const options: Record<string, any> = {};
|
|
638
|
+
const effort = params.reasoningEffort;
|
|
639
|
+
|
|
640
|
+
if (provider === 'openai') {
|
|
641
|
+
const openaiOpts: Record<string, any> = {};
|
|
642
|
+
if (effort) {
|
|
643
|
+
// OpenAI accepts: none | low | medium | high | xhigh
|
|
644
|
+
// 'max' → 'xhigh' (OpenAI's highest)
|
|
645
|
+
openaiOpts.reasoningEffort = effort === 'max' ? 'xhigh' : effort;
|
|
646
|
+
}
|
|
647
|
+
if (params.serviceTier) {
|
|
648
|
+
openaiOpts.serviceTier = params.serviceTier;
|
|
649
|
+
}
|
|
650
|
+
options.openai = openaiOpts;
|
|
651
|
+
}
|
|
157
652
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
653
|
+
if (provider === 'anthropic') {
|
|
654
|
+
const anthropicOpts: Record<string, any> = {};
|
|
655
|
+
if (effort && effort !== 'none') {
|
|
656
|
+
// Use adaptive thinking (Sonnet 4.6+, Opus 4.6+) with effort level.
|
|
657
|
+
// Anthropic accepts effort: low | medium | high | max
|
|
658
|
+
// 'xhigh' has no Anthropic equivalent → map to 'max'
|
|
659
|
+
anthropicOpts.thinking = { type: 'adaptive' };
|
|
660
|
+
anthropicOpts.effort = effort === 'xhigh' ? 'max' : effort;
|
|
161
661
|
}
|
|
662
|
+
options.anthropic = anthropicOpts;
|
|
162
663
|
}
|
|
163
664
|
|
|
164
|
-
if (
|
|
165
|
-
|
|
665
|
+
if (provider === 'google') {
|
|
666
|
+
const googleOpts: Record<string, any> = {};
|
|
667
|
+
if (effort && effort !== 'none') {
|
|
668
|
+
// Google accepts thinkingLevel: minimal | low | medium | high
|
|
669
|
+
// Our 'max'/'xhigh' have no Google equivalent → map to 'high'
|
|
670
|
+
const levelMap: Record<string, string> = {
|
|
671
|
+
low: 'low',
|
|
672
|
+
medium: 'medium',
|
|
673
|
+
high: 'high',
|
|
674
|
+
xhigh: 'high',
|
|
675
|
+
max: 'high',
|
|
676
|
+
};
|
|
677
|
+
googleOpts.thinkingConfig = {
|
|
678
|
+
thinkingLevel: levelMap[effort] ?? 'medium',
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
options.google = googleOpts;
|
|
166
682
|
}
|
|
167
683
|
|
|
168
|
-
|
|
169
|
-
|
|
684
|
+
if (provider === 'xai') {
|
|
685
|
+
const xaiOpts: Record<string, any> = {};
|
|
686
|
+
// Only models with reasoning support accept the reasoningEffort parameter.
|
|
687
|
+
// Models like grok-4 (no "-fast" suffix) reject it with a 400 error.
|
|
688
|
+
const xaiSupportsReasoning = modelString ? /fast/i.test(modelString) : false;
|
|
689
|
+
if (effort && effort !== 'none' && xaiSupportsReasoning) {
|
|
690
|
+
// xAI accepts: low | high
|
|
691
|
+
// Map everything to the closest valid value
|
|
692
|
+
const xaiEffort = effort === 'low' ? 'low' : 'high';
|
|
693
|
+
xaiOpts.reasoningEffort = xaiEffort;
|
|
694
|
+
}
|
|
695
|
+
options.xai = xaiOpts;
|
|
696
|
+
}
|
|
170
697
|
|
|
171
|
-
|
|
172
|
-
this.messageModerators.push(...messageModerators);
|
|
698
|
+
return options;
|
|
173
699
|
}
|
|
174
700
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
701
|
+
// ────────────────────────────────────────────────────────────
|
|
702
|
+
// Background/polling escape hatch (OpenAI-specific)
|
|
703
|
+
// ────────────────────────────────────────────────────────────
|
|
179
704
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
.map((part) => {
|
|
193
|
-
if (typeof part === 'string') {
|
|
194
|
-
return part;
|
|
195
|
-
} else if (part?.type === 'text') {
|
|
196
|
-
return part.text;
|
|
197
|
-
} else {
|
|
198
|
-
return ''; // Handle non-text content types as empty string
|
|
199
|
-
}
|
|
200
|
-
})
|
|
201
|
-
.join(' ');
|
|
202
|
-
}
|
|
203
|
-
})
|
|
204
|
-
.join('. ');
|
|
205
|
-
const encoded = encoder.encode(conversation);
|
|
206
|
-
console.log(`current tokens: ${encoded.length}`);
|
|
207
|
-
if (encoded.length < this.tokenLimit) {
|
|
208
|
-
return;
|
|
705
|
+
private shouldUseBackgroundMode(
|
|
706
|
+
modelString: string,
|
|
707
|
+
params: { backgroundMode?: boolean; reasoningEffort?: ReasoningEffort }
|
|
708
|
+
): boolean {
|
|
709
|
+
if (typeof params.backgroundMode === 'boolean') {
|
|
710
|
+
return params.backgroundMode;
|
|
711
|
+
}
|
|
712
|
+
if (this.isProModel(modelString)) {
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
if (this.isHighReasoningEffort(params.reasoningEffort)) {
|
|
716
|
+
return true;
|
|
209
717
|
}
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
210
720
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
history: this.history,
|
|
214
|
-
functions: this.functions,
|
|
215
|
-
messageModerators: this.messageModerators,
|
|
216
|
-
logLevel: this.params.logLevel,
|
|
217
|
-
}).generateResponse({ messages: [summarizeConversationRequest], model });
|
|
218
|
-
const referenceSummaryRequest = `If there's a file mentioned in the conversation summary, find and read the file to better respond to my next request. If that doesn't find anything, call the ${searchLibrariesFunctionName} function on other keywords in the conversation summary to find a file to read`;
|
|
219
|
-
await new OpenAi({
|
|
220
|
-
history: this.history,
|
|
221
|
-
functions: this.functions,
|
|
222
|
-
messageModerators: this.messageModerators,
|
|
223
|
-
logLevel: this.params.logLevel,
|
|
224
|
-
}).generateResponse({ messages: [referenceSummaryRequest], model });
|
|
721
|
+
private isProModel(model: string): boolean {
|
|
722
|
+
return /(^|[-_.])pro($|[-_.])/.test(String(model ?? '').toLowerCase());
|
|
225
723
|
}
|
|
226
724
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
this.history.push([{ role: 'assistant', content: `Previous conversation summary: ${summary}` }]);
|
|
725
|
+
private isHighReasoningEffort(effort?: ReasoningEffort): boolean {
|
|
726
|
+
return effort === 'high' || effort === 'xhigh' || effort === 'max';
|
|
230
727
|
}
|
|
231
728
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
729
|
+
/**
|
|
730
|
+
* Map our ReasoningEffort to OpenAI's accepted values.
|
|
731
|
+
* OpenAI accepts: none | low | medium | high | xhigh
|
|
732
|
+
* 'max' → 'xhigh' (OpenAI's highest).
|
|
733
|
+
*/
|
|
734
|
+
private mapReasoningEffortForOpenAi(
|
|
735
|
+
effort?: ReasoningEffort
|
|
736
|
+
): 'none' | 'low' | 'medium' | 'high' | 'xhigh' | undefined {
|
|
737
|
+
if (!effort) {
|
|
738
|
+
return undefined;
|
|
739
|
+
}
|
|
740
|
+
if (effort === 'max') {
|
|
741
|
+
return 'xhigh';
|
|
742
|
+
}
|
|
743
|
+
return effort as 'none' | 'low' | 'medium' | 'high' | 'xhigh';
|
|
235
744
|
}
|
|
236
745
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
746
|
+
/**
|
|
747
|
+
* Fall back to OpenAiResponses for background/polling mode.
|
|
748
|
+
* Returns a StreamResult where the text arrives as a single chunk after polling completes.
|
|
749
|
+
*/
|
|
750
|
+
private async generateStreamViaPolling(params: GenerateStreamParams, modelString: string): Promise<StreamResult> {
|
|
751
|
+
const responses = this.createOpenAiResponses(params);
|
|
752
|
+
|
|
753
|
+
// Convert messages to the format OpenAiResponses expects
|
|
754
|
+
const messages = this.convertToOpenAiMessages(params.messages);
|
|
755
|
+
|
|
756
|
+
const result = await responses.generateText({
|
|
757
|
+
messages,
|
|
758
|
+
model: modelString as TiktokenModel,
|
|
759
|
+
abortSignal: params.abortSignal,
|
|
760
|
+
onToolInvocation: params.onToolInvocation,
|
|
761
|
+
onUsageData: params.onUsageData,
|
|
762
|
+
reasoningEffort: this.mapReasoningEffortForOpenAi(params.reasoningEffort),
|
|
763
|
+
maxToolCalls: params.maxToolCalls,
|
|
764
|
+
backgroundMode: params.backgroundMode,
|
|
765
|
+
maxBackgroundWaitMs: params.maxBackgroundWaitMs,
|
|
766
|
+
serviceTier: params.serviceTier,
|
|
240
767
|
});
|
|
241
|
-
|
|
768
|
+
|
|
769
|
+
// Wrap the polling result as a StreamResult
|
|
770
|
+
const text = result.message;
|
|
771
|
+
const usage = result.usagedata;
|
|
772
|
+
const toolInvocations = result.toolInvocations;
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
textStream: (async function* () {
|
|
776
|
+
yield text;
|
|
777
|
+
})(),
|
|
778
|
+
reasoningStream: (async function* () {
|
|
779
|
+
// Reasoning not available via polling mode
|
|
780
|
+
})(),
|
|
781
|
+
fullStream: (async function* () {
|
|
782
|
+
yield { type: 'text-delta' as const, textDelta: text };
|
|
783
|
+
})(),
|
|
784
|
+
text: Promise.resolve(text),
|
|
785
|
+
reasoning: Promise.resolve(''),
|
|
786
|
+
sources: Promise.resolve([]),
|
|
787
|
+
usage: Promise.resolve(usage),
|
|
788
|
+
toolInvocations: Promise.resolve(toolInvocations),
|
|
789
|
+
};
|
|
242
790
|
}
|
|
243
791
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
792
|
+
/**
|
|
793
|
+
* Fall back to OpenAiResponses for generateObject with background/polling.
|
|
794
|
+
*/
|
|
795
|
+
private async generateObjectViaPolling<T>(
|
|
796
|
+
params: GenerateObjectParams<T>,
|
|
797
|
+
modelString: string
|
|
798
|
+
): Promise<GenerateObjectResult<T>> {
|
|
799
|
+
const responses = this.createOpenAiResponses(params);
|
|
800
|
+
|
|
801
|
+
const messages = this.convertToOpenAiMessages(params.messages);
|
|
802
|
+
|
|
803
|
+
const result = await responses.generateObject<T>({
|
|
804
|
+
messages,
|
|
805
|
+
model: modelString as TiktokenModel,
|
|
806
|
+
schema: params.schema,
|
|
807
|
+
abortSignal: params.abortSignal,
|
|
808
|
+
onUsageData: params.onUsageData,
|
|
809
|
+
reasoningEffort: this.mapReasoningEffortForOpenAi(params.reasoningEffort),
|
|
810
|
+
temperature: params.temperature,
|
|
811
|
+
topP: params.topP,
|
|
812
|
+
maxTokens: params.maxTokens,
|
|
813
|
+
backgroundMode: params.backgroundMode,
|
|
814
|
+
maxBackgroundWaitMs: params.maxBackgroundWaitMs,
|
|
815
|
+
serviceTier: params.serviceTier,
|
|
247
816
|
});
|
|
248
|
-
|
|
817
|
+
|
|
818
|
+
return {
|
|
819
|
+
object: result.object,
|
|
820
|
+
usage: result.usageData,
|
|
821
|
+
reasoning: undefined,
|
|
822
|
+
toolInvocations: [],
|
|
823
|
+
};
|
|
249
824
|
}
|
|
250
825
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
826
|
+
private createOpenAiResponses(params: {
|
|
827
|
+
serviceTier?: OpenAiServiceTier;
|
|
828
|
+
maxBackgroundWaitMs?: number;
|
|
829
|
+
}): OpenAiResponses {
|
|
830
|
+
// Lazy require to avoid circular dependency and keep OpenAiResponses optional
|
|
831
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
832
|
+
const { OpenAiResponses: OAIResponses } = require('./OpenAiResponses');
|
|
833
|
+
return new OAIResponses({
|
|
834
|
+
modules: this.params.modules,
|
|
835
|
+
logLevel: this.params.logLevel,
|
|
836
|
+
defaultModel: this.getModelString(this.params.defaultModel) as TiktokenModel,
|
|
254
837
|
});
|
|
255
|
-
this.addMessagesToHistory(chatCompletions, unshift);
|
|
256
838
|
}
|
|
257
839
|
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
if (unshift) {
|
|
261
|
-
this.history.getMessages().unshift(...messages);
|
|
262
|
-
this.history.prune();
|
|
263
|
-
this.systemMessages.unshift(...systemMessages);
|
|
264
|
-
} else {
|
|
265
|
-
this.history.push(messages);
|
|
266
|
-
this.systemMessages.push(...systemMessages);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async generateResponse({
|
|
271
|
-
messages,
|
|
272
|
-
model,
|
|
273
|
-
maxToolCalls,
|
|
274
|
-
...rest
|
|
275
|
-
}: {
|
|
276
|
-
messages: (string | ChatCompletionMessageParam)[];
|
|
277
|
-
model?: TiktokenModel;
|
|
278
|
-
abortSignal?: AbortSignal;
|
|
279
|
-
onUsageData?: (usageData: UsageData) => Promise<void>;
|
|
280
|
-
onToolInvocation?: (evt: ToolInvocationProgressEvent) => void;
|
|
281
|
-
reasoningEffort?: OpenAIApi.Chat.Completions.ChatCompletionReasoningEffort;
|
|
282
|
-
maxToolCalls?: number;
|
|
283
|
-
}) {
|
|
284
|
-
await this.ensureModulesProcessed();
|
|
285
|
-
await this.enforceTokenLimit(messages, model);
|
|
840
|
+
private convertToOpenAiMessages(messages: ConversationMessage[]): Array<string | ChatCompletionMessageParam> {
|
|
841
|
+
const result: Array<string | ChatCompletionMessageParam> = [];
|
|
286
842
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
843
|
+
// Include history
|
|
844
|
+
for (const msg of this.history.getMessages()) {
|
|
845
|
+
const m = msg as any;
|
|
846
|
+
result.push({
|
|
847
|
+
role: m.role as 'system' | 'user' | 'assistant',
|
|
848
|
+
content: typeof m.content === 'string' ? m.content : this.extractTextFromContent(m.content),
|
|
849
|
+
});
|
|
850
|
+
}
|
|
290
851
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
...rest
|
|
305
|
-
}: {
|
|
306
|
-
messages: (string | ChatCompletionMessageParam)[];
|
|
307
|
-
model?: TiktokenModel;
|
|
308
|
-
abortSignal?: AbortSignal;
|
|
309
|
-
onUsageData?: (usageData: UsageData) => Promise<void>;
|
|
310
|
-
onToolInvocation?: (evt: ToolInvocationProgressEvent) => void;
|
|
311
|
-
reasoningEffort?: OpenAIApi.Chat.Completions.ChatCompletionReasoningEffort;
|
|
312
|
-
maxToolCalls?: number;
|
|
313
|
-
}) {
|
|
314
|
-
await this.ensureModulesProcessed();
|
|
315
|
-
await this.enforceTokenLimit(messages, model);
|
|
316
|
-
return await new OpenAi({
|
|
317
|
-
history: this.history,
|
|
318
|
-
functions: this.functions,
|
|
319
|
-
messageModerators: this.messageModerators,
|
|
320
|
-
logLevel: this.params.logLevel,
|
|
321
|
-
...(typeof maxToolCalls !== 'undefined' ? { maxFunctionCalls: maxToolCalls } : {}),
|
|
322
|
-
}).generateStreamingResponse({ messages, model, ...rest });
|
|
852
|
+
// Include call messages
|
|
853
|
+
for (const msg of messages) {
|
|
854
|
+
if (typeof msg === 'string') {
|
|
855
|
+
result.push(msg);
|
|
856
|
+
} else {
|
|
857
|
+
result.push({
|
|
858
|
+
role: msg.role as 'system' | 'user' | 'assistant',
|
|
859
|
+
content: msg.content as string,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return result;
|
|
323
865
|
}
|
|
324
866
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
*/
|
|
329
|
-
async generateObject<T>({
|
|
330
|
-
messages,
|
|
331
|
-
model,
|
|
332
|
-
abortSignal,
|
|
333
|
-
schema,
|
|
334
|
-
temperature,
|
|
335
|
-
topP,
|
|
336
|
-
maxTokens,
|
|
337
|
-
onUsageData,
|
|
338
|
-
recordInHistory = true,
|
|
339
|
-
reasoningEffort,
|
|
340
|
-
}: GenerateObjectParams<unknown>): Promise<GenerateObjectOutcome<T>> {
|
|
341
|
-
await this.ensureModulesProcessed();
|
|
867
|
+
// ────────────────────────────────────────────────────────────
|
|
868
|
+
// Model resolution
|
|
869
|
+
// ────────────────────────────────────────────────────────────
|
|
342
870
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
871
|
+
private resolveModelInstance(model?: LanguageModel | string): LanguageModel {
|
|
872
|
+
const m = model ?? this.params.defaultModel ?? DEFAULT_MODEL;
|
|
873
|
+
return resolveModel(m);
|
|
874
|
+
}
|
|
347
875
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
876
|
+
private getModelString(model?: LanguageModel | string): string {
|
|
877
|
+
if (!model) {
|
|
878
|
+
const def = this.params.defaultModel;
|
|
879
|
+
if (!def) {
|
|
880
|
+
return DEFAULT_MODEL;
|
|
881
|
+
}
|
|
882
|
+
if (typeof def === 'string') {
|
|
883
|
+
return def;
|
|
884
|
+
}
|
|
885
|
+
return (def as any).modelId ?? DEFAULT_MODEL;
|
|
886
|
+
}
|
|
887
|
+
if (typeof model === 'string') {
|
|
888
|
+
return model;
|
|
889
|
+
}
|
|
890
|
+
return (model as any).modelId ?? 'unknown';
|
|
891
|
+
}
|
|
354
892
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
893
|
+
// ────────────────────────────────────────────────────────────
|
|
894
|
+
// Usage processing
|
|
895
|
+
// ────────────────────────────────────────────────────────────
|
|
358
896
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
try {
|
|
379
|
-
JSON.parse(cleaned);
|
|
380
|
-
return cleaned;
|
|
381
|
-
} catch {
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
},
|
|
385
|
-
} as any);
|
|
897
|
+
/**
|
|
898
|
+
* Build a usage promise from a streaming result.
|
|
899
|
+
* Uses `totalUsage` (accumulated across all steps in a tool-call loop)
|
|
900
|
+
* and populates tool call stats from the steps.
|
|
901
|
+
*/
|
|
902
|
+
private async buildUsagePromise(
|
|
903
|
+
result: {
|
|
904
|
+
totalUsage: PromiseLike<LanguageModelUsage>;
|
|
905
|
+
steps: PromiseLike<Array<{ toolCalls?: Array<{ toolName?: string }> }>>;
|
|
906
|
+
},
|
|
907
|
+
modelString: string,
|
|
908
|
+
params: GenerateStreamParams
|
|
909
|
+
): Promise<UsageData> {
|
|
910
|
+
const [sdkUsage, steps] = await Promise.all([result.totalUsage, result.steps]);
|
|
911
|
+
const usage = this.mapSdkUsage(sdkUsage, modelString, steps);
|
|
912
|
+
|
|
913
|
+
if (params.onUsageData) {
|
|
914
|
+
await params.onUsageData(usage);
|
|
915
|
+
}
|
|
386
916
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
typeof m === 'string' ? ({ role: 'user', content: m } as ChatCompletionMessageParam) : m
|
|
390
|
-
);
|
|
391
|
-
this.addMessagesToHistory(chatCompletions);
|
|
917
|
+
return usage;
|
|
918
|
+
}
|
|
392
919
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
920
|
+
private async buildToolInvocationsPromise(result: {
|
|
921
|
+
steps: PromiseLike<
|
|
922
|
+
Array<{
|
|
923
|
+
toolCalls?: Array<{ toolCallId?: string; toolName?: string; args?: unknown }>;
|
|
924
|
+
toolResults?: Array<{ toolCallId?: string; result?: unknown }>;
|
|
925
|
+
}>
|
|
926
|
+
>;
|
|
927
|
+
}): Promise<ToolInvocationResult[]> {
|
|
928
|
+
const steps = await result.steps;
|
|
929
|
+
const invocations: ToolInvocationResult[] = [];
|
|
930
|
+
|
|
931
|
+
for (const step of steps ?? []) {
|
|
932
|
+
for (const toolCall of step.toolCalls ?? []) {
|
|
933
|
+
invocations.push({
|
|
934
|
+
id: toolCall.toolCallId ?? '',
|
|
935
|
+
name: toolCall.toolName ?? '',
|
|
936
|
+
startedAt: new Date(),
|
|
937
|
+
finishedAt: new Date(),
|
|
938
|
+
input: toolCall.args,
|
|
939
|
+
ok: true,
|
|
940
|
+
data: (step.toolResults ?? []).find((r) => r.toolCallId === toolCall.toolCallId)?.result,
|
|
941
|
+
});
|
|
402
942
|
}
|
|
403
943
|
}
|
|
404
944
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
945
|
+
return invocations;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Map AI SDK's `LanguageModelUsage` to our `UsageData`.
|
|
950
|
+
*
|
|
951
|
+
* The AI SDK v6 provides cached/reasoning token breakdowns directly in
|
|
952
|
+
* `LanguageModelUsage.inputTokenDetails` and `outputTokenDetails`, so we
|
|
953
|
+
* use those first and only fall back to provider metadata for older providers.
|
|
954
|
+
*/
|
|
955
|
+
private mapSdkUsage(
|
|
956
|
+
sdkUsage: LanguageModelUsage,
|
|
957
|
+
modelString: string,
|
|
958
|
+
steps?: Array<{ toolCalls?: Array<{ toolName?: string }> }>
|
|
959
|
+
): UsageData {
|
|
960
|
+
const inputTokens = sdkUsage?.inputTokens ?? 0;
|
|
961
|
+
const outputTokens = sdkUsage?.outputTokens ?? 0;
|
|
962
|
+
const totalTokens = sdkUsage?.totalTokens ?? inputTokens + outputTokens;
|
|
963
|
+
|
|
964
|
+
// AI SDK v6 provides structured token details
|
|
965
|
+
const cachedInputTokens = sdkUsage?.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
966
|
+
const reasoningTokens = sdkUsage?.outputTokenDetails?.reasoningTokens ?? 0;
|
|
967
|
+
|
|
968
|
+
const tokenUsage: TokenUsage = {
|
|
969
|
+
inputTokens,
|
|
970
|
+
cachedInputTokens,
|
|
971
|
+
reasoningTokens,
|
|
972
|
+
outputTokens,
|
|
973
|
+
totalTokens,
|
|
974
|
+
};
|
|
409
975
|
|
|
410
|
-
|
|
411
|
-
|
|
976
|
+
// Count steps as individual requests to the assistant
|
|
977
|
+
const stepCount = steps?.length ?? 1;
|
|
978
|
+
const acc = new UsageDataAccumulator({ model: modelString as TiktokenModel });
|
|
979
|
+
acc.addTokenUsage(tokenUsage);
|
|
980
|
+
|
|
981
|
+
// Populate tool call stats from steps
|
|
982
|
+
const callsPerTool: Record<string, number> = {};
|
|
983
|
+
let totalToolCalls = 0;
|
|
984
|
+
for (const step of steps ?? []) {
|
|
985
|
+
for (const toolCall of step.toolCalls ?? []) {
|
|
986
|
+
const name = toolCall.toolName ?? 'unknown';
|
|
987
|
+
callsPerTool[name] = (callsPerTool[name] ?? 0) + 1;
|
|
988
|
+
totalToolCalls++;
|
|
989
|
+
}
|
|
412
990
|
}
|
|
413
991
|
|
|
414
992
|
return {
|
|
415
|
-
|
|
416
|
-
|
|
993
|
+
...acc.usageData,
|
|
994
|
+
totalRequestsToAssistant: stepCount,
|
|
995
|
+
callsPerTool,
|
|
996
|
+
totalToolCalls,
|
|
417
997
|
};
|
|
418
998
|
}
|
|
419
999
|
|
|
420
|
-
/**
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
1000
|
+
/**
|
|
1001
|
+
* Process usage from a generateObject result (single-step, no tool calls).
|
|
1002
|
+
*/
|
|
1003
|
+
private processAiSdkUsage(result: { usage: LanguageModelUsage }, modelString: string): UsageData {
|
|
1004
|
+
return this.mapSdkUsage(result.usage, modelString);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// ────────────────────────────────────────────────────────────
|
|
1008
|
+
// Full stream mapping
|
|
1009
|
+
// ────────────────────────────────────────────────────────────
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Maps the AI SDK's `fullStream` (which emits all event types) into our
|
|
1013
|
+
* `StreamPart` union. This is the primary way to consume streaming output
|
|
1014
|
+
* in real-time, since it yields text, reasoning, and source events in the
|
|
1015
|
+
* order the model produces them.
|
|
1016
|
+
*/
|
|
1017
|
+
private mapFullStream(aiSdkFullStream: AsyncIterable<any>): AsyncIterable<StreamPart> {
|
|
1018
|
+
return {
|
|
1019
|
+
async *[Symbol.asyncIterator]() {
|
|
1020
|
+
for await (const part of aiSdkFullStream) {
|
|
1021
|
+
if (part.type === 'text-delta' && part.textDelta) {
|
|
1022
|
+
yield { type: 'text-delta' as const, textDelta: part.textDelta };
|
|
1023
|
+
} else if (part.type === 'reasoning' && part.textDelta) {
|
|
1024
|
+
yield { type: 'reasoning-delta' as const, textDelta: part.textDelta };
|
|
1025
|
+
} else if (part.type === 'source') {
|
|
1026
|
+
yield {
|
|
1027
|
+
type: 'source' as const,
|
|
1028
|
+
source: {
|
|
1029
|
+
url: part.sourceType === 'url' ? part.url : undefined,
|
|
1030
|
+
title: part.sourceType === 'url' ? part.title : undefined,
|
|
1031
|
+
},
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
private extractReasoningFromResult(result: any): string {
|
|
1040
|
+
try {
|
|
1041
|
+
// Try to get reasoning from provider metadata or response
|
|
1042
|
+
const reasoning = result?.reasoning;
|
|
1043
|
+
if (typeof reasoning === 'string') {
|
|
1044
|
+
return reasoning;
|
|
425
1045
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
1046
|
+
if (Array.isArray(reasoning)) {
|
|
1047
|
+
return reasoning
|
|
1048
|
+
.filter((r: any) => r.type === 'reasoning')
|
|
1049
|
+
.map((r: any) => r.text)
|
|
1050
|
+
.join('');
|
|
1051
|
+
}
|
|
1052
|
+
} catch {
|
|
1053
|
+
// ignore
|
|
1054
|
+
}
|
|
1055
|
+
return '';
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// ────────────────────────────────────────────────────────────
|
|
1059
|
+
// Schema utilities
|
|
1060
|
+
// ────────────────────────────────────────────────────────────
|
|
1061
|
+
|
|
1062
|
+
private isZodSchema(schema: unknown): boolean {
|
|
1063
|
+
if (!schema || (typeof schema !== 'object' && typeof schema !== 'function')) {
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
return (
|
|
1067
|
+
typeof (schema as any).safeParse === 'function' ||
|
|
1068
|
+
(!!(schema as any)._def && typeof (schema as any)._def.typeName === 'string')
|
|
1069
|
+
);
|
|
432
1070
|
}
|
|
433
1071
|
|
|
434
1072
|
/**
|
|
435
|
-
* Strictifies a
|
|
436
|
-
* - Ensures every object has `additionalProperties: false`
|
|
437
|
-
* - Ensures every object has a `required` array that includes **all** keys in `properties`
|
|
438
|
-
* - Adds missing `type: "object"` / `type: "array"` where implied by keywords
|
|
1073
|
+
* Strictifies a JSON Schema for OpenAI Structured Outputs (strict mode).
|
|
439
1074
|
*/
|
|
440
1075
|
private strictifyJsonSchema(schema: any): any {
|
|
441
|
-
const root = JSON.parse(JSON.stringify(schema));
|
|
1076
|
+
const root = JSON.parse(JSON.stringify(schema ?? {}));
|
|
442
1077
|
|
|
443
1078
|
const visit = (node: any) => {
|
|
444
1079
|
if (!node || typeof node !== 'object') {
|
|
445
1080
|
return;
|
|
446
1081
|
}
|
|
447
1082
|
|
|
448
|
-
// If keywords imply a type but it's missing, add it (helps downstream validators)
|
|
449
1083
|
if (!node.type) {
|
|
450
1084
|
if (node.properties || node.additionalProperties || node.patternProperties) {
|
|
451
1085
|
node.type = 'object';
|
|
@@ -456,34 +1090,23 @@ export class Conversation {
|
|
|
456
1090
|
|
|
457
1091
|
const types = Array.isArray(node.type) ? node.type : node.type ? [node.type] : [];
|
|
458
1092
|
|
|
459
|
-
// Objects: enforce strict requirements
|
|
460
1093
|
if (types.includes('object')) {
|
|
461
|
-
// 1) additionalProperties: false
|
|
462
1094
|
if (node.additionalProperties !== false) {
|
|
463
1095
|
node.additionalProperties = false;
|
|
464
1096
|
}
|
|
465
|
-
|
|
466
|
-
// 2) required must exist and include every key in properties
|
|
467
1097
|
if (node.properties && typeof node.properties === 'object') {
|
|
468
1098
|
const propKeys = Object.keys(node.properties);
|
|
469
1099
|
const currentReq: string[] = Array.isArray(node.required) ? node.required.slice() : [];
|
|
470
|
-
|
|
471
|
-
node.required = union;
|
|
472
|
-
|
|
473
|
-
// Recurse into each property schema
|
|
1100
|
+
node.required = Array.from(new Set([...currentReq, ...propKeys]));
|
|
474
1101
|
for (const k of propKeys) {
|
|
475
1102
|
visit(node.properties[k]);
|
|
476
1103
|
}
|
|
477
1104
|
}
|
|
478
|
-
|
|
479
|
-
// Recurse into patternProperties
|
|
480
1105
|
if (node.patternProperties && typeof node.patternProperties === 'object') {
|
|
481
1106
|
for (const k of Object.keys(node.patternProperties)) {
|
|
482
1107
|
visit(node.patternProperties[k]);
|
|
483
1108
|
}
|
|
484
1109
|
}
|
|
485
|
-
|
|
486
|
-
// Recurse into $defs / definitions
|
|
487
1110
|
for (const defsKey of ['$defs', 'definitions']) {
|
|
488
1111
|
if (node[defsKey] && typeof node[defsKey] === 'object') {
|
|
489
1112
|
for (const key of Object.keys(node[defsKey])) {
|
|
@@ -493,7 +1116,6 @@ export class Conversation {
|
|
|
493
1116
|
}
|
|
494
1117
|
}
|
|
495
1118
|
|
|
496
|
-
// Arrays: recurse into items/prefixItems
|
|
497
1119
|
if (types.includes('array')) {
|
|
498
1120
|
if (node.items) {
|
|
499
1121
|
if (Array.isArray(node.items)) {
|
|
@@ -507,14 +1129,11 @@ export class Conversation {
|
|
|
507
1129
|
}
|
|
508
1130
|
}
|
|
509
1131
|
|
|
510
|
-
// Combinators
|
|
511
1132
|
for (const k of ['oneOf', 'anyOf', 'allOf']) {
|
|
512
1133
|
if (Array.isArray(node[k])) {
|
|
513
1134
|
node[k].forEach(visit);
|
|
514
1135
|
}
|
|
515
1136
|
}
|
|
516
|
-
|
|
517
|
-
// Negation
|
|
518
1137
|
if (node.not) {
|
|
519
1138
|
visit(node.not);
|
|
520
1139
|
}
|
|
@@ -523,181 +1142,4 @@ export class Conversation {
|
|
|
523
1142
|
visit(root);
|
|
524
1143
|
return root;
|
|
525
1144
|
}
|
|
526
|
-
|
|
527
|
-
// ---- Usage + provider metadata normalization ----
|
|
528
|
-
|
|
529
|
-
private processUsageData(args: {
|
|
530
|
-
result: GenerateObjectResult<JSONValue>;
|
|
531
|
-
model?: LanguageModel;
|
|
532
|
-
toolCounts?: Map<string, number>;
|
|
533
|
-
toolLedgerLen?: number;
|
|
534
|
-
}): UsageData {
|
|
535
|
-
const { result, model, toolCounts, toolLedgerLen } = args;
|
|
536
|
-
|
|
537
|
-
const u = result?.usage;
|
|
538
|
-
|
|
539
|
-
// Provider-specific extras (OpenAI Responses variants)
|
|
540
|
-
const { cachedInputTokens, reasoningTokens } = this.extractOpenAiUsageDetails?.(result) ?? {};
|
|
541
|
-
|
|
542
|
-
const input = Number.isFinite(u?.inputTokens) ? Number(u.inputTokens) : 0;
|
|
543
|
-
const reasoning = Number.isFinite(reasoningTokens) ? Number(reasoningTokens) : 0;
|
|
544
|
-
const output = Number.isFinite(u?.outputTokens) ? Number(u.outputTokens) : 0;
|
|
545
|
-
const total = Number.isFinite(u?.totalTokens) ? Number(u.totalTokens) : input + output;
|
|
546
|
-
const cached = Number.isFinite(cachedInputTokens) ? Number(cachedInputTokens) : 0;
|
|
547
|
-
|
|
548
|
-
// Resolve model id for pricing/telemetry
|
|
549
|
-
const modelId: any = model?.toString();
|
|
550
|
-
|
|
551
|
-
const resolvedModel = typeof modelId === 'string' && modelId.trim().length > 0 ? modelId : 'unknown';
|
|
552
|
-
|
|
553
|
-
const tokenUsage = {
|
|
554
|
-
inputTokens: input,
|
|
555
|
-
reasoningTokens: reasoning,
|
|
556
|
-
cachedInputTokens: cached,
|
|
557
|
-
outputTokens: output,
|
|
558
|
-
totalTokens: total,
|
|
559
|
-
};
|
|
560
|
-
|
|
561
|
-
const uda = new UsageDataAccumulator({ model: modelId });
|
|
562
|
-
uda.addTokenUsage(tokenUsage);
|
|
563
|
-
|
|
564
|
-
const callsPerTool = toolCounts ? Object.fromEntries(toolCounts) : {};
|
|
565
|
-
const totalToolCalls =
|
|
566
|
-
typeof toolLedgerLen === 'number' ? toolLedgerLen : Object.values(callsPerTool).reduce((a, b) => a + (b || 0), 0);
|
|
567
|
-
|
|
568
|
-
return {
|
|
569
|
-
...uda.usageData,
|
|
570
|
-
totalRequestsToAssistant: 1,
|
|
571
|
-
totalToolCalls,
|
|
572
|
-
callsPerTool,
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// Pull OpenAI-specific cached/extra usage from provider metadata or raw usage.
|
|
577
|
-
// Safe across providers; returns undefined if not available.
|
|
578
|
-
private extractOpenAiUsageDetails(result: any): {
|
|
579
|
-
cachedInputTokens?: number;
|
|
580
|
-
reasoningTokens?: number;
|
|
581
|
-
} {
|
|
582
|
-
try {
|
|
583
|
-
const md = result?.providerMetadata?.openai ?? result?.response?.providerMetadata?.openai;
|
|
584
|
-
const usage = md?.usage ?? result?.response?.usage ?? result?.usage;
|
|
585
|
-
|
|
586
|
-
// OpenAI Responses API has used different shapes over time; try both:
|
|
587
|
-
const cachedInputTokens =
|
|
588
|
-
usage?.input_tokens_details?.cached_tokens ??
|
|
589
|
-
usage?.prompt_tokens_details?.cached_tokens ??
|
|
590
|
-
usage?.cached_input_tokens;
|
|
591
|
-
|
|
592
|
-
// Reasoning tokens (when available on reasoning models)
|
|
593
|
-
const reasoningTokens =
|
|
594
|
-
usage?.output_tokens_details?.reasoning_tokens ??
|
|
595
|
-
usage?.completion_tokens_details?.reasoning_tokens ??
|
|
596
|
-
usage?.reasoning_tokens;
|
|
597
|
-
|
|
598
|
-
return {
|
|
599
|
-
cachedInputTokens: typeof cachedInputTokens === 'number' ? cachedInputTokens : undefined,
|
|
600
|
-
reasoningTokens: typeof reasoningTokens === 'number' ? reasoningTokens : undefined,
|
|
601
|
-
};
|
|
602
|
-
} catch {
|
|
603
|
-
return {};
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
async generateCode({ description, model }: { description: string[]; model?: TiktokenModel }) {
|
|
608
|
-
this.logger.debug({ message: `Generating code`, obj: { description } });
|
|
609
|
-
await this.ensureModulesProcessed();
|
|
610
|
-
const code = await new OpenAi({
|
|
611
|
-
history: this.history,
|
|
612
|
-
functions: this.functions,
|
|
613
|
-
messageModerators: this.messageModerators,
|
|
614
|
-
logLevel: this.params.logLevel,
|
|
615
|
-
}).generateCode({
|
|
616
|
-
messages: description,
|
|
617
|
-
model,
|
|
618
|
-
includeSystemMessages: !this.generatedCode,
|
|
619
|
-
});
|
|
620
|
-
this.logger.debug({ message: `Generated code`, obj: { code } });
|
|
621
|
-
this.generatedCode = true;
|
|
622
|
-
return code;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
async updateCodeFromFile({
|
|
626
|
-
codeToUpdateFilePath,
|
|
627
|
-
dependencyCodeFilePaths,
|
|
628
|
-
description,
|
|
629
|
-
model,
|
|
630
|
-
}: {
|
|
631
|
-
codeToUpdateFilePath: string;
|
|
632
|
-
dependencyCodeFilePaths: string[];
|
|
633
|
-
description: string;
|
|
634
|
-
model?: TiktokenModel;
|
|
635
|
-
}) {
|
|
636
|
-
await this.ensureModulesProcessed();
|
|
637
|
-
const codeToUpdate = await Fs.readFile(codeToUpdateFilePath);
|
|
638
|
-
let dependencyDescription = `Assume the following exists:\n`;
|
|
639
|
-
for (const dependencyCodeFilePath of dependencyCodeFilePaths) {
|
|
640
|
-
const dependencCode = await Fs.readFile(dependencyCodeFilePath);
|
|
641
|
-
dependencyDescription += dependencCode + '\n\n';
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
this.logger.debug({ message: `Updating code from file`, obj: { codeToUpdateFilePath } });
|
|
645
|
-
return await this.updateCode({ code: codeToUpdate, description: dependencyDescription + description, model });
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
async updateCode({ code, description, model }: { code: string; description: string; model?: TiktokenModel }) {
|
|
649
|
-
this.logger.debug({ message: `Updating code`, obj: { description, code } });
|
|
650
|
-
await this.ensureModulesProcessed();
|
|
651
|
-
const updatedCode = await new OpenAi({
|
|
652
|
-
history: this.history,
|
|
653
|
-
functions: this.functions,
|
|
654
|
-
messageModerators: this.messageModerators,
|
|
655
|
-
logLevel: this.params.logLevel,
|
|
656
|
-
}).updateCode({
|
|
657
|
-
code,
|
|
658
|
-
description,
|
|
659
|
-
model,
|
|
660
|
-
includeSystemMessages: !this.generatedCode,
|
|
661
|
-
});
|
|
662
|
-
this.logger.debug({ message: `Updated code`, obj: { updatedCode } });
|
|
663
|
-
this.generatedCode = true;
|
|
664
|
-
return updatedCode;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
async generateList({ description, model }: { description: string[]; model?: TiktokenModel }) {
|
|
668
|
-
await this.ensureModulesProcessed();
|
|
669
|
-
const list = await new OpenAi({
|
|
670
|
-
history: this.history,
|
|
671
|
-
functions: this.functions,
|
|
672
|
-
messageModerators: this.messageModerators,
|
|
673
|
-
logLevel: this.params.logLevel,
|
|
674
|
-
}).generateList({
|
|
675
|
-
messages: description,
|
|
676
|
-
model,
|
|
677
|
-
includeSystemMessages: !this.generatedList,
|
|
678
|
-
});
|
|
679
|
-
this.generatedList = true;
|
|
680
|
-
return list;
|
|
681
|
-
}
|
|
682
1145
|
}
|
|
683
|
-
|
|
684
|
-
export const summarizeConversationHistoryFunctionName = 'summarizeConversationHistory';
|
|
685
|
-
export const summarizeConversationHistoryFunction = (conversation: Conversation) => {
|
|
686
|
-
return {
|
|
687
|
-
definition: {
|
|
688
|
-
name: summarizeConversationHistoryFunctionName,
|
|
689
|
-
description: 'Clear the conversation history and summarize what was in it',
|
|
690
|
-
parameters: {
|
|
691
|
-
type: 'object',
|
|
692
|
-
properties: {
|
|
693
|
-
summary: {
|
|
694
|
-
type: 'string',
|
|
695
|
-
description: 'A 1-3 sentence summary of the current chat history',
|
|
696
|
-
},
|
|
697
|
-
},
|
|
698
|
-
required: ['summary'],
|
|
699
|
-
},
|
|
700
|
-
},
|
|
701
|
-
call: async (params: { summary: string }) => conversation.summarizeConversationHistory(params.summary),
|
|
702
|
-
};
|
|
703
|
-
};
|