@llumiverse/drivers 0.23.0 → 0.24.0-dev.202601221707
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/README.md +141 -218
- package/lib/cjs/azure/azure_foundry.js +46 -2
- package/lib/cjs/azure/azure_foundry.js.map +1 -1
- package/lib/cjs/bedrock/index.js +236 -16
- package/lib/cjs/bedrock/index.js.map +1 -1
- package/lib/cjs/groq/index.js +115 -85
- package/lib/cjs/groq/index.js.map +1 -1
- package/lib/cjs/index.js +1 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/openai/index.js +310 -114
- package/lib/cjs/openai/index.js.map +1 -1
- package/lib/cjs/openai/openai_compatible.js +62 -0
- package/lib/cjs/openai/openai_compatible.js.map +1 -0
- package/lib/cjs/openai/openai_format.js +32 -39
- package/lib/cjs/openai/openai_format.js.map +1 -1
- package/lib/cjs/vertexai/index.js +165 -0
- package/lib/cjs/vertexai/index.js.map +1 -1
- package/lib/cjs/vertexai/models/claude.js +201 -3
- package/lib/cjs/vertexai/models/claude.js.map +1 -1
- package/lib/cjs/vertexai/models/gemini.js +59 -20
- package/lib/cjs/vertexai/models/gemini.js.map +1 -1
- package/lib/cjs/xai/index.js +10 -16
- package/lib/cjs/xai/index.js.map +1 -1
- package/lib/esm/azure/azure_foundry.js +46 -2
- package/lib/esm/azure/azure_foundry.js.map +1 -1
- package/lib/esm/bedrock/index.js +236 -17
- package/lib/esm/bedrock/index.js.map +1 -1
- package/lib/esm/groq/index.js +115 -85
- package/lib/esm/groq/index.js.map +1 -1
- package/lib/esm/index.js +1 -0
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/openai/index.js +311 -115
- package/lib/esm/openai/index.js.map +1 -1
- package/lib/esm/openai/openai_compatible.js +55 -0
- package/lib/esm/openai/openai_compatible.js.map +1 -0
- package/lib/esm/openai/openai_format.js +32 -39
- package/lib/esm/openai/openai_format.js.map +1 -1
- package/lib/esm/vertexai/index.js +166 -1
- package/lib/esm/vertexai/index.js.map +1 -1
- package/lib/esm/vertexai/models/claude.js +199 -3
- package/lib/esm/vertexai/models/claude.js.map +1 -1
- package/lib/esm/vertexai/models/gemini.js +60 -21
- package/lib/esm/vertexai/models/gemini.js.map +1 -1
- package/lib/esm/xai/index.js +10 -16
- package/lib/esm/xai/index.js.map +1 -1
- package/lib/types/azure/azure_foundry.d.ts +7 -5
- package/lib/types/azure/azure_foundry.d.ts.map +1 -1
- package/lib/types/bedrock/index.d.ts +21 -1
- package/lib/types/bedrock/index.d.ts.map +1 -1
- package/lib/types/groq/index.d.ts.map +1 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/openai/index.d.ts +13 -7
- package/lib/types/openai/index.d.ts.map +1 -1
- package/lib/types/openai/openai_compatible.d.ts +26 -0
- package/lib/types/openai/openai_compatible.d.ts.map +1 -0
- package/lib/types/openai/openai_format.d.ts +4 -2
- package/lib/types/openai/openai_format.d.ts.map +1 -1
- package/lib/types/vertexai/index.d.ts +15 -0
- package/lib/types/vertexai/index.d.ts.map +1 -1
- package/lib/types/vertexai/models/claude.d.ts +20 -0
- package/lib/types/vertexai/models/claude.d.ts.map +1 -1
- package/lib/types/vertexai/models/gemini.d.ts +1 -1
- package/lib/types/vertexai/models/gemini.d.ts.map +1 -1
- package/lib/types/xai/index.d.ts +2 -3
- package/lib/types/xai/index.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/azure/azure_foundry.ts +56 -7
- package/src/bedrock/index.ts +297 -26
- package/src/groq/index.ts +120 -94
- package/src/index.ts +1 -0
- package/src/openai/index.ts +363 -136
- package/src/openai/openai_compatible.ts +74 -0
- package/src/openai/openai_format.ts +44 -54
- package/src/vertexai/index.ts +205 -0
- package/src/vertexai/models/claude.ts +233 -3
- package/src/vertexai/models/gemini.ts +78 -27
- package/src/xai/index.ts +10 -17
package/src/openai/index.ts
CHANGED
|
@@ -19,15 +19,21 @@ import {
|
|
|
19
19
|
TrainingJobStatus,
|
|
20
20
|
TrainingOptions,
|
|
21
21
|
TrainingPromptOptions,
|
|
22
|
+
getConversationMeta,
|
|
22
23
|
getModelCapabilities,
|
|
24
|
+
incrementConversationTurn,
|
|
23
25
|
modelModalitiesToArray,
|
|
26
|
+
stripBase64ImagesFromConversation,
|
|
24
27
|
supportsToolUse,
|
|
28
|
+
truncateLargeTextInConversation,
|
|
29
|
+
unwrapConversationArray,
|
|
25
30
|
} from "@llumiverse/core";
|
|
26
|
-
import { asyncMap } from "@llumiverse/core/async";
|
|
27
|
-
import { formatOpenAILikeMultimodalPrompt } from "./openai_format.js";
|
|
28
31
|
import OpenAI, { AzureOpenAI } from "openai";
|
|
29
|
-
import {
|
|
30
|
-
|
|
32
|
+
import { formatOpenAILikeMultimodalPrompt } from "./openai_format.js";
|
|
33
|
+
|
|
34
|
+
// Response API types
|
|
35
|
+
type ResponseInputItem = OpenAI.Responses.ResponseInputItem;
|
|
36
|
+
type EasyInputMessage = OpenAI.Responses.EasyInputMessage;
|
|
31
37
|
|
|
32
38
|
// Helper function to convert string to CompletionResult[]
|
|
33
39
|
function textToCompletionResult(text: string): CompletionResult[] {
|
|
@@ -48,32 +54,24 @@ export interface BaseOpenAIDriverOptions extends DriverOptions {
|
|
|
48
54
|
|
|
49
55
|
export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
50
56
|
BaseOpenAIDriverOptions,
|
|
51
|
-
|
|
57
|
+
ResponseInputItem[]
|
|
52
58
|
> {
|
|
53
|
-
|
|
54
|
-
abstract provider: Providers.openai | Providers.azure_openai | "xai" | Providers.azure_foundry;
|
|
59
|
+
abstract provider: Providers.openai | Providers.azure_openai | Providers.xai | Providers.azure_foundry | Providers.openai_compatible;
|
|
55
60
|
abstract service: OpenAI | AzureOpenAI;
|
|
56
61
|
|
|
57
62
|
constructor(opts: BaseOpenAIDriverOptions) {
|
|
58
63
|
super(opts);
|
|
59
|
-
this.formatPrompt = formatOpenAILikeMultimodalPrompt
|
|
60
|
-
//TODO: better type, we send back OpenAI.Chat.Completions.ChatCompletionMessageParam[] but just not compatible with Function call that we don't use here
|
|
64
|
+
this.formatPrompt = formatOpenAILikeMultimodalPrompt;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
extractDataFromResponse(
|
|
64
68
|
_options: ExecutionOptions,
|
|
65
|
-
result: OpenAI.
|
|
69
|
+
result: OpenAI.Responses.Response
|
|
66
70
|
): Completion {
|
|
67
|
-
const tokenInfo
|
|
68
|
-
prompt: result.usage?.prompt_tokens,
|
|
69
|
-
result: result.usage?.completion_tokens,
|
|
70
|
-
total: result.usage?.total_tokens,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const choice = result.choices[0];
|
|
71
|
+
const tokenInfo = mapUsage(result.usage);
|
|
74
72
|
|
|
75
|
-
const tools = collectTools(
|
|
76
|
-
const data =
|
|
73
|
+
const tools = collectTools(result.output);
|
|
74
|
+
const data = extractTextFromResponse(result);
|
|
77
75
|
|
|
78
76
|
if (!data && !tools) {
|
|
79
77
|
this.logger.error({ result }, "[OpenAI] Response is not valid");
|
|
@@ -83,37 +81,21 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
|
83
81
|
return {
|
|
84
82
|
result: textToCompletionResult(data || ''),
|
|
85
83
|
token_usage: tokenInfo,
|
|
86
|
-
finish_reason:
|
|
84
|
+
finish_reason: responseFinishReason(result, tools),
|
|
87
85
|
tool_use: tools,
|
|
88
86
|
};
|
|
89
87
|
}
|
|
90
88
|
|
|
91
|
-
async requestTextCompletionStream(prompt:
|
|
89
|
+
async requestTextCompletionStream(prompt: ResponseInputItem[], options: ExecutionOptions): Promise<AsyncIterable<CompletionChunkObject>> {
|
|
92
90
|
if (options.model_options?._option_id !== "openai-text" && options.model_options?._option_id !== "openai-thinking") {
|
|
93
91
|
this.logger.warn({ options: options.model_options }, "Invalid model options");
|
|
94
92
|
}
|
|
95
93
|
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
const mapFn = (chunk: OpenAI.Chat.Completions.ChatCompletionChunk) => {
|
|
100
|
-
let result = undefined
|
|
101
|
-
if (useTools && this.provider !== "xai" && options.result_schema) {
|
|
102
|
-
result = chunk.choices[0]?.delta?.tool_calls?.[0].function?.arguments ?? "";
|
|
103
|
-
} else {
|
|
104
|
-
result = chunk.choices[0]?.delta.content ?? "";
|
|
105
|
-
}
|
|
94
|
+
// Include conversation history (same as non-streaming)
|
|
95
|
+
const conversation = updateConversation(options.conversation, prompt);
|
|
106
96
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
finish_reason: openAiFinishReason(chunk.choices[0]?.finish_reason ?? undefined), //Uses expected "stop" , "length" format
|
|
110
|
-
token_usage: {
|
|
111
|
-
prompt: chunk.usage?.prompt_tokens,
|
|
112
|
-
result: chunk.usage?.completion_tokens,
|
|
113
|
-
total: (chunk.usage?.prompt_tokens ?? 0) + (chunk.usage?.completion_tokens ?? 0),
|
|
114
|
-
}
|
|
115
|
-
} satisfies CompletionChunkObject;
|
|
116
|
-
};
|
|
97
|
+
const toolDefs = getToolDefinitions(options.tools);
|
|
98
|
+
const useTools: boolean = toolDefs ? supportsToolUse(options.model, this.provider, true) : false;
|
|
117
99
|
|
|
118
100
|
convertRoles(prompt, options.model);
|
|
119
101
|
|
|
@@ -133,35 +115,31 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
|
133
115
|
}
|
|
134
116
|
}
|
|
135
117
|
|
|
136
|
-
const
|
|
118
|
+
const reasoning = model_options?.reasoning_effort ? { effort: model_options.reasoning_effort } : undefined;
|
|
119
|
+
|
|
120
|
+
const stream = await this.service.responses.create({
|
|
137
121
|
stream: true,
|
|
138
|
-
stream_options: { include_usage: true },
|
|
139
122
|
model: options.model,
|
|
140
|
-
|
|
141
|
-
|
|
123
|
+
input: conversation,
|
|
124
|
+
reasoning,
|
|
142
125
|
temperature: model_options?.temperature,
|
|
143
126
|
top_p: model_options?.top_p,
|
|
144
|
-
|
|
145
|
-
frequency_penalty: model_options?.frequency_penalty,
|
|
146
|
-
n: 1,
|
|
147
|
-
max_completion_tokens: model_options?.max_tokens, //TODO: use max_tokens for older models, currently relying on OpenAI to handle it
|
|
127
|
+
max_output_tokens: model_options?.max_tokens,
|
|
148
128
|
tools: useTools ? toolDefs : undefined,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
json_schema: {
|
|
129
|
+
text: parsedSchema ? {
|
|
130
|
+
format: {
|
|
131
|
+
type: "json_schema",
|
|
153
132
|
name: "format_output",
|
|
154
133
|
schema: parsedSchema,
|
|
155
134
|
strict: strictMode,
|
|
156
135
|
}
|
|
157
136
|
} : undefined,
|
|
158
|
-
}
|
|
159
|
-
) satisfies Stream<OpenAI.Chat.Completions.ChatCompletionChunk>;
|
|
137
|
+
});
|
|
160
138
|
|
|
161
|
-
return
|
|
139
|
+
return mapResponseStream(stream);
|
|
162
140
|
}
|
|
163
141
|
|
|
164
|
-
async requestTextCompletion(prompt:
|
|
142
|
+
async requestTextCompletion(prompt: ResponseInputItem[], options: ExecutionOptions): Promise<Completion> {
|
|
165
143
|
if (options.model_options?._option_id !== "openai-text" && options.model_options?._option_id !== "openai-thinking") {
|
|
166
144
|
this.logger.warn({ options: options.model_options }, "Invalid model options");
|
|
167
145
|
}
|
|
@@ -172,9 +150,9 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
|
172
150
|
insert_image_detail(prompt, model_options?.image_detail ?? "auto");
|
|
173
151
|
|
|
174
152
|
const toolDefs = getToolDefinitions(options.tools);
|
|
175
|
-
const useTools: boolean = toolDefs ? supportsToolUse(options.model,
|
|
153
|
+
const useTools: boolean = toolDefs ? supportsToolUse(options.model, this.provider) : false;
|
|
176
154
|
|
|
177
|
-
let conversation = updateConversation(options.conversation
|
|
155
|
+
let conversation = updateConversation(options.conversation, prompt);
|
|
178
156
|
|
|
179
157
|
let parsedSchema: JSONSchema | undefined = undefined;
|
|
180
158
|
let strictMode = false;
|
|
@@ -189,22 +167,20 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
|
189
167
|
}
|
|
190
168
|
}
|
|
191
169
|
|
|
192
|
-
const
|
|
170
|
+
const reasoning = model_options?.reasoning_effort ? { effort: model_options.reasoning_effort } : undefined;
|
|
171
|
+
|
|
172
|
+
const res = await this.service.responses.create({
|
|
193
173
|
stream: false,
|
|
194
174
|
model: options.model,
|
|
195
|
-
|
|
196
|
-
|
|
175
|
+
input: conversation,
|
|
176
|
+
reasoning,
|
|
197
177
|
temperature: model_options?.temperature,
|
|
198
178
|
top_p: model_options?.top_p,
|
|
199
|
-
|
|
200
|
-
frequency_penalty: model_options?.frequency_penalty,
|
|
201
|
-
n: 1,
|
|
202
|
-
max_completion_tokens: model_options?.max_tokens, //TODO: use max_tokens for older models, currently relying on OpenAI to handle it
|
|
179
|
+
max_output_tokens: model_options?.max_tokens, //TODO: use max_tokens for older models, currently relying on OpenAI to handle it
|
|
203
180
|
tools: useTools ? toolDefs : undefined,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
json_schema: {
|
|
181
|
+
text: parsedSchema ? {
|
|
182
|
+
format: {
|
|
183
|
+
type: "json_schema",
|
|
208
184
|
name: "format_output",
|
|
209
185
|
schema: parsedSchema,
|
|
210
186
|
strict: strictMode,
|
|
@@ -217,8 +193,24 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
|
217
193
|
completion.original_response = res;
|
|
218
194
|
}
|
|
219
195
|
|
|
220
|
-
conversation = updateConversation(conversation,
|
|
221
|
-
|
|
196
|
+
conversation = updateConversation(conversation, createAssistantMessageFromCompletion(completion));
|
|
197
|
+
|
|
198
|
+
// Increment turn counter for deferred stripping
|
|
199
|
+
conversation = incrementConversationTurn(conversation) as ResponseInputItem[];
|
|
200
|
+
|
|
201
|
+
// Strip large base64 image data based on options.stripImagesAfterTurns
|
|
202
|
+
const currentTurn = getConversationMeta(conversation).turnNumber;
|
|
203
|
+
const stripOptions = {
|
|
204
|
+
keepForTurns: options.stripImagesAfterTurns ?? Infinity,
|
|
205
|
+
currentTurn,
|
|
206
|
+
textMaxTokens: options.stripTextMaxTokens
|
|
207
|
+
};
|
|
208
|
+
let processedConversation = stripBase64ImagesFromConversation(conversation, stripOptions);
|
|
209
|
+
|
|
210
|
+
// Truncate large text content if configured
|
|
211
|
+
processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
|
|
212
|
+
|
|
213
|
+
completion.conversation = processedConversation;
|
|
222
214
|
|
|
223
215
|
return completion;
|
|
224
216
|
}
|
|
@@ -233,6 +225,60 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
|
233
225
|
return Promise.resolve(true);
|
|
234
226
|
}
|
|
235
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Build conversation context after streaming completion.
|
|
230
|
+
* Reconstructs the assistant message from accumulated results and applies stripping.
|
|
231
|
+
*/
|
|
232
|
+
buildStreamingConversation(
|
|
233
|
+
prompt: ResponseInputItem[],
|
|
234
|
+
result: unknown[],
|
|
235
|
+
toolUse: unknown[] | undefined,
|
|
236
|
+
options: ExecutionOptions
|
|
237
|
+
): ResponseInputItem[] | undefined {
|
|
238
|
+
// Build assistant message from accumulated CompletionResult[]
|
|
239
|
+
const completionResults = result as CompletionResult[];
|
|
240
|
+
|
|
241
|
+
const textContent = completionResultsToText(completionResults);
|
|
242
|
+
|
|
243
|
+
// Start with the conversation from options or the prompt
|
|
244
|
+
let conversation = updateConversation(options.conversation, prompt);
|
|
245
|
+
|
|
246
|
+
// Add assistant message as EasyInputMessage
|
|
247
|
+
if (textContent) {
|
|
248
|
+
const assistantMessage: EasyInputMessage = {
|
|
249
|
+
role: 'assistant',
|
|
250
|
+
content: textContent,
|
|
251
|
+
};
|
|
252
|
+
conversation = updateConversation(conversation, [assistantMessage]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Add function calls as separate items (Response API format)
|
|
256
|
+
if (toolUse && toolUse.length > 0) {
|
|
257
|
+
const functionCalls: OpenAI.Responses.ResponseFunctionToolCall[] = (toolUse as ToolUse[]).map(t => ({
|
|
258
|
+
type: 'function_call' as const,
|
|
259
|
+
call_id: t.id,
|
|
260
|
+
name: t.tool_name,
|
|
261
|
+
arguments: typeof t.tool_input === 'string' ? t.tool_input : JSON.stringify(t.tool_input ?? {}),
|
|
262
|
+
}));
|
|
263
|
+
conversation = updateConversation(conversation, functionCalls);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Increment turn counter
|
|
267
|
+
conversation = incrementConversationTurn(conversation) as ResponseInputItem[];
|
|
268
|
+
|
|
269
|
+
// Apply stripping based on options
|
|
270
|
+
const currentTurn = getConversationMeta(conversation).turnNumber;
|
|
271
|
+
const stripOptions = {
|
|
272
|
+
keepForTurns: options.stripImagesAfterTurns ?? Infinity,
|
|
273
|
+
currentTurn,
|
|
274
|
+
textMaxTokens: options.stripTextMaxTokens
|
|
275
|
+
};
|
|
276
|
+
let processedConversation = stripBase64ImagesFromConversation(conversation, stripOptions);
|
|
277
|
+
processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
|
|
278
|
+
|
|
279
|
+
return processedConversation as ResponseInputItem[];
|
|
280
|
+
}
|
|
281
|
+
|
|
236
282
|
createTrainingPrompt(options: TrainingPromptOptions): Promise<string> {
|
|
237
283
|
if (options.model.includes("gpt")) {
|
|
238
284
|
return super.createTrainingPrompt(options);
|
|
@@ -377,44 +423,175 @@ function jobInfo(job: OpenAI.FineTuning.Jobs.FineTuningJob): TrainingJob {
|
|
|
377
423
|
}
|
|
378
424
|
}
|
|
379
425
|
|
|
380
|
-
function
|
|
381
|
-
if (
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
426
|
+
function mapUsage(usage?: OpenAI.Responses.ResponseUsage | null): ExecutionTokenUsage | undefined {
|
|
427
|
+
if (!usage) {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
prompt: usage.input_tokens,
|
|
432
|
+
result: usage.output_tokens,
|
|
433
|
+
total: usage.total_tokens,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function completionResultsToText(completionResults: CompletionResult[] | undefined): string {
|
|
438
|
+
if (!completionResults) {
|
|
439
|
+
return '';
|
|
440
|
+
}
|
|
441
|
+
return completionResults
|
|
442
|
+
.map(r => {
|
|
443
|
+
switch (r.type) {
|
|
444
|
+
case 'text':
|
|
445
|
+
return r.value;
|
|
446
|
+
case 'json':
|
|
447
|
+
return typeof r.value === 'string' ? r.value : JSON.stringify(r.value);
|
|
448
|
+
case 'image':
|
|
449
|
+
// Skip images in conversation - they're in the result
|
|
450
|
+
return '';
|
|
451
|
+
default:
|
|
452
|
+
return String((r as any).value || '');
|
|
453
|
+
}
|
|
454
|
+
})
|
|
455
|
+
.join('');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function createAssistantMessageFromCompletion(completion: Completion): ResponseInputItem[] {
|
|
459
|
+
const textContent = completionResultsToText(completion.result);
|
|
460
|
+
const result: ResponseInputItem[] = [];
|
|
461
|
+
|
|
462
|
+
// Add assistant text message if present
|
|
463
|
+
if (textContent) {
|
|
464
|
+
const assistantMessage: EasyInputMessage = {
|
|
465
|
+
role: 'assistant',
|
|
466
|
+
content: textContent,
|
|
467
|
+
};
|
|
468
|
+
result.push(assistantMessage);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Add function calls as separate items (Response API format)
|
|
472
|
+
if (completion.tool_use && completion.tool_use.length > 0) {
|
|
473
|
+
for (const t of completion.tool_use) {
|
|
474
|
+
const functionCall: OpenAI.Responses.ResponseFunctionToolCall = {
|
|
475
|
+
type: 'function_call',
|
|
476
|
+
call_id: t.id,
|
|
477
|
+
name: t.tool_name,
|
|
478
|
+
arguments: typeof t.tool_input === 'string'
|
|
479
|
+
? t.tool_input
|
|
480
|
+
: JSON.stringify(t.tool_input ?? {}),
|
|
481
|
+
};
|
|
482
|
+
result.push(functionCall);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function mapResponseStream(stream: AsyncIterable<OpenAI.Responses.ResponseStreamEvent>): AsyncIterable<CompletionChunkObject> {
|
|
490
|
+
const toolCallMetadata = new Map<string, { syntheticId: string, name?: string }>();
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
async *[Symbol.asyncIterator]() {
|
|
494
|
+
for await (const event of stream) {
|
|
495
|
+
if (event.type === 'response.output_item.added' && event.item.type === 'function_call') {
|
|
496
|
+
const syntheticId = `tool_${event.output_index}`;
|
|
497
|
+
const actualId = event.item.id ?? event.item.call_id;
|
|
498
|
+
if (actualId) {
|
|
499
|
+
toolCallMetadata.set(actualId, { syntheticId, name: event.item.name });
|
|
500
|
+
}
|
|
501
|
+
const toolUse: ToolUse & { _actual_id?: string } = {
|
|
502
|
+
id: syntheticId,
|
|
503
|
+
_actual_id: actualId,
|
|
504
|
+
tool_name: event.item.name,
|
|
505
|
+
tool_input: '' as any,
|
|
506
|
+
};
|
|
507
|
+
yield {
|
|
508
|
+
result: [],
|
|
509
|
+
tool_use: [toolUse],
|
|
510
|
+
} satisfies CompletionChunkObject;
|
|
511
|
+
} else if (event.type === 'response.function_call_arguments.delta') {
|
|
512
|
+
const metadata = toolCallMetadata.get(event.item_id);
|
|
513
|
+
const syntheticId = metadata?.syntheticId ?? `tool_${event.output_index}`;
|
|
514
|
+
const toolUse: ToolUse & { _actual_id?: string } = {
|
|
515
|
+
id: syntheticId,
|
|
516
|
+
_actual_id: event.item_id,
|
|
517
|
+
tool_name: metadata?.name ?? '',
|
|
518
|
+
tool_input: event.delta as any,
|
|
519
|
+
};
|
|
520
|
+
yield {
|
|
521
|
+
result: [],
|
|
522
|
+
tool_use: [toolUse],
|
|
523
|
+
} satisfies CompletionChunkObject;
|
|
524
|
+
}
|
|
525
|
+
// Note: We don't emit response.function_call_arguments.done because the arguments were already
|
|
526
|
+
// streamed via delta events. Emitting it again would duplicate the tool_input content.
|
|
527
|
+
// We only update the metadata to ensure the tool name is captured.
|
|
528
|
+
else if (event.type === 'response.function_call_arguments.done') {
|
|
529
|
+
// Just update metadata, don't yield (arguments already accumulated from delta events)
|
|
530
|
+
const metadata = toolCallMetadata.get(event.item_id);
|
|
531
|
+
const syntheticId = metadata?.syntheticId ?? `tool_${event.output_index}`;
|
|
532
|
+
const tool_name = metadata?.name ?? event.name ?? '';
|
|
533
|
+
if (event.item_id) {
|
|
534
|
+
toolCallMetadata.set(event.item_id, { syntheticId, name: tool_name });
|
|
387
535
|
}
|
|
388
|
-
|
|
389
|
-
|
|
536
|
+
} else if (event.type === 'response.output_text.delta') {
|
|
537
|
+
yield {
|
|
538
|
+
result: textToCompletionResult(event.delta),
|
|
539
|
+
} satisfies CompletionChunkObject;
|
|
540
|
+
}
|
|
541
|
+
// Note: We don't emit response.output_text.done because the text was already
|
|
542
|
+
// streamed via delta events. Emitting it again would duplicate the content.
|
|
543
|
+
else if (event.type === 'response.completed' || event.type === 'response.incomplete' || event.type === 'response.failed') {
|
|
544
|
+
const finalTools = collectTools(event.response.output);
|
|
545
|
+
yield {
|
|
546
|
+
result: [],
|
|
547
|
+
finish_reason: responseFinishReason(event.response, finalTools),
|
|
548
|
+
token_usage: mapUsage(event.response.usage),
|
|
549
|
+
} satisfies CompletionChunkObject;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function insert_image_detail(items: ResponseInputItem[], detail_level: string): ResponseInputItem[] {
|
|
557
|
+
if (detail_level === "auto" || detail_level === "low" || detail_level === "high") {
|
|
558
|
+
for (const item of items) {
|
|
559
|
+
// Check if it's an EasyInputMessage or Message with content array
|
|
560
|
+
if ('role' in item && 'content' in item && item.role !== 'assistant') {
|
|
561
|
+
const content = (item as EasyInputMessage).content;
|
|
562
|
+
if (Array.isArray(content)) {
|
|
563
|
+
for (const part of content) {
|
|
564
|
+
if (typeof part === 'object' && part.type === 'input_image') {
|
|
565
|
+
(part as any).detail = detail_level;
|
|
566
|
+
}
|
|
390
567
|
}
|
|
391
568
|
}
|
|
392
569
|
}
|
|
393
570
|
}
|
|
394
571
|
}
|
|
395
|
-
return
|
|
572
|
+
return items;
|
|
396
573
|
}
|
|
397
574
|
|
|
398
|
-
function convertRoles(
|
|
575
|
+
function convertRoles(items: ResponseInputItem[], model: string): ResponseInputItem[] {
|
|
399
576
|
//New openai models use developer role instead of system
|
|
400
577
|
if (model.includes("o1") || model.includes("o3")) {
|
|
401
578
|
if (model.includes("o1-mini") || model.includes("o1-preview")) {
|
|
402
579
|
//o1-mini and o1-preview support neither system nor developer
|
|
403
|
-
for (const
|
|
404
|
-
if (
|
|
405
|
-
(
|
|
580
|
+
for (const item of items) {
|
|
581
|
+
if ('role' in item && (item as EasyInputMessage).role === 'system') {
|
|
582
|
+
(item as any).role = 'user';
|
|
406
583
|
}
|
|
407
584
|
}
|
|
408
585
|
} else {
|
|
409
586
|
//Models newer than o1 use developer role
|
|
410
|
-
for (const
|
|
411
|
-
if (
|
|
412
|
-
(
|
|
587
|
+
for (const item of items) {
|
|
588
|
+
if ('role' in item && (item as EasyInputMessage).role === 'system') {
|
|
589
|
+
(item as any).role = 'developer';
|
|
413
590
|
}
|
|
414
591
|
}
|
|
415
592
|
}
|
|
416
593
|
}
|
|
417
|
-
return
|
|
594
|
+
return items;
|
|
418
595
|
}
|
|
419
596
|
|
|
420
597
|
//Structured output support is typically aligned with tool use support
|
|
@@ -427,10 +604,10 @@ function supportsSchema(model: string): boolean {
|
|
|
427
604
|
return supportsToolUse(model, "openai");
|
|
428
605
|
}
|
|
429
606
|
|
|
430
|
-
function getToolDefinitions(tools: ToolDefinition[] | undefined | null): OpenAI.
|
|
607
|
+
function getToolDefinitions(tools: ToolDefinition[] | undefined | null): OpenAI.Responses.Tool[] | undefined {
|
|
431
608
|
return tools ? tools.map(getToolDefinition) : undefined;
|
|
432
609
|
}
|
|
433
|
-
function getToolDefinition(toolDef: ToolDefinition): OpenAI.
|
|
610
|
+
function getToolDefinition(toolDef: ToolDefinition): OpenAI.Responses.FunctionTool {
|
|
434
611
|
let parsedSchema: JSONSchema | undefined = undefined;
|
|
435
612
|
let strictMode = false;
|
|
436
613
|
if (toolDef.input_schema) {
|
|
@@ -446,64 +623,50 @@ function getToolDefinition(toolDef: ToolDefinition): OpenAI.ChatCompletionTool {
|
|
|
446
623
|
|
|
447
624
|
return {
|
|
448
625
|
type: "function",
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
},
|
|
455
|
-
} satisfies OpenAI.ChatCompletionTool;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function openAiFinishReason(finish_reason?: string): string | undefined {
|
|
459
|
-
if (finish_reason === "tool_calls") {
|
|
460
|
-
return "tool_use";
|
|
461
|
-
}
|
|
462
|
-
return finish_reason;
|
|
626
|
+
name: toolDef.name,
|
|
627
|
+
description: toolDef.description,
|
|
628
|
+
parameters: parsedSchema ?? null,
|
|
629
|
+
strict: strictMode,
|
|
630
|
+
};
|
|
463
631
|
}
|
|
464
632
|
|
|
465
|
-
function updateConversation(conversation:
|
|
466
|
-
if (!
|
|
467
|
-
|
|
633
|
+
function updateConversation(conversation: unknown, items: ResponseInputItem[]): ResponseInputItem[] {
|
|
634
|
+
if (!items) {
|
|
635
|
+
// Unwrap array if wrapped, otherwise treat as array
|
|
636
|
+
const unwrapped = unwrapConversationArray<ResponseInputItem>(conversation);
|
|
637
|
+
return unwrapped ?? (conversation as ResponseInputItem[] || []);
|
|
468
638
|
}
|
|
469
639
|
if (!conversation) {
|
|
470
|
-
return
|
|
640
|
+
return items;
|
|
471
641
|
}
|
|
472
|
-
|
|
642
|
+
// Unwrap array if wrapped, otherwise treat as array
|
|
643
|
+
const unwrapped = unwrapConversationArray<ResponseInputItem>(conversation);
|
|
644
|
+
const convArray = unwrapped ?? (conversation as ResponseInputItem[]);
|
|
645
|
+
return [...convArray, ...items];
|
|
473
646
|
}
|
|
474
647
|
|
|
475
|
-
export function collectTools(
|
|
476
|
-
if (!
|
|
648
|
+
export function collectTools(output?: OpenAI.Responses.ResponseOutputItem[]): ToolUse[] | undefined {
|
|
649
|
+
if (!output) {
|
|
477
650
|
return undefined;
|
|
478
651
|
}
|
|
479
652
|
|
|
480
653
|
const tools: ToolUse[] = [];
|
|
481
|
-
for (const
|
|
482
|
-
|
|
483
|
-
id
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
654
|
+
for (const item of output) {
|
|
655
|
+
if (item.type === 'function_call') {
|
|
656
|
+
const id = item.call_id || item.id;
|
|
657
|
+
if (!id) {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
tools.push({
|
|
661
|
+
id,
|
|
662
|
+
tool_name: item.name ?? '',
|
|
663
|
+
tool_input: safeJsonParse(item.arguments),
|
|
664
|
+
});
|
|
665
|
+
}
|
|
488
666
|
}
|
|
489
667
|
return tools.length > 0 ? tools : undefined;
|
|
490
668
|
}
|
|
491
669
|
|
|
492
|
-
function createPromptFromResponse(response: OpenAI.Chat.Completions.ChatCompletionMessage): ChatCompletionMessageParam[] {
|
|
493
|
-
const messages: ChatCompletionMessageParam[] = [];
|
|
494
|
-
if (response) {
|
|
495
|
-
messages.push({
|
|
496
|
-
role: response.role,
|
|
497
|
-
content: [{
|
|
498
|
-
type: "text",
|
|
499
|
-
text: response.content ?? ""
|
|
500
|
-
}],
|
|
501
|
-
tool_calls: response.tool_calls,
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
return messages;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
670
|
//For strict mode false
|
|
508
671
|
function limitedSchemaFormat(schema: JSONSchema): JSONSchema {
|
|
509
672
|
const formattedSchema = { ...schema };
|
|
@@ -511,6 +674,18 @@ function limitedSchemaFormat(schema: JSONSchema): JSONSchema {
|
|
|
511
674
|
// Defaults not supported
|
|
512
675
|
delete formattedSchema.default;
|
|
513
676
|
|
|
677
|
+
// OpenAI requires type field even in non-strict mode
|
|
678
|
+
// If no type is specified, default to 'object' for properties with format/editor hints,
|
|
679
|
+
// otherwise 'string' as a safe fallback
|
|
680
|
+
if (!formattedSchema.type && formattedSchema.description) {
|
|
681
|
+
// Properties with format: "document" or editor hints are typically objects
|
|
682
|
+
if (formattedSchema.format === 'document' || formattedSchema.editor) {
|
|
683
|
+
formattedSchema.type = 'object';
|
|
684
|
+
} else {
|
|
685
|
+
formattedSchema.type = 'string';
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
514
689
|
if (formattedSchema?.properties) {
|
|
515
690
|
// Process each property recursively
|
|
516
691
|
for (const propName of Object.keys(formattedSchema.properties)) {
|
|
@@ -556,6 +731,11 @@ function openAISchemaFormat(schema: JSONSchema, nesting: number = 0): JSONSchema
|
|
|
556
731
|
for (const propName of Object.keys(formattedSchema.properties)) {
|
|
557
732
|
const property = formattedSchema.properties[propName];
|
|
558
733
|
|
|
734
|
+
// OpenAI strict mode requires all properties to have a type
|
|
735
|
+
if (!property?.type) {
|
|
736
|
+
throw new Error(`Property '${propName}' is missing required 'type' field for OpenAI strict mode`);
|
|
737
|
+
}
|
|
738
|
+
|
|
559
739
|
// Recursively process properties
|
|
560
740
|
formattedSchema.properties[propName] = openAISchemaFormat(property, nesting + 1);
|
|
561
741
|
|
|
@@ -575,4 +755,51 @@ function openAISchemaFormat(schema: JSONSchema, nesting: number = 0): JSONSchema
|
|
|
575
755
|
}
|
|
576
756
|
|
|
577
757
|
return formattedSchema
|
|
578
|
-
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function extractTextFromResponse(response: OpenAI.Responses.Response): string {
|
|
761
|
+
if (response.output_text) {
|
|
762
|
+
return response.output_text;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const collected: string[] = [];
|
|
766
|
+
for (const item of response.output ?? []) {
|
|
767
|
+
if (item.type === 'message') {
|
|
768
|
+
const text = item.content
|
|
769
|
+
.map(part => part.type === 'output_text' ? part.text : '')
|
|
770
|
+
.join('');
|
|
771
|
+
if (text) {
|
|
772
|
+
collected.push(text);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return collected.join("\n");
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function responseFinishReason(response: OpenAI.Responses.Response, tools?: ToolUse[] | undefined): string | undefined {
|
|
781
|
+
if (tools && tools.length > 0) {
|
|
782
|
+
return "tool_use";
|
|
783
|
+
}
|
|
784
|
+
if (response.status === 'incomplete') {
|
|
785
|
+
if (response.incomplete_details?.reason === 'max_output_tokens') {
|
|
786
|
+
return 'length';
|
|
787
|
+
}
|
|
788
|
+
return response.incomplete_details?.reason ?? 'incomplete';
|
|
789
|
+
}
|
|
790
|
+
if (response.status && response.status !== 'completed') {
|
|
791
|
+
return response.status;
|
|
792
|
+
}
|
|
793
|
+
return 'stop';
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function safeJsonParse(value: unknown): any {
|
|
797
|
+
if (typeof value !== 'string') {
|
|
798
|
+
return value;
|
|
799
|
+
}
|
|
800
|
+
try {
|
|
801
|
+
return JSON.parse(value);
|
|
802
|
+
} catch {
|
|
803
|
+
return value;
|
|
804
|
+
}
|
|
805
|
+
}
|