@rcrsr/rill-ext-openai 0.8.6 → 0.11.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 +8 -127
- package/dist/index.d.ts +2 -6
- package/dist/index.js +566 -167
- package/package.json +12 -11
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @rcrsr/rill-ext-openai
|
|
2
2
|
|
|
3
|
-
[rill](https://rill.run) extension for [OpenAI](https://platform.openai.com/docs) API integration. Provides `message`, `messages`, `embed`, `embed_batch`, and `
|
|
3
|
+
[rill](https://rill.run) extension for [OpenAI](https://platform.openai.com/docs) API integration. Provides `message`, `messages`, `embed`, `embed_batch`, `tool_loop`, and `generate` host functions. Compatible with any OpenAI-compatible server (LM Studio, Ollama, vLLM).
|
|
4
4
|
|
|
5
5
|
> **Experimental.** Breaking changes will occur before stabilization.
|
|
6
6
|
|
|
@@ -20,7 +20,7 @@ import { createOpenAIExtension } from '@rcrsr/rill-ext-openai';
|
|
|
20
20
|
|
|
21
21
|
const ext = createOpenAIExtension({
|
|
22
22
|
api_key: process.env.OPENAI_API_KEY!,
|
|
23
|
-
model: 'gpt-
|
|
23
|
+
model: 'gpt-4o',
|
|
24
24
|
});
|
|
25
25
|
const prefixed = prefixFunctions('openai', ext);
|
|
26
26
|
const { dispose, ...functions } = prefixed;
|
|
@@ -36,134 +36,15 @@ const result = await execute(parse(script), ctx);
|
|
|
36
36
|
dispose?.();
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
##
|
|
40
|
-
|
|
41
|
-
All functions return a dict with `content`, `model`, `usage`, `stop_reason`, `id`, and `messages`.
|
|
42
|
-
|
|
43
|
-
### openai::message(text, options?)
|
|
44
|
-
|
|
45
|
-
Send a single message to OpenAI.
|
|
46
|
-
|
|
47
|
-
```rill
|
|
48
|
-
openai::message("Analyze this code for security issues") => $response
|
|
49
|
-
$response.content -> log
|
|
50
|
-
$response.usage.output -> log
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### openai::messages(messages, options?)
|
|
54
|
-
|
|
55
|
-
Send a multi-turn conversation.
|
|
56
|
-
|
|
57
|
-
```rill
|
|
58
|
-
openai::messages([
|
|
59
|
-
[role: "user", content: "What is rill?"],
|
|
60
|
-
[role: "assistant", content: "A scripting language for AI agents."],
|
|
61
|
-
[role: "user", content: "Show me an example."]
|
|
62
|
-
]) => $response
|
|
63
|
-
$response.content -> log
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### openai::embed(text)
|
|
67
|
-
|
|
68
|
-
Generate an embedding vector for text. Requires `embed_model` in config.
|
|
69
|
-
|
|
70
|
-
```rill
|
|
71
|
-
openai::embed("Hello world") => $vector
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### openai::embed_batch(texts)
|
|
75
|
-
|
|
76
|
-
Generate embedding vectors for multiple texts in a single API call.
|
|
77
|
-
|
|
78
|
-
```rill
|
|
79
|
-
openai::embed_batch(["Hello", "World"]) => $vectors
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### openai::tool_loop(prompt, options)
|
|
83
|
-
|
|
84
|
-
Execute a tool-use loop where the model calls rill functions iteratively.
|
|
85
|
-
|
|
86
|
-
```rill
|
|
87
|
-
openai::tool_loop("Find the weather", [tools: $my_tools]) => $result
|
|
88
|
-
$result.content -> log
|
|
89
|
-
$result.turns -> log
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Configuration
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
const ext = createOpenAIExtension({
|
|
96
|
-
api_key: process.env.OPENAI_API_KEY!,
|
|
97
|
-
model: 'gpt-4-turbo',
|
|
98
|
-
temperature: 0.7,
|
|
99
|
-
base_url: 'http://localhost:1234/v1', // OpenAI-compatible server
|
|
100
|
-
max_tokens: 4096,
|
|
101
|
-
max_retries: 3,
|
|
102
|
-
timeout: 30000,
|
|
103
|
-
system: 'You are a helpful assistant.',
|
|
104
|
-
embed_model: 'text-embedding-3-small',
|
|
105
|
-
});
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
| Option | Type | Default | Description |
|
|
109
|
-
|--------|------|---------|-------------|
|
|
110
|
-
| `api_key` | string | required | OpenAI API key |
|
|
111
|
-
| `model` | string | required | Model identifier |
|
|
112
|
-
| `temperature` | number | undefined | Temperature (0.0-2.0) |
|
|
113
|
-
| `base_url` | string | undefined | Custom API endpoint URL |
|
|
114
|
-
| `max_tokens` | number | `4096` | Max tokens in response |
|
|
115
|
-
| `max_retries` | number | undefined | Max retry attempts |
|
|
116
|
-
| `timeout` | number | undefined | Request timeout in ms |
|
|
117
|
-
| `system` | string | undefined | Default system prompt |
|
|
118
|
-
| `embed_model` | string | undefined | Embedding model identifier |
|
|
119
|
-
|
|
120
|
-
## Result Shape
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
interface OpenAIResult {
|
|
124
|
-
content: string; // response text
|
|
125
|
-
model: string; // model used
|
|
126
|
-
usage: {
|
|
127
|
-
input: number; // prompt tokens
|
|
128
|
-
output: number; // completion tokens
|
|
129
|
-
};
|
|
130
|
-
stop_reason: string; // finish reason
|
|
131
|
-
id: string; // request ID
|
|
132
|
-
messages: Array<{ // full conversation history
|
|
133
|
-
role: string;
|
|
134
|
-
content: string;
|
|
135
|
-
}>;
|
|
136
|
-
}
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
## Lifecycle
|
|
140
|
-
|
|
141
|
-
Call `dispose()` on the extension to cancel pending requests:
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
const ext = createOpenAIExtension({ ... });
|
|
145
|
-
// ... use extension ...
|
|
146
|
-
await ext.dispose?.();
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
## Test Host
|
|
150
|
-
|
|
151
|
-
A runnable example at `examples/test-host.ts` wires up the extension with the rill runtime:
|
|
152
|
-
|
|
153
|
-
```bash
|
|
154
|
-
pnpm exec tsx examples/test-host.ts
|
|
155
|
-
pnpm exec tsx examples/test-host.ts -e 'openai::message("Tell me a joke") -> log'
|
|
156
|
-
pnpm exec tsx examples/test-host.ts script.rill
|
|
157
|
-
```
|
|
39
|
+
## Documentation
|
|
158
40
|
|
|
159
|
-
|
|
41
|
+
See [full documentation](docs/extension-llm-openai.md) for configuration, functions, error handling, events, and examples.
|
|
160
42
|
|
|
161
|
-
##
|
|
43
|
+
## Related
|
|
162
44
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
| [Host API Reference](https://github.com/rcrsr/rill/blob/main/docs/ref-host-api.md) | Runtime context and host functions |
|
|
45
|
+
- [rill](https://github.com/rcrsr/rill) — Core language runtime
|
|
46
|
+
- [Extensions Guide](https://github.com/rcrsr/rill/blob/main/docs/integration-extensions.md) — Extension contract and patterns
|
|
47
|
+
- [Host API Reference](https://github.com/rcrsr/rill/blob/main/docs/ref-host-api.md) — Runtime context and host functions
|
|
167
48
|
|
|
168
49
|
## License
|
|
169
50
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by dts-bundle-generator v9.5.1
|
|
2
2
|
|
|
3
|
-
import { ExtensionResult } from '@rcrsr/rill';
|
|
3
|
+
import { ExtensionConfigSchema, ExtensionResult } from '@rcrsr/rill';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Base configuration for LLM extensions
|
|
@@ -73,12 +73,8 @@ export type OpenAIExtensionConfig = LLMProviderConfig;
|
|
|
73
73
|
* ```
|
|
74
74
|
*/
|
|
75
75
|
export declare function createOpenAIExtension(config: OpenAIExtensionConfig): ExtensionResult;
|
|
76
|
-
/**
|
|
77
|
-
* @rcrsr/rill-ext-openai
|
|
78
|
-
*
|
|
79
|
-
* Extension for OpenAI API integration with rill scripts.
|
|
80
|
-
*/
|
|
81
76
|
export declare const VERSION = "0.0.1";
|
|
77
|
+
export declare const configSchema: ExtensionConfigSchema;
|
|
82
78
|
|
|
83
79
|
export {
|
|
84
80
|
LLMProviderConfig as LLMExtensionConfig,
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/factory.ts
|
|
2
2
|
import OpenAI from "openai";
|
|
3
3
|
import {
|
|
4
|
-
RuntimeError as
|
|
4
|
+
RuntimeError as RuntimeError6,
|
|
5
5
|
emitExtensionEvent,
|
|
6
6
|
createVector,
|
|
7
|
-
|
|
7
|
+
isDict as isDict2,
|
|
8
8
|
isVector
|
|
9
9
|
} from "@rcrsr/rill";
|
|
10
10
|
|
|
@@ -76,29 +76,152 @@ function mapProviderError(providerName, error, detect) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// ../../shared/ext-llm/dist/tool-loop.js
|
|
79
|
-
import { isCallable, isDict, RuntimeError as
|
|
79
|
+
import { invokeCallable, isCallable, isDict, isRuntimeCallable, RuntimeError as RuntimeError4 } from "@rcrsr/rill";
|
|
80
|
+
|
|
81
|
+
// ../../shared/ext-llm/dist/schema.js
|
|
82
|
+
import { RuntimeError as RuntimeError3 } from "@rcrsr/rill";
|
|
83
|
+
var RILL_TYPE_MAP = {
|
|
84
|
+
string: "string",
|
|
85
|
+
number: "number",
|
|
86
|
+
bool: "boolean",
|
|
87
|
+
list: "array",
|
|
88
|
+
dict: "object",
|
|
89
|
+
vector: "object",
|
|
90
|
+
shape: "object"
|
|
91
|
+
};
|
|
92
|
+
function mapRillType(rillType) {
|
|
93
|
+
const jsonType = RILL_TYPE_MAP[rillType];
|
|
94
|
+
if (jsonType === void 0) {
|
|
95
|
+
throw new RuntimeError3("RILL-R004", `unsupported type: ${rillType}`);
|
|
96
|
+
}
|
|
97
|
+
return jsonType;
|
|
98
|
+
}
|
|
99
|
+
function buildPropertyFromStructuralType(rillType) {
|
|
100
|
+
if (rillType.type === "closure" || rillType.type === "tuple") {
|
|
101
|
+
throw new RuntimeError3("RILL-R004", `unsupported type for JSON Schema: ${rillType.type}`);
|
|
102
|
+
}
|
|
103
|
+
if (rillType.type === "any") {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
if (rillType.type === "list") {
|
|
107
|
+
const property = { type: "array" };
|
|
108
|
+
if (rillType.element !== void 0) {
|
|
109
|
+
property.items = buildPropertyFromStructuralType(rillType.element);
|
|
110
|
+
}
|
|
111
|
+
return property;
|
|
112
|
+
}
|
|
113
|
+
if (rillType.type === "dict") {
|
|
114
|
+
return { type: "object" };
|
|
115
|
+
}
|
|
116
|
+
return { type: mapRillType(rillType.type) };
|
|
117
|
+
}
|
|
118
|
+
function buildJsonSchemaFromStructuralType(type, params) {
|
|
119
|
+
const properties = {};
|
|
120
|
+
const required = [];
|
|
121
|
+
if (type.type === "closure") {
|
|
122
|
+
const closureParams = type.params ?? [];
|
|
123
|
+
for (let i = 0; i < closureParams.length; i++) {
|
|
124
|
+
const [paramName, paramType] = closureParams[i];
|
|
125
|
+
const rillParam = params?.[i];
|
|
126
|
+
const property = buildPropertyFromStructuralType(paramType);
|
|
127
|
+
const description = rillParam?.annotations["description"];
|
|
128
|
+
if (typeof description === "string") {
|
|
129
|
+
property.description = description;
|
|
130
|
+
}
|
|
131
|
+
const enumAnnotation = rillParam?.annotations["enum"];
|
|
132
|
+
if (Array.isArray(enumAnnotation)) {
|
|
133
|
+
property.enum = enumAnnotation;
|
|
134
|
+
}
|
|
135
|
+
properties[paramName] = property;
|
|
136
|
+
if (rillParam === void 0 || rillParam.defaultValue === void 0) {
|
|
137
|
+
required.push(paramName);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { type: "object", properties, required, additionalProperties: false };
|
|
142
|
+
}
|
|
143
|
+
function buildJsonSchema(rillSchema) {
|
|
144
|
+
const properties = {};
|
|
145
|
+
const required = [];
|
|
146
|
+
for (const [key, value] of Object.entries(rillSchema)) {
|
|
147
|
+
if (typeof value === "string") {
|
|
148
|
+
properties[key] = buildProperty(value);
|
|
149
|
+
} else if (typeof value === "object" && value !== null) {
|
|
150
|
+
properties[key] = buildProperty(value);
|
|
151
|
+
} else {
|
|
152
|
+
throw new RuntimeError3("RILL-R004", `unsupported type: ${String(value)}`);
|
|
153
|
+
}
|
|
154
|
+
required.push(key);
|
|
155
|
+
}
|
|
156
|
+
return { type: "object", properties, required, additionalProperties: false };
|
|
157
|
+
}
|
|
158
|
+
function buildProperty(descriptor) {
|
|
159
|
+
if (typeof descriptor === "string") {
|
|
160
|
+
const jsonType2 = mapRillType(descriptor);
|
|
161
|
+
return { type: jsonType2 };
|
|
162
|
+
}
|
|
163
|
+
const rillType = descriptor["type"];
|
|
164
|
+
if (typeof rillType !== "string") {
|
|
165
|
+
throw new RuntimeError3("RILL-R004", `unsupported type: ${String(rillType)}`);
|
|
166
|
+
}
|
|
167
|
+
const jsonType = mapRillType(rillType);
|
|
168
|
+
const property = { type: jsonType };
|
|
169
|
+
const description = descriptor["description"];
|
|
170
|
+
if (typeof description === "string") {
|
|
171
|
+
property.description = description;
|
|
172
|
+
}
|
|
173
|
+
if ("enum" in descriptor) {
|
|
174
|
+
if (rillType !== "string") {
|
|
175
|
+
throw new RuntimeError3("RILL-R004", "enum is only valid for string type");
|
|
176
|
+
}
|
|
177
|
+
const enumValues = descriptor["enum"];
|
|
178
|
+
if (Array.isArray(enumValues)) {
|
|
179
|
+
property.enum = enumValues;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (rillType === "list" && "items" in descriptor) {
|
|
183
|
+
const items = descriptor["items"];
|
|
184
|
+
if (typeof items === "string") {
|
|
185
|
+
property.items = buildProperty(items);
|
|
186
|
+
} else if (typeof items === "object" && items !== null) {
|
|
187
|
+
property.items = buildProperty(items);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (rillType === "dict" && "properties" in descriptor) {
|
|
191
|
+
const nestedProps = descriptor["properties"];
|
|
192
|
+
if (typeof nestedProps === "object" && nestedProps !== null) {
|
|
193
|
+
const subSchema = buildJsonSchema(nestedProps);
|
|
194
|
+
property.properties = subSchema.properties;
|
|
195
|
+
property.required = subSchema.required;
|
|
196
|
+
property.additionalProperties = false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return property;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ../../shared/ext-llm/dist/tool-loop.js
|
|
80
203
|
async function executeToolCall(toolName, toolInput, tools, context) {
|
|
81
204
|
if (!isDict(tools)) {
|
|
82
|
-
throw new
|
|
205
|
+
throw new RuntimeError4("RILL-R004", "tool_loop: tools must be a dict of name \u2192 callable");
|
|
83
206
|
}
|
|
84
207
|
const toolsDict = tools;
|
|
85
208
|
const toolFn = toolsDict[toolName];
|
|
86
209
|
if (toolFn === void 0 || toolFn === null) {
|
|
87
|
-
throw new
|
|
210
|
+
throw new RuntimeError4("RILL-R004", `Unknown tool: ${toolName}`);
|
|
88
211
|
}
|
|
89
212
|
if (!isCallable(toolFn)) {
|
|
90
|
-
throw new
|
|
213
|
+
throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: tool must be callable`);
|
|
91
214
|
}
|
|
92
215
|
if (typeof toolInput !== "object" || toolInput === null) {
|
|
93
|
-
throw new
|
|
216
|
+
throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: input must be an object`);
|
|
94
217
|
}
|
|
95
218
|
const callable = toolFn;
|
|
96
|
-
if (callable.kind !== "runtime" && callable.kind !== "application") {
|
|
97
|
-
throw new
|
|
219
|
+
if (callable.kind !== "runtime" && callable.kind !== "application" && callable.kind !== "script") {
|
|
220
|
+
throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: tool must be application, runtime, or script callable`);
|
|
98
221
|
}
|
|
99
222
|
try {
|
|
100
223
|
let args;
|
|
101
|
-
if (callable.kind === "application" && callable.params) {
|
|
224
|
+
if ((callable.kind === "application" || callable.kind === "script") && callable.params && callable.params.length > 0) {
|
|
102
225
|
const params = callable.params;
|
|
103
226
|
const inputDict = toolInput;
|
|
104
227
|
args = params.map((param) => {
|
|
@@ -108,6 +231,12 @@ async function executeToolCall(toolName, toolInput, tools, context) {
|
|
|
108
231
|
} else {
|
|
109
232
|
args = [toolInput];
|
|
110
233
|
}
|
|
234
|
+
if (callable.kind === "script") {
|
|
235
|
+
if (!context) {
|
|
236
|
+
throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: script callable requires a runtime context`);
|
|
237
|
+
}
|
|
238
|
+
return await invokeCallable(callable, args, context);
|
|
239
|
+
}
|
|
111
240
|
const ctx = context ?? {
|
|
112
241
|
parent: void 0,
|
|
113
242
|
variables: /* @__PURE__ */ new Map(),
|
|
@@ -116,77 +245,122 @@ async function executeToolCall(toolName, toolInput, tools, context) {
|
|
|
116
245
|
const result = callable.fn(args, ctx);
|
|
117
246
|
return result instanceof Promise ? await result : result;
|
|
118
247
|
} catch (error) {
|
|
119
|
-
if (error instanceof
|
|
248
|
+
if (error instanceof RuntimeError4) {
|
|
120
249
|
throw error;
|
|
121
250
|
}
|
|
122
251
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
123
|
-
throw new
|
|
252
|
+
throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: ${message}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function sanitizeToolName(name) {
|
|
256
|
+
const match = name.match(/^[a-zA-Z0-9_-]*/);
|
|
257
|
+
const sanitized = match ? match[0] : "";
|
|
258
|
+
return sanitized.length > 0 ? sanitized : name;
|
|
259
|
+
}
|
|
260
|
+
function patchResponseToolCallNames(response, nameMap) {
|
|
261
|
+
if (!nameMap.size || !response || typeof response !== "object")
|
|
262
|
+
return;
|
|
263
|
+
const resp = response;
|
|
264
|
+
if (Array.isArray(resp["choices"])) {
|
|
265
|
+
for (const choice of resp["choices"]) {
|
|
266
|
+
const msg = choice?.["message"];
|
|
267
|
+
const tcs = msg?.["tool_calls"];
|
|
268
|
+
if (Array.isArray(tcs)) {
|
|
269
|
+
for (const tc of tcs) {
|
|
270
|
+
const fn = tc?.["function"];
|
|
271
|
+
if (fn && typeof fn["name"] === "string") {
|
|
272
|
+
const orig = fn["name"];
|
|
273
|
+
const san = nameMap.get(orig);
|
|
274
|
+
if (san !== void 0)
|
|
275
|
+
fn["name"] = san;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (Array.isArray(resp["content"])) {
|
|
282
|
+
for (const block of resp["content"]) {
|
|
283
|
+
const b = block;
|
|
284
|
+
if (b?.["type"] === "tool_use" && typeof b?.["name"] === "string") {
|
|
285
|
+
const orig = b["name"];
|
|
286
|
+
const san = nameMap.get(orig);
|
|
287
|
+
if (san !== void 0)
|
|
288
|
+
b["name"] = san;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (Array.isArray(resp["functionCalls"])) {
|
|
293
|
+
for (const fc of resp["functionCalls"]) {
|
|
294
|
+
const f = fc;
|
|
295
|
+
if (typeof f?.["name"] === "string") {
|
|
296
|
+
const orig = f["name"];
|
|
297
|
+
const san = nameMap.get(orig);
|
|
298
|
+
if (san !== void 0)
|
|
299
|
+
f["name"] = san;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (Array.isArray(resp["candidates"])) {
|
|
304
|
+
for (const cand of resp["candidates"]) {
|
|
305
|
+
const content = cand?.["content"];
|
|
306
|
+
const parts = content?.["parts"];
|
|
307
|
+
if (Array.isArray(parts)) {
|
|
308
|
+
for (const part of parts) {
|
|
309
|
+
const fc = part?.["functionCall"];
|
|
310
|
+
if (fc && typeof fc["name"] === "string") {
|
|
311
|
+
const orig = fc["name"];
|
|
312
|
+
const san = nameMap.get(orig);
|
|
313
|
+
if (san !== void 0)
|
|
314
|
+
fc["name"] = san;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
124
319
|
}
|
|
125
320
|
}
|
|
126
321
|
async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent, maxTurns = 10, context) {
|
|
127
322
|
if (tools === void 0) {
|
|
128
|
-
throw new
|
|
323
|
+
throw new RuntimeError4("RILL-R004", "tools parameter is required");
|
|
129
324
|
}
|
|
130
325
|
if (!isDict(tools)) {
|
|
131
|
-
throw new
|
|
326
|
+
throw new RuntimeError4("RILL-R004", "tool_loop: tools must be a dict of name \u2192 callable");
|
|
132
327
|
}
|
|
133
328
|
const toolsDict = tools;
|
|
134
329
|
const toolDescriptors = Object.entries(toolsDict).map(([name, fn]) => {
|
|
135
330
|
const fnValue = fn;
|
|
331
|
+
if (isRuntimeCallable(fnValue)) {
|
|
332
|
+
throw new RuntimeError4("RILL-R004", `tool_loop: builtin "${name}" cannot be used as a tool \u2014 wrap in a closure`);
|
|
333
|
+
}
|
|
136
334
|
if (!isCallable(fnValue)) {
|
|
137
|
-
throw new
|
|
335
|
+
throw new RuntimeError4("RILL-R004", `tool_loop: tool "${name}" is not a callable`);
|
|
138
336
|
}
|
|
139
337
|
const callable = fnValue;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
jsonSchemaType = "object";
|
|
162
|
-
break;
|
|
163
|
-
case null:
|
|
164
|
-
jsonSchemaType = "string";
|
|
165
|
-
break;
|
|
166
|
-
default:
|
|
167
|
-
jsonSchemaType = "string";
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
const property = {
|
|
171
|
-
type: jsonSchemaType
|
|
172
|
-
};
|
|
173
|
-
if (param.description) {
|
|
174
|
-
property["description"] = param.description;
|
|
175
|
-
}
|
|
176
|
-
properties[param.name] = property;
|
|
177
|
-
if (param.defaultValue === null) {
|
|
178
|
-
required.push(param.name);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
338
|
+
let description;
|
|
339
|
+
if (callable.kind === "script") {
|
|
340
|
+
description = callable.annotations["description"] ?? "";
|
|
341
|
+
} else {
|
|
342
|
+
description = callable.description ?? "";
|
|
343
|
+
}
|
|
344
|
+
let inputSchema;
|
|
345
|
+
const params = callable.kind === "application" ? callable.params ?? [] : callable.kind === "script" ? callable.params : [];
|
|
346
|
+
if (params.length > 0) {
|
|
347
|
+
const closureType = {
|
|
348
|
+
type: "closure",
|
|
349
|
+
params: params.map((p2) => [p2.name, p2.type ?? { type: "any" }])
|
|
350
|
+
};
|
|
351
|
+
const builtSchema = buildJsonSchemaFromStructuralType(closureType, [...params]);
|
|
352
|
+
inputSchema = {
|
|
353
|
+
type: "object",
|
|
354
|
+
properties: builtSchema.properties,
|
|
355
|
+
required: builtSchema.required
|
|
356
|
+
};
|
|
357
|
+
} else {
|
|
358
|
+
inputSchema = { type: "object", properties: {}, required: [] };
|
|
181
359
|
}
|
|
182
360
|
return {
|
|
183
361
|
name,
|
|
184
362
|
description,
|
|
185
|
-
input_schema:
|
|
186
|
-
type: "object",
|
|
187
|
-
properties,
|
|
188
|
-
required
|
|
189
|
-
}
|
|
363
|
+
input_schema: inputSchema
|
|
190
364
|
};
|
|
191
365
|
});
|
|
192
366
|
const providerTools = callbacks.buildTools(toolDescriptors);
|
|
@@ -203,7 +377,7 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
|
|
|
203
377
|
response = await callbacks.callAPI(currentMessages, providerTools);
|
|
204
378
|
} catch (error) {
|
|
205
379
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
206
|
-
throw new
|
|
380
|
+
throw new RuntimeError4("RILL-R004", `Provider API error: ${message}`, void 0, { cause: error });
|
|
207
381
|
}
|
|
208
382
|
if (typeof response === "object" && response !== null && "usage" in response) {
|
|
209
383
|
const usage = response["usage"];
|
|
@@ -215,7 +389,17 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
|
|
|
215
389
|
totalOutputTokens += outputTokens;
|
|
216
390
|
}
|
|
217
391
|
}
|
|
218
|
-
const
|
|
392
|
+
const rawToolCalls = callbacks.extractToolCalls(response);
|
|
393
|
+
const nameMap = /* @__PURE__ */ new Map();
|
|
394
|
+
const toolCalls = rawToolCalls?.map((tc) => {
|
|
395
|
+
const sanitized = sanitizeToolName(tc.name);
|
|
396
|
+
if (sanitized !== tc.name)
|
|
397
|
+
nameMap.set(tc.name, sanitized);
|
|
398
|
+
return sanitized !== tc.name ? { ...tc, name: sanitized } : tc;
|
|
399
|
+
}) ?? null;
|
|
400
|
+
if (nameMap.size > 0) {
|
|
401
|
+
patchResponseToolCallNames(response, nameMap);
|
|
402
|
+
}
|
|
219
403
|
if (toolCalls === null || toolCalls.length === 0) {
|
|
220
404
|
return {
|
|
221
405
|
response,
|
|
@@ -240,7 +424,7 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
|
|
|
240
424
|
const duration = Date.now() - toolStartTime;
|
|
241
425
|
consecutiveErrors++;
|
|
242
426
|
let originalError;
|
|
243
|
-
if (error instanceof
|
|
427
|
+
if (error instanceof RuntimeError4) {
|
|
244
428
|
const prefix = `Invalid tool input for ${name}: `;
|
|
245
429
|
if (error.message.startsWith(prefix)) {
|
|
246
430
|
originalError = error.message.slice(prefix.length);
|
|
@@ -265,12 +449,20 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
|
|
|
265
449
|
duration
|
|
266
450
|
});
|
|
267
451
|
if (consecutiveErrors >= maxErrors) {
|
|
268
|
-
throw new
|
|
452
|
+
throw new RuntimeError4("RILL-R004", `Tool execution failed: ${maxErrors} consecutive errors (last: ${name}: ${originalError})`);
|
|
269
453
|
}
|
|
270
454
|
}
|
|
271
455
|
}
|
|
456
|
+
const assistantMessage = callbacks.formatAssistantMessage(response);
|
|
457
|
+
if (assistantMessage != null) {
|
|
458
|
+
currentMessages.push(assistantMessage);
|
|
459
|
+
}
|
|
272
460
|
const toolResultMessage = callbacks.formatToolResult(toolResults);
|
|
273
|
-
|
|
461
|
+
if (Array.isArray(toolResultMessage)) {
|
|
462
|
+
currentMessages.push(...toolResultMessage);
|
|
463
|
+
} else {
|
|
464
|
+
currentMessages.push(toolResultMessage);
|
|
465
|
+
}
|
|
274
466
|
}
|
|
275
467
|
return {
|
|
276
468
|
response: null,
|
|
@@ -280,8 +472,128 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
|
|
|
280
472
|
};
|
|
281
473
|
}
|
|
282
474
|
|
|
475
|
+
// ../../shared/ext-param/dist/param.js
|
|
476
|
+
import { RuntimeError as RuntimeError5 } from "@rcrsr/rill";
|
|
477
|
+
function validateParamName(name) {
|
|
478
|
+
if (name === "") {
|
|
479
|
+
throw new RuntimeError5("RILL-R001", "param name must not be empty");
|
|
480
|
+
}
|
|
481
|
+
if (/\s/.test(name)) {
|
|
482
|
+
throw new RuntimeError5("RILL-R001", "param name must be a valid identifier");
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function buildAnnotations(desc) {
|
|
486
|
+
if (desc !== void 0) {
|
|
487
|
+
return { description: desc };
|
|
488
|
+
}
|
|
489
|
+
return {};
|
|
490
|
+
}
|
|
491
|
+
var p = {
|
|
492
|
+
/**
|
|
493
|
+
* IR-1: Creates a string parameter descriptor.
|
|
494
|
+
*
|
|
495
|
+
* @param name - Parameter name (must be a valid identifier)
|
|
496
|
+
* @param desc - Optional description
|
|
497
|
+
* @returns RillParam with type 'string'
|
|
498
|
+
*/
|
|
499
|
+
str(name, desc) {
|
|
500
|
+
validateParamName(name);
|
|
501
|
+
return {
|
|
502
|
+
name,
|
|
503
|
+
type: { type: "string" },
|
|
504
|
+
defaultValue: void 0,
|
|
505
|
+
annotations: buildAnnotations(desc)
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
/**
|
|
509
|
+
* IR-2: Creates a number parameter descriptor.
|
|
510
|
+
*
|
|
511
|
+
* @param name - Parameter name (must be a valid identifier)
|
|
512
|
+
* @param desc - Optional description
|
|
513
|
+
* @param def - Optional default value
|
|
514
|
+
* @returns RillParam with type 'number'
|
|
515
|
+
*/
|
|
516
|
+
num(name, desc, def) {
|
|
517
|
+
validateParamName(name);
|
|
518
|
+
return {
|
|
519
|
+
name,
|
|
520
|
+
type: { type: "number" },
|
|
521
|
+
defaultValue: def,
|
|
522
|
+
annotations: buildAnnotations(desc)
|
|
523
|
+
};
|
|
524
|
+
},
|
|
525
|
+
/**
|
|
526
|
+
* IR-3: Creates a boolean parameter descriptor.
|
|
527
|
+
*
|
|
528
|
+
* @param name - Parameter name (must be a valid identifier)
|
|
529
|
+
* @param desc - Optional description
|
|
530
|
+
* @param def - Optional default value
|
|
531
|
+
* @returns RillParam with type 'bool'
|
|
532
|
+
*/
|
|
533
|
+
bool(name, desc, def) {
|
|
534
|
+
validateParamName(name);
|
|
535
|
+
return {
|
|
536
|
+
name,
|
|
537
|
+
type: { type: "bool" },
|
|
538
|
+
defaultValue: def,
|
|
539
|
+
annotations: buildAnnotations(desc)
|
|
540
|
+
};
|
|
541
|
+
},
|
|
542
|
+
/**
|
|
543
|
+
* IR-4: Creates a dict parameter descriptor.
|
|
544
|
+
*
|
|
545
|
+
* @param name - Parameter name (must be a valid identifier)
|
|
546
|
+
* @param desc - Optional description
|
|
547
|
+
* @param def - Optional default value
|
|
548
|
+
* @returns RillParam with type 'dict'
|
|
549
|
+
*/
|
|
550
|
+
dict(name, desc, def) {
|
|
551
|
+
validateParamName(name);
|
|
552
|
+
return {
|
|
553
|
+
name,
|
|
554
|
+
type: { type: "dict" },
|
|
555
|
+
defaultValue: def,
|
|
556
|
+
annotations: buildAnnotations(desc)
|
|
557
|
+
};
|
|
558
|
+
},
|
|
559
|
+
/**
|
|
560
|
+
* IR-5: Creates a list parameter descriptor.
|
|
561
|
+
*
|
|
562
|
+
* @param name - Parameter name (must be a valid identifier)
|
|
563
|
+
* @param itemType - Optional element type; omitted when not provided
|
|
564
|
+
* @param desc - Optional description
|
|
565
|
+
* @returns RillParam with type 'list' (with element if itemType provided)
|
|
566
|
+
*/
|
|
567
|
+
list(name, itemType, desc) {
|
|
568
|
+
validateParamName(name);
|
|
569
|
+
const type = itemType !== void 0 ? { type: "list", element: itemType } : { type: "list" };
|
|
570
|
+
return {
|
|
571
|
+
name,
|
|
572
|
+
type,
|
|
573
|
+
defaultValue: void 0,
|
|
574
|
+
annotations: buildAnnotations(desc)
|
|
575
|
+
};
|
|
576
|
+
},
|
|
577
|
+
/**
|
|
578
|
+
* IR-6: Creates a callable parameter descriptor.
|
|
579
|
+
*
|
|
580
|
+
* @param name - Parameter name (must be a valid identifier)
|
|
581
|
+
* @param desc - Optional description
|
|
582
|
+
* @returns RillParam with type 'closure'
|
|
583
|
+
*/
|
|
584
|
+
callable(name, desc) {
|
|
585
|
+
validateParamName(name);
|
|
586
|
+
return {
|
|
587
|
+
name,
|
|
588
|
+
type: { type: "closure" },
|
|
589
|
+
defaultValue: void 0,
|
|
590
|
+
annotations: buildAnnotations(desc)
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
283
595
|
// src/factory.ts
|
|
284
|
-
var
|
|
596
|
+
var DEFAULT_MAX_COMPLETION_TOKENS = 4096;
|
|
285
597
|
var detectOpenAIError = (error) => {
|
|
286
598
|
if (error instanceof OpenAI.APIError) {
|
|
287
599
|
return {
|
|
@@ -303,7 +615,7 @@ function createOpenAIExtension(config) {
|
|
|
303
615
|
});
|
|
304
616
|
const factoryModel = config.model;
|
|
305
617
|
const factoryTemperature = config.temperature;
|
|
306
|
-
const factoryMaxTokens = config.max_tokens ??
|
|
618
|
+
const factoryMaxTokens = config.max_tokens ?? DEFAULT_MAX_COMPLETION_TOKENS;
|
|
307
619
|
const factorySystem = config.system;
|
|
308
620
|
const factoryEmbedModel = config.embed_model;
|
|
309
621
|
void factoryEmbedModel;
|
|
@@ -328,8 +640,8 @@ function createOpenAIExtension(config) {
|
|
|
328
640
|
// IR-4: openai::message
|
|
329
641
|
message: {
|
|
330
642
|
params: [
|
|
331
|
-
|
|
332
|
-
|
|
643
|
+
p.str("text"),
|
|
644
|
+
p.dict("options", void 0, {})
|
|
333
645
|
],
|
|
334
646
|
fn: async (args, ctx) => {
|
|
335
647
|
const startTime = Date.now();
|
|
@@ -337,7 +649,7 @@ function createOpenAIExtension(config) {
|
|
|
337
649
|
const text = args[0];
|
|
338
650
|
const options = args[1] ?? {};
|
|
339
651
|
if (text.trim().length === 0) {
|
|
340
|
-
throw new
|
|
652
|
+
throw new RuntimeError6("RILL-R004", "prompt text cannot be empty");
|
|
341
653
|
}
|
|
342
654
|
const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
|
|
343
655
|
const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
|
|
@@ -354,7 +666,7 @@ function createOpenAIExtension(config) {
|
|
|
354
666
|
});
|
|
355
667
|
const apiParams = {
|
|
356
668
|
model: factoryModel,
|
|
357
|
-
|
|
669
|
+
max_completion_tokens: maxTokens,
|
|
358
670
|
messages: apiMessages
|
|
359
671
|
};
|
|
360
672
|
if (factoryTemperature !== void 0) {
|
|
@@ -383,7 +695,9 @@ function createOpenAIExtension(config) {
|
|
|
383
695
|
subsystem: "extension:openai",
|
|
384
696
|
duration,
|
|
385
697
|
model: response.model,
|
|
386
|
-
usage: result2.usage
|
|
698
|
+
usage: result2.usage,
|
|
699
|
+
request: apiMessages,
|
|
700
|
+
content
|
|
387
701
|
});
|
|
388
702
|
return result2;
|
|
389
703
|
} catch (error) {
|
|
@@ -403,13 +717,13 @@ function createOpenAIExtension(config) {
|
|
|
403
717
|
}
|
|
404
718
|
},
|
|
405
719
|
description: "Send single message to OpenAI API",
|
|
406
|
-
returnType: "dict"
|
|
720
|
+
returnType: { type: "dict" }
|
|
407
721
|
},
|
|
408
722
|
// IR-5: openai::messages
|
|
409
723
|
messages: {
|
|
410
724
|
params: [
|
|
411
|
-
|
|
412
|
-
|
|
725
|
+
p.list("messages"),
|
|
726
|
+
p.dict("options", void 0, {})
|
|
413
727
|
],
|
|
414
728
|
fn: async (args, ctx) => {
|
|
415
729
|
const startTime = Date.now();
|
|
@@ -417,7 +731,7 @@ function createOpenAIExtension(config) {
|
|
|
417
731
|
const messages = args[0];
|
|
418
732
|
const options = args[1] ?? {};
|
|
419
733
|
if (messages.length === 0) {
|
|
420
|
-
throw new
|
|
734
|
+
throw new RuntimeError6(
|
|
421
735
|
"RILL-R004",
|
|
422
736
|
"messages list cannot be empty"
|
|
423
737
|
);
|
|
@@ -434,18 +748,18 @@ function createOpenAIExtension(config) {
|
|
|
434
748
|
for (let i = 0; i < messages.length; i++) {
|
|
435
749
|
const msg = messages[i];
|
|
436
750
|
if (!msg || typeof msg !== "object" || !("role" in msg)) {
|
|
437
|
-
throw new
|
|
751
|
+
throw new RuntimeError6(
|
|
438
752
|
"RILL-R004",
|
|
439
753
|
"message missing required 'role' field"
|
|
440
754
|
);
|
|
441
755
|
}
|
|
442
756
|
const role = msg["role"];
|
|
443
757
|
if (role !== "user" && role !== "assistant" && role !== "tool") {
|
|
444
|
-
throw new
|
|
758
|
+
throw new RuntimeError6("RILL-R004", `invalid role '${role}'`);
|
|
445
759
|
}
|
|
446
760
|
if (role === "user" || role === "tool") {
|
|
447
761
|
if (!("content" in msg) || typeof msg["content"] !== "string") {
|
|
448
|
-
throw new
|
|
762
|
+
throw new RuntimeError6(
|
|
449
763
|
"RILL-R004",
|
|
450
764
|
`${role} message requires 'content'`
|
|
451
765
|
);
|
|
@@ -458,7 +772,7 @@ function createOpenAIExtension(config) {
|
|
|
458
772
|
const hasContent = "content" in msg && msg["content"];
|
|
459
773
|
const hasToolCalls = "tool_calls" in msg && msg["tool_calls"];
|
|
460
774
|
if (!hasContent && !hasToolCalls) {
|
|
461
|
-
throw new
|
|
775
|
+
throw new RuntimeError6(
|
|
462
776
|
"RILL-R004",
|
|
463
777
|
"assistant message requires 'content' or 'tool_calls'"
|
|
464
778
|
);
|
|
@@ -473,7 +787,7 @@ function createOpenAIExtension(config) {
|
|
|
473
787
|
}
|
|
474
788
|
const apiParams = {
|
|
475
789
|
model: factoryModel,
|
|
476
|
-
|
|
790
|
+
max_completion_tokens: maxTokens,
|
|
477
791
|
messages: apiMessages
|
|
478
792
|
};
|
|
479
793
|
if (factoryTemperature !== void 0) {
|
|
@@ -507,7 +821,9 @@ function createOpenAIExtension(config) {
|
|
|
507
821
|
subsystem: "extension:openai",
|
|
508
822
|
duration,
|
|
509
823
|
model: response.model,
|
|
510
|
-
usage: result2.usage
|
|
824
|
+
usage: result2.usage,
|
|
825
|
+
request: apiMessages,
|
|
826
|
+
content
|
|
511
827
|
});
|
|
512
828
|
return result2;
|
|
513
829
|
} catch (error) {
|
|
@@ -527,11 +843,11 @@ function createOpenAIExtension(config) {
|
|
|
527
843
|
}
|
|
528
844
|
},
|
|
529
845
|
description: "Send multi-turn conversation to OpenAI API",
|
|
530
|
-
returnType: "dict"
|
|
846
|
+
returnType: { type: "dict" }
|
|
531
847
|
},
|
|
532
848
|
// IR-6: openai::embed
|
|
533
849
|
embed: {
|
|
534
|
-
params: [
|
|
850
|
+
params: [p.str("text")],
|
|
535
851
|
fn: async (args, ctx) => {
|
|
536
852
|
const startTime = Date.now();
|
|
537
853
|
try {
|
|
@@ -545,7 +861,7 @@ function createOpenAIExtension(config) {
|
|
|
545
861
|
});
|
|
546
862
|
const embeddingData = response.data[0]?.embedding;
|
|
547
863
|
if (!embeddingData || embeddingData.length === 0) {
|
|
548
|
-
throw new
|
|
864
|
+
throw new RuntimeError6(
|
|
549
865
|
"RILL-R004",
|
|
550
866
|
"OpenAI: empty embedding returned"
|
|
551
867
|
);
|
|
@@ -578,11 +894,11 @@ function createOpenAIExtension(config) {
|
|
|
578
894
|
}
|
|
579
895
|
},
|
|
580
896
|
description: "Generate embedding vector for text",
|
|
581
|
-
returnType: "vector"
|
|
897
|
+
returnType: { type: "vector" }
|
|
582
898
|
},
|
|
583
899
|
// IR-7: openai::embed_batch
|
|
584
900
|
embed_batch: {
|
|
585
|
-
params: [
|
|
901
|
+
params: [p.list("texts")],
|
|
586
902
|
fn: async (args, ctx) => {
|
|
587
903
|
const startTime = Date.now();
|
|
588
904
|
try {
|
|
@@ -601,7 +917,7 @@ function createOpenAIExtension(config) {
|
|
|
601
917
|
for (const embeddingItem of response.data) {
|
|
602
918
|
const embeddingData = embeddingItem.embedding;
|
|
603
919
|
if (!embeddingData || embeddingData.length === 0) {
|
|
604
|
-
throw new
|
|
920
|
+
throw new RuntimeError6(
|
|
605
921
|
"RILL-R004",
|
|
606
922
|
"OpenAI: empty embedding returned"
|
|
607
923
|
);
|
|
@@ -639,13 +955,13 @@ function createOpenAIExtension(config) {
|
|
|
639
955
|
}
|
|
640
956
|
},
|
|
641
957
|
description: "Generate embedding vectors for multiple texts",
|
|
642
|
-
returnType: "list"
|
|
958
|
+
returnType: { type: "list" }
|
|
643
959
|
},
|
|
644
960
|
// IR-8: openai::tool_loop
|
|
645
961
|
tool_loop: {
|
|
646
962
|
params: [
|
|
647
|
-
|
|
648
|
-
|
|
963
|
+
p.str("prompt"),
|
|
964
|
+
p.dict("options", void 0, {})
|
|
649
965
|
],
|
|
650
966
|
fn: async (args, ctx) => {
|
|
651
967
|
const startTime = Date.now();
|
|
@@ -653,79 +969,14 @@ function createOpenAIExtension(config) {
|
|
|
653
969
|
const prompt = args[0];
|
|
654
970
|
const options = args[1] ?? {};
|
|
655
971
|
if (prompt.trim().length === 0) {
|
|
656
|
-
throw new
|
|
972
|
+
throw new RuntimeError6("RILL-R004", "prompt text cannot be empty");
|
|
657
973
|
}
|
|
658
|
-
if (!("tools" in options) || !
|
|
659
|
-
throw new
|
|
974
|
+
if (!("tools" in options) || !isDict2(options["tools"])) {
|
|
975
|
+
throw new RuntimeError6(
|
|
660
976
|
"RILL-R004",
|
|
661
977
|
"tool_loop requires 'tools' option"
|
|
662
978
|
);
|
|
663
979
|
}
|
|
664
|
-
const toolDescriptors = options["tools"];
|
|
665
|
-
const toolsDict = {};
|
|
666
|
-
for (const descriptor of toolDescriptors) {
|
|
667
|
-
const name = typeof descriptor["name"] === "string" ? descriptor["name"] : null;
|
|
668
|
-
if (!name) {
|
|
669
|
-
throw new RuntimeError4(
|
|
670
|
-
"RILL-R004",
|
|
671
|
-
"tool descriptor missing name"
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
const toolFnValue = descriptor["fn"];
|
|
675
|
-
if (!toolFnValue) {
|
|
676
|
-
throw new RuntimeError4(
|
|
677
|
-
"RILL-R004",
|
|
678
|
-
`tool '${name}' missing fn property`
|
|
679
|
-
);
|
|
680
|
-
}
|
|
681
|
-
if (!isCallable2(toolFnValue)) {
|
|
682
|
-
throw new RuntimeError4(
|
|
683
|
-
"RILL-R004",
|
|
684
|
-
`tool '${name}' fn must be callable`
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
const paramsObj = descriptor["params"];
|
|
688
|
-
const description = typeof descriptor["description"] === "string" ? descriptor["description"] : "";
|
|
689
|
-
let enhancedCallable = toolFnValue;
|
|
690
|
-
if (paramsObj && typeof paramsObj === "object" && !Array.isArray(paramsObj)) {
|
|
691
|
-
const params = Object.entries(
|
|
692
|
-
paramsObj
|
|
693
|
-
).map(([paramName, paramMeta]) => {
|
|
694
|
-
const meta = paramMeta;
|
|
695
|
-
const typeStr = typeof meta["type"] === "string" ? meta["type"] : null;
|
|
696
|
-
let typeName = null;
|
|
697
|
-
if (typeStr === "string") typeName = "string";
|
|
698
|
-
else if (typeStr === "number") typeName = "number";
|
|
699
|
-
else if (typeStr === "bool" || typeStr === "boolean")
|
|
700
|
-
typeName = "bool";
|
|
701
|
-
else if (typeStr === "list" || typeStr === "array")
|
|
702
|
-
typeName = "list";
|
|
703
|
-
else if (typeStr === "dict" || typeStr === "object")
|
|
704
|
-
typeName = "dict";
|
|
705
|
-
else if (typeStr === "vector") typeName = "vector";
|
|
706
|
-
const param = {
|
|
707
|
-
name: paramName,
|
|
708
|
-
typeName,
|
|
709
|
-
defaultValue: null,
|
|
710
|
-
annotations: {}
|
|
711
|
-
};
|
|
712
|
-
if (typeof meta["description"] === "string") {
|
|
713
|
-
param.description = meta["description"];
|
|
714
|
-
}
|
|
715
|
-
return param;
|
|
716
|
-
});
|
|
717
|
-
const baseCallable = toolFnValue;
|
|
718
|
-
enhancedCallable = {
|
|
719
|
-
__type: "callable",
|
|
720
|
-
kind: "application",
|
|
721
|
-
params,
|
|
722
|
-
fn: baseCallable.fn,
|
|
723
|
-
description,
|
|
724
|
-
isProperty: baseCallable.isProperty ?? false
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
toolsDict[name] = enhancedCallable;
|
|
728
|
-
}
|
|
729
980
|
const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
|
|
730
981
|
const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
|
|
731
982
|
const maxErrors = typeof options["max_errors"] === "number" ? options["max_errors"] : 3;
|
|
@@ -741,17 +992,17 @@ function createOpenAIExtension(config) {
|
|
|
741
992
|
const prependedMessages = options["messages"];
|
|
742
993
|
for (const msg of prependedMessages) {
|
|
743
994
|
if (!msg || typeof msg !== "object" || !("role" in msg)) {
|
|
744
|
-
throw new
|
|
995
|
+
throw new RuntimeError6(
|
|
745
996
|
"RILL-R004",
|
|
746
997
|
"message missing required 'role' field"
|
|
747
998
|
);
|
|
748
999
|
}
|
|
749
1000
|
const role = msg["role"];
|
|
750
1001
|
if (role !== "user" && role !== "assistant") {
|
|
751
|
-
throw new
|
|
1002
|
+
throw new RuntimeError6("RILL-R004", `invalid role '${role}'`);
|
|
752
1003
|
}
|
|
753
1004
|
if (!("content" in msg) || typeof msg["content"] !== "string") {
|
|
754
|
-
throw new
|
|
1005
|
+
throw new RuntimeError6(
|
|
755
1006
|
"RILL-R004",
|
|
756
1007
|
`${role} message requires 'content'`
|
|
757
1008
|
);
|
|
@@ -782,7 +1033,7 @@ function createOpenAIExtension(config) {
|
|
|
782
1033
|
callAPI: async (msgs, tools) => {
|
|
783
1034
|
const apiParams = {
|
|
784
1035
|
model: factoryModel,
|
|
785
|
-
|
|
1036
|
+
max_completion_tokens: maxTokens,
|
|
786
1037
|
messages: msgs,
|
|
787
1038
|
tools,
|
|
788
1039
|
tool_choice: "auto"
|
|
@@ -839,6 +1090,21 @@ function createOpenAIExtension(config) {
|
|
|
839
1090
|
};
|
|
840
1091
|
});
|
|
841
1092
|
},
|
|
1093
|
+
// Extract assistant message (with tool_calls) from OpenAI response
|
|
1094
|
+
formatAssistantMessage: (response2) => {
|
|
1095
|
+
if (!response2 || typeof response2 !== "object" || !("choices" in response2)) {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
const choices = response2.choices;
|
|
1099
|
+
if (!Array.isArray(choices) || choices.length === 0) {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
const choice = choices[0];
|
|
1103
|
+
if (!choice || typeof choice !== "object" || !("message" in choice)) {
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
return choice.message;
|
|
1107
|
+
},
|
|
842
1108
|
// Format tool results into OpenAI message format
|
|
843
1109
|
formatToolResult: (toolResults) => {
|
|
844
1110
|
return toolResults.map((tr) => ({
|
|
@@ -850,7 +1116,7 @@ function createOpenAIExtension(config) {
|
|
|
850
1116
|
};
|
|
851
1117
|
const loopResult = await executeToolLoop(
|
|
852
1118
|
messages,
|
|
853
|
-
|
|
1119
|
+
options["tools"],
|
|
854
1120
|
maxErrors,
|
|
855
1121
|
callbacks,
|
|
856
1122
|
(event, data) => {
|
|
@@ -908,7 +1174,9 @@ function createOpenAIExtension(config) {
|
|
|
908
1174
|
subsystem: "extension:openai",
|
|
909
1175
|
turns: loopResult.turns,
|
|
910
1176
|
total_duration: duration,
|
|
911
|
-
usage: result2.usage
|
|
1177
|
+
usage: result2.usage,
|
|
1178
|
+
request: messages,
|
|
1179
|
+
content
|
|
912
1180
|
});
|
|
913
1181
|
return result2;
|
|
914
1182
|
} catch (error) {
|
|
@@ -928,7 +1196,126 @@ function createOpenAIExtension(config) {
|
|
|
928
1196
|
}
|
|
929
1197
|
},
|
|
930
1198
|
description: "Execute tool-use loop with OpenAI API",
|
|
931
|
-
returnType: "dict"
|
|
1199
|
+
returnType: { type: "dict" }
|
|
1200
|
+
},
|
|
1201
|
+
// IR-3: openai::generate
|
|
1202
|
+
generate: {
|
|
1203
|
+
params: [
|
|
1204
|
+
p.str("prompt"),
|
|
1205
|
+
p.dict("options")
|
|
1206
|
+
],
|
|
1207
|
+
fn: async (args, ctx) => {
|
|
1208
|
+
const startTime = Date.now();
|
|
1209
|
+
try {
|
|
1210
|
+
const prompt = args[0];
|
|
1211
|
+
const options = args[1] ?? {};
|
|
1212
|
+
if (!("schema" in options) || options["schema"] === null || options["schema"] === void 0) {
|
|
1213
|
+
throw new RuntimeError6(
|
|
1214
|
+
"RILL-R004",
|
|
1215
|
+
"generate requires 'schema' option"
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
const rillSchema = options["schema"];
|
|
1219
|
+
const jsonSchema = buildJsonSchema(rillSchema);
|
|
1220
|
+
const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
|
|
1221
|
+
const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
|
|
1222
|
+
const apiMessages = [];
|
|
1223
|
+
if (system !== void 0) {
|
|
1224
|
+
apiMessages.push({
|
|
1225
|
+
role: "system",
|
|
1226
|
+
content: system
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
if ("messages" in options && Array.isArray(options["messages"])) {
|
|
1230
|
+
const prependedMessages = options["messages"];
|
|
1231
|
+
for (const msg of prependedMessages) {
|
|
1232
|
+
if (!msg || typeof msg !== "object" || !("role" in msg)) {
|
|
1233
|
+
throw new RuntimeError6(
|
|
1234
|
+
"RILL-R004",
|
|
1235
|
+
"message missing required 'role' field"
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
const role = msg["role"];
|
|
1239
|
+
if (role !== "user" && role !== "assistant") {
|
|
1240
|
+
throw new RuntimeError6("RILL-R004", `invalid role '${role}'`);
|
|
1241
|
+
}
|
|
1242
|
+
if (!("content" in msg) || typeof msg["content"] !== "string") {
|
|
1243
|
+
throw new RuntimeError6(
|
|
1244
|
+
"RILL-R004",
|
|
1245
|
+
`${role} message requires 'content'`
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
apiMessages.push({
|
|
1249
|
+
role,
|
|
1250
|
+
content: msg["content"]
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
apiMessages.push({ role: "user", content: prompt });
|
|
1255
|
+
const apiParams = {
|
|
1256
|
+
model: factoryModel,
|
|
1257
|
+
max_completion_tokens: maxTokens,
|
|
1258
|
+
messages: apiMessages,
|
|
1259
|
+
response_format: {
|
|
1260
|
+
type: "json_schema",
|
|
1261
|
+
json_schema: {
|
|
1262
|
+
name: "output",
|
|
1263
|
+
schema: jsonSchema,
|
|
1264
|
+
strict: true
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
if (factoryTemperature !== void 0) {
|
|
1269
|
+
apiParams.temperature = factoryTemperature;
|
|
1270
|
+
}
|
|
1271
|
+
const response = await client.chat.completions.create(apiParams);
|
|
1272
|
+
const raw = response.choices[0]?.message?.content ?? "";
|
|
1273
|
+
let data;
|
|
1274
|
+
try {
|
|
1275
|
+
data = JSON.parse(raw);
|
|
1276
|
+
} catch (parseError) {
|
|
1277
|
+
const detail = parseError instanceof Error ? parseError.message : String(parseError);
|
|
1278
|
+
throw new RuntimeError6(
|
|
1279
|
+
"RILL-R004",
|
|
1280
|
+
`generate: failed to parse response JSON: ${detail}`
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
const result2 = {
|
|
1284
|
+
data,
|
|
1285
|
+
raw,
|
|
1286
|
+
model: response.model,
|
|
1287
|
+
usage: {
|
|
1288
|
+
input: response.usage?.prompt_tokens ?? 0,
|
|
1289
|
+
output: response.usage?.completion_tokens ?? 0
|
|
1290
|
+
},
|
|
1291
|
+
stop_reason: response.choices[0]?.finish_reason ?? "unknown",
|
|
1292
|
+
id: response.id
|
|
1293
|
+
};
|
|
1294
|
+
const duration = Date.now() - startTime;
|
|
1295
|
+
emitExtensionEvent(ctx, {
|
|
1296
|
+
event: "openai:generate",
|
|
1297
|
+
subsystem: "extension:openai",
|
|
1298
|
+
duration,
|
|
1299
|
+
model: response.model,
|
|
1300
|
+
usage: result2.usage,
|
|
1301
|
+
request: apiMessages,
|
|
1302
|
+
content: raw
|
|
1303
|
+
});
|
|
1304
|
+
return result2;
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
const duration = Date.now() - startTime;
|
|
1307
|
+
const rillError = error instanceof RuntimeError6 ? error : mapProviderError("OpenAI", error, detectOpenAIError);
|
|
1308
|
+
emitExtensionEvent(ctx, {
|
|
1309
|
+
event: "openai:error",
|
|
1310
|
+
subsystem: "extension:openai",
|
|
1311
|
+
error: rillError.message,
|
|
1312
|
+
duration
|
|
1313
|
+
});
|
|
1314
|
+
throw rillError;
|
|
1315
|
+
}
|
|
1316
|
+
},
|
|
1317
|
+
description: "Generate structured output from OpenAI API",
|
|
1318
|
+
returnType: { type: "dict" }
|
|
932
1319
|
}
|
|
933
1320
|
};
|
|
934
1321
|
result.dispose = dispose;
|
|
@@ -937,7 +1324,19 @@ function createOpenAIExtension(config) {
|
|
|
937
1324
|
|
|
938
1325
|
// src/index.ts
|
|
939
1326
|
var VERSION = "0.0.1";
|
|
1327
|
+
var configSchema = {
|
|
1328
|
+
api_key: { type: "string", required: true, secret: true },
|
|
1329
|
+
model: { type: "string", required: true },
|
|
1330
|
+
base_url: { type: "string" },
|
|
1331
|
+
temperature: { type: "number" },
|
|
1332
|
+
max_tokens: { type: "number" },
|
|
1333
|
+
timeout: { type: "number" },
|
|
1334
|
+
max_retries: { type: "number" },
|
|
1335
|
+
system: { type: "string" },
|
|
1336
|
+
embed_model: { type: "string" }
|
|
1337
|
+
};
|
|
940
1338
|
export {
|
|
941
1339
|
VERSION,
|
|
1340
|
+
configSchema,
|
|
942
1341
|
createOpenAIExtension
|
|
943
1342
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rcrsr/rill-ext-openai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "rill extension for OpenAI API integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Andre Bremer",
|
|
@@ -17,33 +17,34 @@
|
|
|
17
17
|
"scripting"
|
|
18
18
|
],
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"@rcrsr/rill": "^0.
|
|
20
|
+
"@rcrsr/rill": "^0.11.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@
|
|
23
|
+
"@rcrsr/rill": "^0.11.0",
|
|
24
|
+
"@types/node": "^25.3.0",
|
|
24
25
|
"dts-bundle-generator": "^9.5.1",
|
|
25
|
-
"tsup": "^8.5.
|
|
26
|
-
"undici-types": "^7.
|
|
27
|
-
"@rcrsr/rill-ext-llm-shared": "^0.0.1"
|
|
28
|
-
"@rcrsr/rill": "^0.8.6"
|
|
26
|
+
"tsup": "^8.5.1",
|
|
27
|
+
"undici-types": "^7.22.0",
|
|
28
|
+
"@rcrsr/rill-ext-llm-shared": "^0.0.1"
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"dist"
|
|
32
32
|
],
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
35
|
-
"url": "git+https://github.com/rcrsr/rill.git",
|
|
35
|
+
"url": "git+https://github.com/rcrsr/rill-ext.git",
|
|
36
36
|
"directory": "packages/ext/llm-openai"
|
|
37
37
|
},
|
|
38
|
-
"homepage": "https://
|
|
38
|
+
"homepage": "https://github.com/rcrsr/rill-ext/tree/main/packages/ext/llm-openai#readme",
|
|
39
39
|
"bugs": {
|
|
40
|
-
"url": "https://github.com/rcrsr/rill/issues"
|
|
40
|
+
"url": "https://github.com/rcrsr/rill-ext/issues"
|
|
41
41
|
},
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"openai": "^6.
|
|
46
|
+
"openai": "^6.25.0",
|
|
47
|
+
"@rcrsr/rill-ext-param-shared": "0.0.1"
|
|
47
48
|
},
|
|
48
49
|
"scripts": {
|
|
49
50
|
"build": "tsup && dts-bundle-generator --config dts-bundle-generator.config.cjs",
|