@rcrsr/rill-ext-openai 0.16.0 → 0.18.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/README.md +37 -27
- package/dist/index.d.ts +2 -2
- package/dist/index.js +612 -465
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -5,11 +5,12 @@ import { createRequire } from "module";
|
|
|
5
5
|
import OpenAI from "openai";
|
|
6
6
|
import {
|
|
7
7
|
RuntimeError as RuntimeError6,
|
|
8
|
+
createRillStream,
|
|
8
9
|
emitExtensionEvent,
|
|
9
10
|
createVector,
|
|
10
|
-
isDict as isDict2,
|
|
11
11
|
isVector,
|
|
12
|
-
|
|
12
|
+
structureToTypeValue,
|
|
13
|
+
toCallable
|
|
13
14
|
} from "@rcrsr/rill";
|
|
14
15
|
|
|
15
16
|
// ../../shared/ext-llm/dist/validation.js
|
|
@@ -49,7 +50,7 @@ function validateEmbedBatch(texts) {
|
|
|
49
50
|
if (typeof item !== "string") {
|
|
50
51
|
throw new RuntimeError("RILL-R001", "embed_batch requires list of strings");
|
|
51
52
|
}
|
|
52
|
-
if (item === "") {
|
|
53
|
+
if (item.trim() === "") {
|
|
53
54
|
throw new RuntimeError("RILL-R001", `embed text cannot be empty at index ${i}`);
|
|
54
55
|
}
|
|
55
56
|
validated.push(item);
|
|
@@ -101,28 +102,29 @@ function mapRillType(rillType) {
|
|
|
101
102
|
return jsonType;
|
|
102
103
|
}
|
|
103
104
|
function buildPropertyFromStructuralType(rillType) {
|
|
104
|
-
if (rillType.
|
|
105
|
-
throw new RuntimeError3("RILL-R004", `unsupported type for JSON Schema: ${rillType.
|
|
105
|
+
if (rillType.kind === "closure" || rillType.kind === "tuple") {
|
|
106
|
+
throw new RuntimeError3("RILL-R004", `unsupported type for JSON Schema: ${rillType.kind}`);
|
|
106
107
|
}
|
|
107
|
-
if (rillType.
|
|
108
|
+
if (rillType.kind === "any") {
|
|
108
109
|
return {};
|
|
109
110
|
}
|
|
110
|
-
if (rillType.
|
|
111
|
+
if (rillType.kind === "list") {
|
|
112
|
+
const listType = rillType;
|
|
111
113
|
const property = { type: "array" };
|
|
112
|
-
if (
|
|
113
|
-
property.items = buildPropertyFromStructuralType(
|
|
114
|
+
if (listType.element !== void 0) {
|
|
115
|
+
property.items = buildPropertyFromStructuralType(listType.element);
|
|
114
116
|
}
|
|
115
117
|
return property;
|
|
116
118
|
}
|
|
117
|
-
if (rillType.
|
|
119
|
+
if (rillType.kind === "dict") {
|
|
118
120
|
return { type: "object" };
|
|
119
121
|
}
|
|
120
|
-
return { type: mapRillType(rillType.
|
|
122
|
+
return { type: mapRillType(rillType.kind) };
|
|
121
123
|
}
|
|
122
124
|
function buildJsonSchemaFromStructuralType(type, params) {
|
|
123
125
|
const properties = {};
|
|
124
126
|
const required = [];
|
|
125
|
-
if (type.
|
|
127
|
+
if (type.kind === "closure") {
|
|
126
128
|
const closureParams = type.params ?? [];
|
|
127
129
|
for (let i = 0; i < closureParams.length; i++) {
|
|
128
130
|
const fieldDef = closureParams[i];
|
|
@@ -238,7 +240,7 @@ async function executeToolCall(toolName, toolInput, tools, context) {
|
|
|
238
240
|
return value !== void 0 ? value : void 0;
|
|
239
241
|
});
|
|
240
242
|
} else {
|
|
241
|
-
args = [
|
|
243
|
+
args = [];
|
|
242
244
|
}
|
|
243
245
|
return await invokeCallable(callable, args, context);
|
|
244
246
|
}
|
|
@@ -323,7 +325,7 @@ function patchResponseToolCallNames(response, nameMap) {
|
|
|
323
325
|
}
|
|
324
326
|
}
|
|
325
327
|
}
|
|
326
|
-
async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent, maxTurns = 10, context) {
|
|
328
|
+
async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent, maxTurns = 10, context, yieldChunk, signal) {
|
|
327
329
|
if (tools === void 0) {
|
|
328
330
|
throw new RuntimeError4("RILL-R004", "tools parameter is required");
|
|
329
331
|
}
|
|
@@ -345,8 +347,8 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
|
|
|
345
347
|
const params = callable.kind === "application" ? callable.params ?? [] : callable.kind === "script" ? callable.params : [];
|
|
346
348
|
if (params.length > 0) {
|
|
347
349
|
const closureType = {
|
|
348
|
-
|
|
349
|
-
params: params.map((p2) => ({ name: p2.name, type: p2.type ?? {
|
|
350
|
+
kind: "closure",
|
|
351
|
+
params: params.map((p2) => ({ name: p2.name, type: p2.type ?? { kind: "any" } }))
|
|
350
352
|
};
|
|
351
353
|
const builtSchema = buildJsonSchemaFromStructuralType(closureType, [...params]);
|
|
352
354
|
inputSchema = {
|
|
@@ -371,10 +373,20 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
|
|
|
371
373
|
let currentMessages = [...messages];
|
|
372
374
|
let turnCount = 0;
|
|
373
375
|
while (turnCount < maxTurns) {
|
|
376
|
+
if (signal?.aborted) {
|
|
377
|
+
throw new RuntimeError4("RILL-R004", "tool_loop cancelled");
|
|
378
|
+
}
|
|
374
379
|
turnCount++;
|
|
375
380
|
let response;
|
|
376
381
|
try {
|
|
377
|
-
|
|
382
|
+
if (yieldChunk !== void 0 && callbacks.callAPIStreaming !== void 0) {
|
|
383
|
+
const onTextDelta = (text) => {
|
|
384
|
+
yieldChunk({ type: "text_delta", text });
|
|
385
|
+
};
|
|
386
|
+
response = await callbacks.callAPIStreaming(currentMessages, providerTools, onTextDelta, signal);
|
|
387
|
+
} else {
|
|
388
|
+
response = await callbacks.callAPI(currentMessages, providerTools, signal);
|
|
389
|
+
}
|
|
378
390
|
} catch (error) {
|
|
379
391
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
380
392
|
throw new RuntimeError4("RILL-R004", `Provider API error: ${message}`, void 0, { cause: error });
|
|
@@ -412,6 +424,13 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
|
|
|
412
424
|
for (const toolCall of toolCalls) {
|
|
413
425
|
const { id, name, input } = toolCall;
|
|
414
426
|
emitEvent("tool_call", { tool_name: name, args: input });
|
|
427
|
+
if (yieldChunk !== void 0) {
|
|
428
|
+
yieldChunk({
|
|
429
|
+
type: "tool_call",
|
|
430
|
+
name,
|
|
431
|
+
args: input
|
|
432
|
+
});
|
|
433
|
+
}
|
|
415
434
|
const toolStartTime = Date.now();
|
|
416
435
|
try {
|
|
417
436
|
const result = await executeToolCall(name, input, tools, context);
|
|
@@ -419,6 +438,9 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
|
|
|
419
438
|
toolResults.push({ id, name, result });
|
|
420
439
|
executedToolCalls.push({ name, result });
|
|
421
440
|
consecutiveErrors = 0;
|
|
441
|
+
if (yieldChunk !== void 0) {
|
|
442
|
+
yieldChunk({ type: "tool_result", name, result });
|
|
443
|
+
}
|
|
422
444
|
emitEvent("tool_result", { tool_name: name, duration });
|
|
423
445
|
} catch (error) {
|
|
424
446
|
const duration = Date.now() - toolStartTime;
|
|
@@ -471,6 +493,9 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
|
|
|
471
493
|
turns: turnCount
|
|
472
494
|
};
|
|
473
495
|
}
|
|
496
|
+
function buildResponseMessages(inputMessages, assistantContent) {
|
|
497
|
+
return [...inputMessages, { role: "assistant", content: assistantContent }];
|
|
498
|
+
}
|
|
474
499
|
|
|
475
500
|
// ../../shared/ext-param/dist/param.js
|
|
476
501
|
import { RuntimeError as RuntimeError5 } from "@rcrsr/rill";
|
|
@@ -500,7 +525,7 @@ var p = {
|
|
|
500
525
|
validateParamName(name);
|
|
501
526
|
return {
|
|
502
527
|
name,
|
|
503
|
-
type: {
|
|
528
|
+
type: { kind: "string" },
|
|
504
529
|
defaultValue: void 0,
|
|
505
530
|
annotations: buildAnnotations(desc)
|
|
506
531
|
};
|
|
@@ -517,7 +542,7 @@ var p = {
|
|
|
517
542
|
validateParamName(name);
|
|
518
543
|
return {
|
|
519
544
|
name,
|
|
520
|
-
type: {
|
|
545
|
+
type: { kind: "number" },
|
|
521
546
|
defaultValue: def,
|
|
522
547
|
annotations: buildAnnotations(desc)
|
|
523
548
|
};
|
|
@@ -534,7 +559,7 @@ var p = {
|
|
|
534
559
|
validateParamName(name);
|
|
535
560
|
return {
|
|
536
561
|
name,
|
|
537
|
-
type: {
|
|
562
|
+
type: { kind: "bool" },
|
|
538
563
|
defaultValue: def,
|
|
539
564
|
annotations: buildAnnotations(desc)
|
|
540
565
|
};
|
|
@@ -550,7 +575,7 @@ var p = {
|
|
|
550
575
|
*/
|
|
551
576
|
dict(name, desc, def, fields) {
|
|
552
577
|
validateParamName(name);
|
|
553
|
-
const type = fields !== void 0 ? {
|
|
578
|
+
const type = fields !== void 0 ? { kind: "dict", fields } : { kind: "dict" };
|
|
554
579
|
return {
|
|
555
580
|
name,
|
|
556
581
|
type,
|
|
@@ -568,7 +593,7 @@ var p = {
|
|
|
568
593
|
*/
|
|
569
594
|
list(name, itemType, desc) {
|
|
570
595
|
validateParamName(name);
|
|
571
|
-
const type = itemType !== void 0 ? {
|
|
596
|
+
const type = itemType !== void 0 ? { kind: "list", element: itemType } : { kind: "list" };
|
|
572
597
|
return {
|
|
573
598
|
name,
|
|
574
599
|
type,
|
|
@@ -587,7 +612,7 @@ var p = {
|
|
|
587
612
|
validateParamName(name);
|
|
588
613
|
return {
|
|
589
614
|
name,
|
|
590
|
-
type: {
|
|
615
|
+
type: { kind: "closure" },
|
|
591
616
|
defaultValue: void 0,
|
|
592
617
|
annotations: buildAnnotations(desc)
|
|
593
618
|
};
|
|
@@ -638,238 +663,303 @@ function createOpenAIExtension(config) {
|
|
|
638
663
|
console.warn(`Failed to cleanup OpenAI SDK: ${message}`);
|
|
639
664
|
}
|
|
640
665
|
};
|
|
641
|
-
const
|
|
666
|
+
const fnDict = {
|
|
642
667
|
// IR-4: openai::message
|
|
643
668
|
message: {
|
|
644
669
|
params: [
|
|
645
670
|
p.str("text"),
|
|
646
671
|
p.dict("options", void 0, {}, {
|
|
647
|
-
system: { type: {
|
|
648
|
-
max_tokens: { type: {
|
|
672
|
+
system: { type: { kind: "string" }, defaultValue: "" },
|
|
673
|
+
max_tokens: { type: { kind: "number" }, defaultValue: 0 }
|
|
649
674
|
})
|
|
650
675
|
],
|
|
651
|
-
fn:
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const apiMessages = [];
|
|
662
|
-
if (system !== void 0) {
|
|
663
|
-
apiMessages.push({
|
|
664
|
-
role: "system",
|
|
665
|
-
content: system
|
|
666
|
-
});
|
|
667
|
-
}
|
|
676
|
+
fn: (args, ctx) => {
|
|
677
|
+
const text = args["text"];
|
|
678
|
+
const options = args["options"] ?? {};
|
|
679
|
+
if (text.trim().length === 0) {
|
|
680
|
+
throw new RuntimeError6("RILL-R004", "prompt text cannot be empty");
|
|
681
|
+
}
|
|
682
|
+
const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
|
|
683
|
+
const maxTokens = typeof options["max_tokens"] === "number" && options["max_tokens"] > 0 ? options["max_tokens"] : factoryMaxTokens;
|
|
684
|
+
const apiMessages = [];
|
|
685
|
+
if (system !== void 0) {
|
|
668
686
|
apiMessages.push({
|
|
669
|
-
role: "
|
|
670
|
-
content:
|
|
687
|
+
role: "system",
|
|
688
|
+
content: system
|
|
671
689
|
});
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
690
|
+
}
|
|
691
|
+
apiMessages.push({
|
|
692
|
+
role: "user",
|
|
693
|
+
content: text
|
|
694
|
+
});
|
|
695
|
+
const runner = client.chat.completions.stream({
|
|
696
|
+
model: factoryModel,
|
|
697
|
+
max_completion_tokens: maxTokens,
|
|
698
|
+
messages: apiMessages,
|
|
699
|
+
stream_options: { include_usage: true },
|
|
700
|
+
...factoryTemperature !== void 0 ? { temperature: factoryTemperature } : {}
|
|
701
|
+
});
|
|
702
|
+
async function* chunks() {
|
|
703
|
+
try {
|
|
704
|
+
for await (const chunk of runner) {
|
|
705
|
+
const delta = chunk.choices[0]?.delta?.content;
|
|
706
|
+
if (delta) {
|
|
707
|
+
yield delta;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
} catch (error) {
|
|
711
|
+
throw mapProviderError("OpenAI", error, detectOpenAIError);
|
|
679
712
|
}
|
|
680
|
-
const response = await client.chat.completions.create(apiParams);
|
|
681
|
-
const content = response.choices[0]?.message?.content ?? "";
|
|
682
|
-
const result2 = {
|
|
683
|
-
content,
|
|
684
|
-
model: response.model,
|
|
685
|
-
usage: {
|
|
686
|
-
input: response.usage?.prompt_tokens ?? 0,
|
|
687
|
-
output: response.usage?.completion_tokens ?? 0
|
|
688
|
-
},
|
|
689
|
-
stop_reason: response.choices[0]?.finish_reason ?? "unknown",
|
|
690
|
-
id: response.id,
|
|
691
|
-
messages: [
|
|
692
|
-
...system ? [{ role: "system", content: system }] : [],
|
|
693
|
-
{ role: "user", content: text },
|
|
694
|
-
{ role: "assistant", content }
|
|
695
|
-
]
|
|
696
|
-
};
|
|
697
|
-
const duration = Date.now() - startTime;
|
|
698
|
-
emitExtensionEvent(ctx, {
|
|
699
|
-
event: "openai:message",
|
|
700
|
-
subsystem: "extension:openai",
|
|
701
|
-
duration,
|
|
702
|
-
model: response.model,
|
|
703
|
-
usage: result2.usage,
|
|
704
|
-
request: apiMessages,
|
|
705
|
-
content
|
|
706
|
-
});
|
|
707
|
-
return result2;
|
|
708
|
-
} catch (error) {
|
|
709
|
-
const duration = Date.now() - startTime;
|
|
710
|
-
const rillError = mapProviderError(
|
|
711
|
-
"OpenAI",
|
|
712
|
-
error,
|
|
713
|
-
detectOpenAIError
|
|
714
|
-
);
|
|
715
|
-
emitExtensionEvent(ctx, {
|
|
716
|
-
event: "openai:error",
|
|
717
|
-
subsystem: "extension:openai",
|
|
718
|
-
error: rillError.message,
|
|
719
|
-
duration
|
|
720
|
-
});
|
|
721
|
-
throw rillError;
|
|
722
713
|
}
|
|
714
|
+
const resolve = async () => {
|
|
715
|
+
const startTime = Date.now();
|
|
716
|
+
try {
|
|
717
|
+
const response = await runner.finalChatCompletion();
|
|
718
|
+
const content = response.choices[0]?.message?.content ?? "";
|
|
719
|
+
const result = {
|
|
720
|
+
content,
|
|
721
|
+
model: response.model,
|
|
722
|
+
usage: {
|
|
723
|
+
input: response.usage?.prompt_tokens ?? 0,
|
|
724
|
+
output: response.usage?.completion_tokens ?? 0
|
|
725
|
+
},
|
|
726
|
+
stop_reason: response.choices[0]?.finish_reason ?? "unknown",
|
|
727
|
+
id: response.id,
|
|
728
|
+
messages: buildResponseMessages(
|
|
729
|
+
[
|
|
730
|
+
...system ? [{ role: "system", content: system }] : [],
|
|
731
|
+
{ role: "user", content: text }
|
|
732
|
+
],
|
|
733
|
+
content
|
|
734
|
+
)
|
|
735
|
+
};
|
|
736
|
+
const duration = Date.now() - startTime;
|
|
737
|
+
emitExtensionEvent(ctx, {
|
|
738
|
+
event: "openai:message",
|
|
739
|
+
subsystem: "extension:openai",
|
|
740
|
+
duration,
|
|
741
|
+
model: response.model,
|
|
742
|
+
usage: result.usage,
|
|
743
|
+
request: apiMessages,
|
|
744
|
+
content
|
|
745
|
+
});
|
|
746
|
+
return result;
|
|
747
|
+
} catch (error) {
|
|
748
|
+
const duration = Date.now() - startTime;
|
|
749
|
+
const rillError = mapProviderError("OpenAI", error, detectOpenAIError);
|
|
750
|
+
emitExtensionEvent(ctx, {
|
|
751
|
+
event: "openai:error",
|
|
752
|
+
subsystem: "extension:openai",
|
|
753
|
+
error: rillError.message,
|
|
754
|
+
duration
|
|
755
|
+
});
|
|
756
|
+
throw rillError;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
const retType = {
|
|
760
|
+
kind: "dict",
|
|
761
|
+
fields: {
|
|
762
|
+
content: { type: { kind: "string" } },
|
|
763
|
+
model: { type: { kind: "string" } },
|
|
764
|
+
usage: { type: { kind: "dict", fields: { input: { type: { kind: "number" } }, output: { type: { kind: "number" } } } } },
|
|
765
|
+
stop_reason: { type: { kind: "string" } },
|
|
766
|
+
id: { type: { kind: "string" } },
|
|
767
|
+
messages: { type: { kind: "list", element: { kind: "dict" } } }
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
return createRillStream({
|
|
771
|
+
chunks: chunks(),
|
|
772
|
+
resolve,
|
|
773
|
+
dispose: () => {
|
|
774
|
+
runner.abort();
|
|
775
|
+
},
|
|
776
|
+
chunkType: { kind: "string" },
|
|
777
|
+
retType
|
|
778
|
+
});
|
|
723
779
|
},
|
|
724
780
|
annotations: { description: "Send single message to OpenAI API" },
|
|
725
|
-
returnType:
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
781
|
+
returnType: structureToTypeValue({
|
|
782
|
+
kind: "stream",
|
|
783
|
+
chunk: { kind: "string" },
|
|
784
|
+
ret: {
|
|
785
|
+
kind: "dict",
|
|
786
|
+
fields: {
|
|
787
|
+
content: { type: { kind: "string" } },
|
|
788
|
+
model: { type: { kind: "string" } },
|
|
789
|
+
usage: { type: { kind: "dict", fields: { input: { type: { kind: "number" } }, output: { type: { kind: "number" } } } } },
|
|
790
|
+
stop_reason: { type: { kind: "string" } },
|
|
791
|
+
id: { type: { kind: "string" } },
|
|
792
|
+
messages: { type: { kind: "list", element: { kind: "dict" } } }
|
|
793
|
+
}
|
|
734
794
|
}
|
|
735
795
|
})
|
|
736
796
|
},
|
|
737
797
|
// IR-5: openai::messages
|
|
738
798
|
messages: {
|
|
739
799
|
params: [
|
|
740
|
-
p.list("messages", {
|
|
800
|
+
p.list("messages", { kind: "dict", fields: { role: { type: { kind: "string" } }, content: { type: { kind: "string" } } } }),
|
|
741
801
|
p.dict("options", void 0, {}, {
|
|
742
|
-
system: { type: {
|
|
743
|
-
max_tokens: { type: {
|
|
802
|
+
system: { type: { kind: "string" }, defaultValue: "" },
|
|
803
|
+
max_tokens: { type: { kind: "number" }, defaultValue: 0 }
|
|
744
804
|
})
|
|
745
805
|
],
|
|
746
|
-
fn:
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
806
|
+
fn: (args, ctx) => {
|
|
807
|
+
const messages = args["messages"];
|
|
808
|
+
const options = args["options"] ?? {};
|
|
809
|
+
if (messages.length === 0) {
|
|
810
|
+
throw new RuntimeError6(
|
|
811
|
+
"RILL-R004",
|
|
812
|
+
"messages list cannot be empty"
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
|
|
816
|
+
const maxTokens = typeof options["max_tokens"] === "number" && options["max_tokens"] > 0 ? options["max_tokens"] : factoryMaxTokens;
|
|
817
|
+
const apiMessages = [];
|
|
818
|
+
if (system !== void 0) {
|
|
819
|
+
apiMessages.push({
|
|
820
|
+
role: "system",
|
|
821
|
+
content: system
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
for (let i = 0; i < messages.length; i++) {
|
|
825
|
+
const msg = messages[i];
|
|
826
|
+
if (!msg || typeof msg !== "object" || !("role" in msg)) {
|
|
752
827
|
throw new RuntimeError6(
|
|
753
828
|
"RILL-R004",
|
|
754
|
-
"
|
|
829
|
+
"message missing required 'role' field"
|
|
755
830
|
);
|
|
756
831
|
}
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (system !== void 0) {
|
|
761
|
-
apiMessages.push({
|
|
762
|
-
role: "system",
|
|
763
|
-
content: system
|
|
764
|
-
});
|
|
832
|
+
const role = msg["role"];
|
|
833
|
+
if (role !== "user" && role !== "assistant" && role !== "tool") {
|
|
834
|
+
throw new RuntimeError6("RILL-R004", `invalid role '${role}'`);
|
|
765
835
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (!msg || typeof msg !== "object" || !("role" in msg)) {
|
|
836
|
+
if (role === "user" || role === "tool") {
|
|
837
|
+
if (!("content" in msg) || typeof msg["content"] !== "string") {
|
|
769
838
|
throw new RuntimeError6(
|
|
770
839
|
"RILL-R004",
|
|
771
|
-
|
|
840
|
+
`${role} message requires 'content'`
|
|
772
841
|
);
|
|
773
842
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
843
|
+
apiMessages.push({
|
|
844
|
+
role,
|
|
845
|
+
content: msg["content"]
|
|
846
|
+
});
|
|
847
|
+
} else if (role === "assistant") {
|
|
848
|
+
const hasContent = "content" in msg && msg["content"];
|
|
849
|
+
const hasToolCalls = "tool_calls" in msg && msg["tool_calls"];
|
|
850
|
+
if (!hasContent && !hasToolCalls) {
|
|
851
|
+
throw new RuntimeError6(
|
|
852
|
+
"RILL-R004",
|
|
853
|
+
"assistant message requires 'content' or 'tool_calls'"
|
|
854
|
+
);
|
|
777
855
|
}
|
|
778
|
-
if (
|
|
779
|
-
if (!("content" in msg) || typeof msg["content"] !== "string") {
|
|
780
|
-
throw new RuntimeError6(
|
|
781
|
-
"RILL-R004",
|
|
782
|
-
`${role} message requires 'content'`
|
|
783
|
-
);
|
|
784
|
-
}
|
|
856
|
+
if (hasContent) {
|
|
785
857
|
apiMessages.push({
|
|
786
|
-
role,
|
|
858
|
+
role: "assistant",
|
|
787
859
|
content: msg["content"]
|
|
788
860
|
});
|
|
789
|
-
} else if (role === "assistant") {
|
|
790
|
-
const hasContent = "content" in msg && msg["content"];
|
|
791
|
-
const hasToolCalls = "tool_calls" in msg && msg["tool_calls"];
|
|
792
|
-
if (!hasContent && !hasToolCalls) {
|
|
793
|
-
throw new RuntimeError6(
|
|
794
|
-
"RILL-R004",
|
|
795
|
-
"assistant message requires 'content' or 'tool_calls'"
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
if (hasContent) {
|
|
799
|
-
apiMessages.push({
|
|
800
|
-
role: "assistant",
|
|
801
|
-
content: msg["content"]
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
861
|
}
|
|
805
862
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
863
|
+
}
|
|
864
|
+
const runner = client.chat.completions.stream({
|
|
865
|
+
model: factoryModel,
|
|
866
|
+
max_completion_tokens: maxTokens,
|
|
867
|
+
messages: apiMessages,
|
|
868
|
+
stream_options: { include_usage: true },
|
|
869
|
+
...factoryTemperature !== void 0 ? { temperature: factoryTemperature } : {}
|
|
870
|
+
});
|
|
871
|
+
async function* chunks() {
|
|
872
|
+
try {
|
|
873
|
+
for await (const chunk of runner) {
|
|
874
|
+
const delta = chunk.choices[0]?.delta?.content;
|
|
875
|
+
if (delta) {
|
|
876
|
+
yield delta;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
} catch (error) {
|
|
880
|
+
throw mapProviderError("OpenAI", error, detectOpenAIError);
|
|
813
881
|
}
|
|
814
|
-
const response = await client.chat.completions.create(apiParams);
|
|
815
|
-
const content = response.choices[0]?.message?.content ?? "";
|
|
816
|
-
const fullMessages = [
|
|
817
|
-
...messages.map((m) => {
|
|
818
|
-
const normalized = { role: m["role"] };
|
|
819
|
-
if ("content" in m) normalized["content"] = m["content"];
|
|
820
|
-
if ("tool_calls" in m) normalized["tool_calls"] = m["tool_calls"];
|
|
821
|
-
return normalized;
|
|
822
|
-
}),
|
|
823
|
-
{ role: "assistant", content }
|
|
824
|
-
];
|
|
825
|
-
const result2 = {
|
|
826
|
-
content,
|
|
827
|
-
model: response.model,
|
|
828
|
-
usage: {
|
|
829
|
-
input: response.usage?.prompt_tokens ?? 0,
|
|
830
|
-
output: response.usage?.completion_tokens ?? 0
|
|
831
|
-
},
|
|
832
|
-
stop_reason: response.choices[0]?.finish_reason ?? "unknown",
|
|
833
|
-
id: response.id,
|
|
834
|
-
messages: fullMessages
|
|
835
|
-
};
|
|
836
|
-
const duration = Date.now() - startTime;
|
|
837
|
-
emitExtensionEvent(ctx, {
|
|
838
|
-
event: "openai:messages",
|
|
839
|
-
subsystem: "extension:openai",
|
|
840
|
-
duration,
|
|
841
|
-
model: response.model,
|
|
842
|
-
usage: result2.usage,
|
|
843
|
-
request: apiMessages,
|
|
844
|
-
content
|
|
845
|
-
});
|
|
846
|
-
return result2;
|
|
847
|
-
} catch (error) {
|
|
848
|
-
const duration = Date.now() - startTime;
|
|
849
|
-
const rillError = mapProviderError(
|
|
850
|
-
"OpenAI",
|
|
851
|
-
error,
|
|
852
|
-
detectOpenAIError
|
|
853
|
-
);
|
|
854
|
-
emitExtensionEvent(ctx, {
|
|
855
|
-
event: "openai:error",
|
|
856
|
-
subsystem: "extension:openai",
|
|
857
|
-
error: rillError.message,
|
|
858
|
-
duration
|
|
859
|
-
});
|
|
860
|
-
throw rillError;
|
|
861
882
|
}
|
|
883
|
+
const resolve = async () => {
|
|
884
|
+
const startTime = Date.now();
|
|
885
|
+
try {
|
|
886
|
+
const response = await runner.finalChatCompletion();
|
|
887
|
+
const content = response.choices[0]?.message?.content ?? "";
|
|
888
|
+
const result = {
|
|
889
|
+
content,
|
|
890
|
+
model: response.model,
|
|
891
|
+
usage: {
|
|
892
|
+
input: response.usage?.prompt_tokens ?? 0,
|
|
893
|
+
output: response.usage?.completion_tokens ?? 0
|
|
894
|
+
},
|
|
895
|
+
stop_reason: response.choices[0]?.finish_reason ?? "unknown",
|
|
896
|
+
id: response.id,
|
|
897
|
+
messages: buildResponseMessages(
|
|
898
|
+
messages.map((m) => ({
|
|
899
|
+
role: m["role"],
|
|
900
|
+
content: m["content"] ?? ""
|
|
901
|
+
})),
|
|
902
|
+
content
|
|
903
|
+
)
|
|
904
|
+
};
|
|
905
|
+
const duration = Date.now() - startTime;
|
|
906
|
+
emitExtensionEvent(ctx, {
|
|
907
|
+
event: "openai:messages",
|
|
908
|
+
subsystem: "extension:openai",
|
|
909
|
+
duration,
|
|
910
|
+
model: response.model,
|
|
911
|
+
usage: result.usage,
|
|
912
|
+
request: apiMessages,
|
|
913
|
+
content
|
|
914
|
+
});
|
|
915
|
+
return result;
|
|
916
|
+
} catch (error) {
|
|
917
|
+
const duration = Date.now() - startTime;
|
|
918
|
+
const rillError = mapProviderError("OpenAI", error, detectOpenAIError);
|
|
919
|
+
emitExtensionEvent(ctx, {
|
|
920
|
+
event: "openai:error",
|
|
921
|
+
subsystem: "extension:openai",
|
|
922
|
+
error: rillError.message,
|
|
923
|
+
duration
|
|
924
|
+
});
|
|
925
|
+
throw rillError;
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
const retType = {
|
|
929
|
+
kind: "dict",
|
|
930
|
+
fields: {
|
|
931
|
+
content: { type: { kind: "string" } },
|
|
932
|
+
model: { type: { kind: "string" } },
|
|
933
|
+
usage: { type: { kind: "dict", fields: { input: { type: { kind: "number" } }, output: { type: { kind: "number" } } } } },
|
|
934
|
+
stop_reason: { type: { kind: "string" } },
|
|
935
|
+
id: { type: { kind: "string" } },
|
|
936
|
+
messages: { type: { kind: "list", element: { kind: "dict" } } }
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
return createRillStream({
|
|
940
|
+
chunks: chunks(),
|
|
941
|
+
resolve,
|
|
942
|
+
dispose: () => {
|
|
943
|
+
runner.abort();
|
|
944
|
+
},
|
|
945
|
+
chunkType: { kind: "string" },
|
|
946
|
+
retType
|
|
947
|
+
});
|
|
862
948
|
},
|
|
863
949
|
annotations: { description: "Send multi-turn conversation to OpenAI API" },
|
|
864
|
-
returnType:
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
950
|
+
returnType: structureToTypeValue({
|
|
951
|
+
kind: "stream",
|
|
952
|
+
chunk: { kind: "string" },
|
|
953
|
+
ret: {
|
|
954
|
+
kind: "dict",
|
|
955
|
+
fields: {
|
|
956
|
+
content: { type: { kind: "string" } },
|
|
957
|
+
model: { type: { kind: "string" } },
|
|
958
|
+
usage: { type: { kind: "dict", fields: { input: { type: { kind: "number" } }, output: { type: { kind: "number" } } } } },
|
|
959
|
+
stop_reason: { type: { kind: "string" } },
|
|
960
|
+
id: { type: { kind: "string" } },
|
|
961
|
+
messages: { type: { kind: "list", element: { kind: "dict" } } }
|
|
962
|
+
}
|
|
873
963
|
}
|
|
874
964
|
})
|
|
875
965
|
},
|
|
@@ -922,7 +1012,7 @@ function createOpenAIExtension(config) {
|
|
|
922
1012
|
}
|
|
923
1013
|
},
|
|
924
1014
|
annotations: { description: "Generate embedding vector for text" },
|
|
925
|
-
returnType:
|
|
1015
|
+
returnType: structureToTypeValue({ kind: "vector" })
|
|
926
1016
|
},
|
|
927
1017
|
// IR-7: openai::embed_batch
|
|
928
1018
|
embed_batch: {
|
|
@@ -983,263 +1073,313 @@ function createOpenAIExtension(config) {
|
|
|
983
1073
|
}
|
|
984
1074
|
},
|
|
985
1075
|
annotations: { description: "Generate embedding vectors for multiple texts" },
|
|
986
|
-
returnType:
|
|
1076
|
+
returnType: structureToTypeValue({ kind: "list", element: { kind: "vector" } })
|
|
987
1077
|
},
|
|
988
1078
|
// IR-8: openai::tool_loop
|
|
989
1079
|
tool_loop: {
|
|
990
1080
|
params: [
|
|
991
1081
|
p.str("prompt"),
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1082
|
+
{
|
|
1083
|
+
name: "tools",
|
|
1084
|
+
type: { kind: "dict", valueType: { kind: "closure" } },
|
|
1085
|
+
defaultValue: void 0,
|
|
1086
|
+
annotations: {}
|
|
1087
|
+
},
|
|
1088
|
+
p.dict("options", void 0, void 0, {
|
|
1089
|
+
system: { type: { kind: "string" }, defaultValue: "" },
|
|
1090
|
+
max_tokens: { type: { kind: "number" }, defaultValue: 0 },
|
|
1091
|
+
max_errors: { type: { kind: "number" }, defaultValue: 3 },
|
|
1092
|
+
max_turns: { type: { kind: "number" }, defaultValue: 10 },
|
|
1093
|
+
messages: { type: { kind: "list", element: { kind: "dict", fields: { role: { type: { kind: "string" } }, content: { type: { kind: "string" } } } } }, defaultValue: [] }
|
|
999
1094
|
})
|
|
1000
1095
|
],
|
|
1001
|
-
fn:
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1096
|
+
fn: (args, ctx) => {
|
|
1097
|
+
const prompt = args["prompt"];
|
|
1098
|
+
const toolsDict = args["tools"];
|
|
1099
|
+
const options = args["options"] ?? {};
|
|
1100
|
+
if (prompt.trim().length === 0) {
|
|
1101
|
+
throw new RuntimeError6("RILL-R004", "prompt text cannot be empty");
|
|
1102
|
+
}
|
|
1103
|
+
const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
|
|
1104
|
+
const maxTokens = typeof options["max_tokens"] === "number" && options["max_tokens"] > 0 ? options["max_tokens"] : factoryMaxTokens;
|
|
1105
|
+
const maxErrors = typeof options["max_errors"] === "number" ? options["max_errors"] : 3;
|
|
1106
|
+
const maxTurns = typeof options["max_turns"] === "number" ? options["max_turns"] : 10;
|
|
1107
|
+
const messages = [];
|
|
1108
|
+
if (system !== void 0) {
|
|
1109
|
+
messages.push({
|
|
1110
|
+
role: "system",
|
|
1111
|
+
content: system
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
if ("messages" in options && Array.isArray(options["messages"])) {
|
|
1115
|
+
const prependedMessages = options["messages"];
|
|
1116
|
+
for (const msg of prependedMessages) {
|
|
1117
|
+
if (!msg || typeof msg !== "object" || !("role" in msg)) {
|
|
1118
|
+
throw new RuntimeError6(
|
|
1119
|
+
"RILL-R004",
|
|
1120
|
+
"message missing required 'role' field"
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
const role = msg["role"];
|
|
1124
|
+
if (role !== "user" && role !== "assistant") {
|
|
1125
|
+
throw new RuntimeError6("RILL-R004", `invalid role '${role}'`);
|
|
1126
|
+
}
|
|
1127
|
+
if (!("content" in msg) || typeof msg["content"] !== "string") {
|
|
1128
|
+
throw new RuntimeError6(
|
|
1129
|
+
"RILL-R004",
|
|
1130
|
+
`${role} message requires 'content'`
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1021
1133
|
messages.push({
|
|
1022
|
-
role
|
|
1023
|
-
content:
|
|
1134
|
+
role,
|
|
1135
|
+
content: msg["content"]
|
|
1024
1136
|
});
|
|
1025
1137
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1138
|
+
}
|
|
1139
|
+
messages.push({
|
|
1140
|
+
role: "user",
|
|
1141
|
+
content: prompt
|
|
1142
|
+
});
|
|
1143
|
+
const callbacks = {
|
|
1144
|
+
// Build OpenAI Tool format from tool definitions
|
|
1145
|
+
buildTools: (toolDefs) => {
|
|
1146
|
+
return toolDefs.map((def) => ({
|
|
1147
|
+
type: "function",
|
|
1148
|
+
function: {
|
|
1149
|
+
name: def.name,
|
|
1150
|
+
description: def.description,
|
|
1151
|
+
parameters: def.input_schema
|
|
1034
1152
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1153
|
+
}));
|
|
1154
|
+
},
|
|
1155
|
+
// Call OpenAI API (non-streaming fallback)
|
|
1156
|
+
callAPI: async (msgs, tools, signal) => {
|
|
1157
|
+
const apiParams = {
|
|
1158
|
+
model: factoryModel,
|
|
1159
|
+
max_completion_tokens: maxTokens,
|
|
1160
|
+
messages: msgs,
|
|
1161
|
+
tools,
|
|
1162
|
+
tool_choice: "auto"
|
|
1163
|
+
};
|
|
1164
|
+
if (factoryTemperature !== void 0) {
|
|
1165
|
+
apiParams.temperature = factoryTemperature;
|
|
1166
|
+
}
|
|
1167
|
+
const response = await client.chat.completions.create(apiParams, { signal });
|
|
1168
|
+
return {
|
|
1169
|
+
...response,
|
|
1170
|
+
usage: {
|
|
1171
|
+
input_tokens: response.usage?.prompt_tokens ?? 0,
|
|
1172
|
+
output_tokens: response.usage?.completion_tokens ?? 0
|
|
1038
1173
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1174
|
+
};
|
|
1175
|
+
},
|
|
1176
|
+
// Call OpenAI API with streaming — IR-3: callAPIStreaming
|
|
1177
|
+
callAPIStreaming: async (msgs, tools, onTextDelta, signal) => {
|
|
1178
|
+
const streamRunner = client.chat.completions.stream({
|
|
1179
|
+
model: factoryModel,
|
|
1180
|
+
max_completion_tokens: maxTokens,
|
|
1181
|
+
messages: msgs,
|
|
1182
|
+
tools,
|
|
1183
|
+
tool_choice: "auto",
|
|
1184
|
+
stream_options: { include_usage: true },
|
|
1185
|
+
...factoryTemperature !== void 0 ? { temperature: factoryTemperature } : {}
|
|
1186
|
+
}, { signal });
|
|
1187
|
+
streamRunner.on("content", (delta) => {
|
|
1188
|
+
onTextDelta(delta);
|
|
1189
|
+
});
|
|
1190
|
+
const response = await streamRunner.finalChatCompletion();
|
|
1191
|
+
return {
|
|
1192
|
+
...response,
|
|
1193
|
+
usage: {
|
|
1194
|
+
input_tokens: response.usage?.prompt_tokens ?? 0,
|
|
1195
|
+
output_tokens: response.usage?.completion_tokens ?? 0
|
|
1044
1196
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1197
|
+
};
|
|
1198
|
+
},
|
|
1199
|
+
// Extract tool calls from OpenAI response
|
|
1200
|
+
extractToolCalls: (response) => {
|
|
1201
|
+
if (!response || typeof response !== "object" || !("choices" in response)) {
|
|
1202
|
+
return null;
|
|
1049
1203
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
apiParams.temperature = factoryTemperature;
|
|
1204
|
+
const choices = response.choices;
|
|
1205
|
+
if (!Array.isArray(choices) || choices.length === 0) {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
const choice = choices[0];
|
|
1209
|
+
if (!choice || typeof choice !== "object" || !("message" in choice)) {
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
const message = choice.message;
|
|
1213
|
+
if (!message || typeof message !== "object" || !("tool_calls" in message)) {
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
const toolCalls = message.tool_calls;
|
|
1217
|
+
if (!toolCalls || !Array.isArray(toolCalls)) {
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
const functionToolCalls = toolCalls.filter(
|
|
1221
|
+
(tc) => typeof tc === "object" && tc !== null && "type" in tc && tc.type === "function"
|
|
1222
|
+
);
|
|
1223
|
+
return functionToolCalls.map((tc) => {
|
|
1224
|
+
const functionCall = tc;
|
|
1225
|
+
const args2 = functionCall.function.arguments;
|
|
1226
|
+
let parsedArgs;
|
|
1227
|
+
try {
|
|
1228
|
+
parsedArgs = JSON.parse(args2);
|
|
1229
|
+
} catch {
|
|
1230
|
+
parsedArgs = {};
|
|
1078
1231
|
}
|
|
1079
|
-
const response2 = await client.chat.completions.create(apiParams);
|
|
1080
1232
|
return {
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
output_tokens: response2.usage?.completion_tokens ?? 0
|
|
1085
|
-
}
|
|
1233
|
+
id: tc.id,
|
|
1234
|
+
name: functionCall.function.name,
|
|
1235
|
+
input: parsedArgs
|
|
1086
1236
|
};
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
const choices = response2.choices;
|
|
1094
|
-
if (!Array.isArray(choices) || choices.length === 0) {
|
|
1095
|
-
return null;
|
|
1096
|
-
}
|
|
1097
|
-
const choice = choices[0];
|
|
1098
|
-
if (!choice || typeof choice !== "object" || !("message" in choice)) {
|
|
1099
|
-
return null;
|
|
1100
|
-
}
|
|
1101
|
-
const message = choice.message;
|
|
1102
|
-
if (!message || typeof message !== "object" || !("tool_calls" in message)) {
|
|
1103
|
-
return null;
|
|
1104
|
-
}
|
|
1105
|
-
const toolCalls = message.tool_calls;
|
|
1106
|
-
if (!toolCalls || !Array.isArray(toolCalls)) {
|
|
1107
|
-
return null;
|
|
1108
|
-
}
|
|
1109
|
-
const functionToolCalls = toolCalls.filter(
|
|
1110
|
-
(tc) => typeof tc === "object" && tc !== null && "type" in tc && tc.type === "function"
|
|
1111
|
-
);
|
|
1112
|
-
return functionToolCalls.map((tc) => {
|
|
1113
|
-
const functionCall = tc;
|
|
1114
|
-
const args2 = functionCall.function.arguments;
|
|
1115
|
-
let parsedArgs;
|
|
1116
|
-
try {
|
|
1117
|
-
parsedArgs = JSON.parse(args2);
|
|
1118
|
-
} catch {
|
|
1119
|
-
parsedArgs = {};
|
|
1120
|
-
}
|
|
1121
|
-
return {
|
|
1122
|
-
id: tc.id,
|
|
1123
|
-
name: functionCall.function.name,
|
|
1124
|
-
input: parsedArgs
|
|
1125
|
-
};
|
|
1126
|
-
});
|
|
1127
|
-
},
|
|
1128
|
-
// Extract assistant message (with tool_calls) from OpenAI response
|
|
1129
|
-
formatAssistantMessage: (response2) => {
|
|
1130
|
-
if (!response2 || typeof response2 !== "object" || !("choices" in response2)) {
|
|
1131
|
-
return null;
|
|
1132
|
-
}
|
|
1133
|
-
const choices = response2.choices;
|
|
1134
|
-
if (!Array.isArray(choices) || choices.length === 0) {
|
|
1135
|
-
return null;
|
|
1136
|
-
}
|
|
1137
|
-
const choice = choices[0];
|
|
1138
|
-
if (!choice || typeof choice !== "object" || !("message" in choice)) {
|
|
1139
|
-
return null;
|
|
1140
|
-
}
|
|
1141
|
-
return choice.message;
|
|
1142
|
-
},
|
|
1143
|
-
// Format tool results into OpenAI message format
|
|
1144
|
-
formatToolResult: (toolResults) => {
|
|
1145
|
-
return toolResults.map((tr) => ({
|
|
1146
|
-
role: "tool",
|
|
1147
|
-
tool_call_id: tr.id,
|
|
1148
|
-
content: tr.error ? JSON.stringify({ error: tr.error, code: "RILL-R001" }) : typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result)
|
|
1149
|
-
}));
|
|
1237
|
+
});
|
|
1238
|
+
},
|
|
1239
|
+
// Extract assistant message (with tool_calls) from OpenAI response
|
|
1240
|
+
formatAssistantMessage: (response) => {
|
|
1241
|
+
if (!response || typeof response !== "object" || !("choices" in response)) {
|
|
1242
|
+
return null;
|
|
1150
1243
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
options["tools"],
|
|
1155
|
-
maxErrors,
|
|
1156
|
-
callbacks,
|
|
1157
|
-
(event, data) => {
|
|
1158
|
-
const eventMap = {
|
|
1159
|
-
tool_call: "openai:tool_call",
|
|
1160
|
-
tool_result: "openai:tool_result"
|
|
1161
|
-
};
|
|
1162
|
-
emitExtensionEvent(ctx, {
|
|
1163
|
-
event: eventMap[event] || event,
|
|
1164
|
-
subsystem: "extension:openai",
|
|
1165
|
-
...data
|
|
1166
|
-
});
|
|
1167
|
-
},
|
|
1168
|
-
maxTurns,
|
|
1169
|
-
ctx
|
|
1170
|
-
);
|
|
1171
|
-
const response = loopResult.response;
|
|
1172
|
-
const content = response?.choices[0]?.message?.content ?? "";
|
|
1173
|
-
const stopReason = loopResult.turns >= maxTurns ? "max_turns" : response?.choices[0]?.finish_reason ?? "stop";
|
|
1174
|
-
const fullMessages = [];
|
|
1175
|
-
for (const msg of messages) {
|
|
1176
|
-
if ("role" in msg && msg.role !== "system") {
|
|
1177
|
-
const historyMsg = {
|
|
1178
|
-
role: msg.role
|
|
1179
|
-
};
|
|
1180
|
-
if ("content" in msg && msg.content) {
|
|
1181
|
-
historyMsg["content"] = msg.content;
|
|
1182
|
-
}
|
|
1183
|
-
if ("tool_calls" in msg && msg.tool_calls) {
|
|
1184
|
-
historyMsg["tool_calls"] = msg.tool_calls;
|
|
1185
|
-
}
|
|
1186
|
-
fullMessages.push(historyMsg);
|
|
1244
|
+
const choices = response.choices;
|
|
1245
|
+
if (!Array.isArray(choices) || choices.length === 0) {
|
|
1246
|
+
return null;
|
|
1187
1247
|
}
|
|
1248
|
+
const choice = choices[0];
|
|
1249
|
+
if (!choice || typeof choice !== "object" || !("message" in choice)) {
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1252
|
+
return choice.message;
|
|
1253
|
+
},
|
|
1254
|
+
// Format tool results into OpenAI message format
|
|
1255
|
+
formatToolResult: (toolResults) => {
|
|
1256
|
+
return toolResults.map((tr) => ({
|
|
1257
|
+
role: "tool",
|
|
1258
|
+
tool_call_id: tr.id,
|
|
1259
|
+
content: tr.error ? JSON.stringify({ error: tr.error, code: "RILL-R001" }) : typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result)
|
|
1260
|
+
}));
|
|
1188
1261
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1262
|
+
};
|
|
1263
|
+
const chunkBuffer = [];
|
|
1264
|
+
const yieldChunk = (chunk) => {
|
|
1265
|
+
chunkBuffer.push(chunk);
|
|
1266
|
+
};
|
|
1267
|
+
const toolLoopAbortController = new AbortController();
|
|
1268
|
+
const loopPromise = executeToolLoop(
|
|
1269
|
+
messages,
|
|
1270
|
+
toolsDict,
|
|
1271
|
+
maxErrors,
|
|
1272
|
+
callbacks,
|
|
1273
|
+
(event, data) => {
|
|
1274
|
+
const eventMap = {
|
|
1275
|
+
tool_call: "openai:tool_call",
|
|
1276
|
+
tool_result: "openai:tool_result"
|
|
1277
|
+
};
|
|
1278
|
+
emitExtensionEvent(ctx, {
|
|
1279
|
+
event: eventMap[event] || event,
|
|
1280
|
+
subsystem: "extension:openai",
|
|
1281
|
+
...data
|
|
1193
1282
|
});
|
|
1283
|
+
},
|
|
1284
|
+
maxTurns,
|
|
1285
|
+
ctx,
|
|
1286
|
+
yieldChunk,
|
|
1287
|
+
toolLoopAbortController.signal
|
|
1288
|
+
);
|
|
1289
|
+
async function* chunks() {
|
|
1290
|
+
try {
|
|
1291
|
+
await loopPromise;
|
|
1292
|
+
for (const chunk of chunkBuffer) {
|
|
1293
|
+
yield chunk;
|
|
1294
|
+
}
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
const rillError = mapProviderError("OpenAI", error, detectOpenAIError);
|
|
1297
|
+
throw rillError;
|
|
1194
1298
|
}
|
|
1195
|
-
const result2 = {
|
|
1196
|
-
content,
|
|
1197
|
-
model: factoryModel,
|
|
1198
|
-
usage: {
|
|
1199
|
-
input: loopResult.totalTokens.input,
|
|
1200
|
-
output: loopResult.totalTokens.output
|
|
1201
|
-
},
|
|
1202
|
-
stop_reason: stopReason,
|
|
1203
|
-
turns: loopResult.turns,
|
|
1204
|
-
messages: fullMessages
|
|
1205
|
-
};
|
|
1206
|
-
const duration = Date.now() - startTime;
|
|
1207
|
-
emitExtensionEvent(ctx, {
|
|
1208
|
-
event: "openai:tool_loop",
|
|
1209
|
-
subsystem: "extension:openai",
|
|
1210
|
-
turns: loopResult.turns,
|
|
1211
|
-
total_duration: duration,
|
|
1212
|
-
usage: result2.usage,
|
|
1213
|
-
request: messages,
|
|
1214
|
-
content
|
|
1215
|
-
});
|
|
1216
|
-
return result2;
|
|
1217
|
-
} catch (error) {
|
|
1218
|
-
const duration = Date.now() - startTime;
|
|
1219
|
-
const rillError = mapProviderError(
|
|
1220
|
-
"OpenAI",
|
|
1221
|
-
error,
|
|
1222
|
-
detectOpenAIError
|
|
1223
|
-
);
|
|
1224
|
-
emitExtensionEvent(ctx, {
|
|
1225
|
-
event: "openai:error",
|
|
1226
|
-
subsystem: "extension:openai",
|
|
1227
|
-
error: rillError.message,
|
|
1228
|
-
duration
|
|
1229
|
-
});
|
|
1230
|
-
throw rillError;
|
|
1231
1299
|
}
|
|
1300
|
+
const resolve = async () => {
|
|
1301
|
+
const startTime = Date.now();
|
|
1302
|
+
try {
|
|
1303
|
+
const loopResult = await loopPromise;
|
|
1304
|
+
const response = loopResult.response;
|
|
1305
|
+
const content = response?.choices[0]?.message?.content ?? "";
|
|
1306
|
+
const stopReason = loopResult.turns >= maxTurns ? "max_turns" : response?.choices[0]?.finish_reason ?? "stop";
|
|
1307
|
+
const inputMessages = messages.filter((m) => "role" in m && m["role"] !== "system").map((m) => {
|
|
1308
|
+
const msg = m;
|
|
1309
|
+
return {
|
|
1310
|
+
role: msg["role"],
|
|
1311
|
+
content: msg["content"] == null ? "" : typeof msg["content"] === "string" ? msg["content"] : JSON.stringify(msg["content"])
|
|
1312
|
+
};
|
|
1313
|
+
});
|
|
1314
|
+
const result = {
|
|
1315
|
+
content,
|
|
1316
|
+
model: factoryModel,
|
|
1317
|
+
usage: {
|
|
1318
|
+
input: loopResult.totalTokens.input,
|
|
1319
|
+
output: loopResult.totalTokens.output
|
|
1320
|
+
},
|
|
1321
|
+
stop_reason: stopReason,
|
|
1322
|
+
turns: loopResult.turns,
|
|
1323
|
+
messages: response ? buildResponseMessages(inputMessages, content) : inputMessages
|
|
1324
|
+
};
|
|
1325
|
+
const duration = Date.now() - startTime;
|
|
1326
|
+
emitExtensionEvent(ctx, {
|
|
1327
|
+
event: "openai:tool_loop",
|
|
1328
|
+
subsystem: "extension:openai",
|
|
1329
|
+
turns: loopResult.turns,
|
|
1330
|
+
total_duration: duration,
|
|
1331
|
+
usage: result.usage,
|
|
1332
|
+
request: messages,
|
|
1333
|
+
content
|
|
1334
|
+
});
|
|
1335
|
+
return result;
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
const duration = Date.now() - startTime;
|
|
1338
|
+
const rillError = mapProviderError("OpenAI", error, detectOpenAIError);
|
|
1339
|
+
emitExtensionEvent(ctx, {
|
|
1340
|
+
event: "openai:error",
|
|
1341
|
+
subsystem: "extension:openai",
|
|
1342
|
+
error: rillError.message,
|
|
1343
|
+
duration
|
|
1344
|
+
});
|
|
1345
|
+
throw rillError;
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
const retType = {
|
|
1349
|
+
kind: "dict",
|
|
1350
|
+
fields: {
|
|
1351
|
+
content: { type: { kind: "string" } },
|
|
1352
|
+
model: { type: { kind: "string" } },
|
|
1353
|
+
usage: { type: { kind: "dict", fields: { input: { type: { kind: "number" } }, output: { type: { kind: "number" } } } } },
|
|
1354
|
+
stop_reason: { type: { kind: "string" } },
|
|
1355
|
+
turns: { type: { kind: "number" } },
|
|
1356
|
+
messages: { type: { kind: "list", element: { kind: "dict" } } }
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
return createRillStream({
|
|
1360
|
+
chunks: chunks(),
|
|
1361
|
+
resolve,
|
|
1362
|
+
dispose: () => {
|
|
1363
|
+
toolLoopAbortController.abort();
|
|
1364
|
+
},
|
|
1365
|
+
chunkType: { kind: "dict" },
|
|
1366
|
+
retType
|
|
1367
|
+
});
|
|
1232
1368
|
},
|
|
1233
1369
|
annotations: { description: "Execute tool-use loop with OpenAI API" },
|
|
1234
|
-
returnType:
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1370
|
+
returnType: structureToTypeValue({
|
|
1371
|
+
kind: "stream",
|
|
1372
|
+
chunk: { kind: "dict" },
|
|
1373
|
+
ret: {
|
|
1374
|
+
kind: "dict",
|
|
1375
|
+
fields: {
|
|
1376
|
+
content: { type: { kind: "string" } },
|
|
1377
|
+
model: { type: { kind: "string" } },
|
|
1378
|
+
usage: { type: { kind: "dict", fields: { input: { type: { kind: "number" } }, output: { type: { kind: "number" } } } } },
|
|
1379
|
+
stop_reason: { type: { kind: "string" } },
|
|
1380
|
+
turns: { type: { kind: "number" } },
|
|
1381
|
+
messages: { type: { kind: "list", element: { kind: "dict" } } }
|
|
1382
|
+
}
|
|
1243
1383
|
}
|
|
1244
1384
|
})
|
|
1245
1385
|
},
|
|
@@ -1248,10 +1388,10 @@ function createOpenAIExtension(config) {
|
|
|
1248
1388
|
params: [
|
|
1249
1389
|
p.str("prompt"),
|
|
1250
1390
|
p.dict("options", void 0, {}, {
|
|
1251
|
-
schema: { type: {
|
|
1252
|
-
system: { type: {
|
|
1253
|
-
max_tokens: { type: {
|
|
1254
|
-
messages: { type: {
|
|
1391
|
+
schema: { type: { kind: "dict" } },
|
|
1392
|
+
system: { type: { kind: "string" }, defaultValue: "" },
|
|
1393
|
+
max_tokens: { type: { kind: "number" }, defaultValue: 0 },
|
|
1394
|
+
messages: { type: { kind: "list", element: { kind: "dict", fields: { role: { type: { kind: "string" } }, content: { type: { kind: "string" } } } } }, defaultValue: [] }
|
|
1255
1395
|
})
|
|
1256
1396
|
],
|
|
1257
1397
|
fn: async (args, ctx) => {
|
|
@@ -1268,7 +1408,7 @@ function createOpenAIExtension(config) {
|
|
|
1268
1408
|
const rillSchema = options["schema"];
|
|
1269
1409
|
const jsonSchema = buildJsonSchema(rillSchema);
|
|
1270
1410
|
const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
|
|
1271
|
-
const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
|
|
1411
|
+
const maxTokens = typeof options["max_tokens"] === "number" && options["max_tokens"] > 0 ? options["max_tokens"] : factoryMaxTokens;
|
|
1272
1412
|
const apiMessages = [];
|
|
1273
1413
|
if (system !== void 0) {
|
|
1274
1414
|
apiMessages.push({
|
|
@@ -1330,7 +1470,7 @@ function createOpenAIExtension(config) {
|
|
|
1330
1470
|
`generate: failed to parse response JSON: ${detail}`
|
|
1331
1471
|
);
|
|
1332
1472
|
}
|
|
1333
|
-
const
|
|
1473
|
+
const result = {
|
|
1334
1474
|
data,
|
|
1335
1475
|
raw,
|
|
1336
1476
|
model: response.model,
|
|
@@ -1347,11 +1487,11 @@ function createOpenAIExtension(config) {
|
|
|
1347
1487
|
subsystem: "extension:openai",
|
|
1348
1488
|
duration,
|
|
1349
1489
|
model: response.model,
|
|
1350
|
-
usage:
|
|
1490
|
+
usage: result.usage,
|
|
1351
1491
|
request: apiMessages,
|
|
1352
1492
|
content: raw
|
|
1353
1493
|
});
|
|
1354
|
-
return
|
|
1494
|
+
return result;
|
|
1355
1495
|
} catch (error) {
|
|
1356
1496
|
const duration = Date.now() - startTime;
|
|
1357
1497
|
const rillError = error instanceof RuntimeError6 ? error : mapProviderError("OpenAI", error, detectOpenAIError);
|
|
@@ -1365,21 +1505,28 @@ function createOpenAIExtension(config) {
|
|
|
1365
1505
|
}
|
|
1366
1506
|
},
|
|
1367
1507
|
annotations: { description: "Generate structured output from OpenAI API" },
|
|
1368
|
-
returnType:
|
|
1369
|
-
|
|
1508
|
+
returnType: structureToTypeValue({
|
|
1509
|
+
kind: "dict",
|
|
1370
1510
|
fields: {
|
|
1371
|
-
data: { type: {
|
|
1372
|
-
raw: { type: {
|
|
1373
|
-
model: { type: {
|
|
1374
|
-
usage: { type: {
|
|
1375
|
-
stop_reason: { type: {
|
|
1376
|
-
id: { type: {
|
|
1511
|
+
data: { type: { kind: "any" } },
|
|
1512
|
+
raw: { type: { kind: "string" } },
|
|
1513
|
+
model: { type: { kind: "string" } },
|
|
1514
|
+
usage: { type: { kind: "dict", fields: { input: { type: { kind: "number" } }, output: { type: { kind: "number" } } } } },
|
|
1515
|
+
stop_reason: { type: { kind: "string" } },
|
|
1516
|
+
id: { type: { kind: "string" } }
|
|
1377
1517
|
}
|
|
1378
1518
|
})
|
|
1379
1519
|
}
|
|
1380
1520
|
};
|
|
1381
|
-
|
|
1382
|
-
|
|
1521
|
+
const callableDict = {
|
|
1522
|
+
message: toCallable(fnDict.message),
|
|
1523
|
+
messages: toCallable(fnDict.messages),
|
|
1524
|
+
embed: toCallable(fnDict.embed),
|
|
1525
|
+
embed_batch: toCallable(fnDict.embed_batch),
|
|
1526
|
+
tool_loop: toCallable(fnDict.tool_loop),
|
|
1527
|
+
generate: toCallable(fnDict.generate)
|
|
1528
|
+
};
|
|
1529
|
+
return { value: callableDict, dispose };
|
|
1383
1530
|
}
|
|
1384
1531
|
|
|
1385
1532
|
// src/index.ts
|