@rcrsr/rill-ext-openai 0.8.6 → 0.9.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 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 `tool_loop` host functions. Compatible with any OpenAI-compatible server (LM Studio, Ollama, vLLM).
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-4-turbo',
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
- ## Host Functions
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
- Requires `OPENAI_API_KEY` environment variable (or `--base-url` for local servers).
41
+ See [full documentation](docs/extension-llm-openai.md) for configuration, functions, error handling, events, and examples.
160
42
 
161
- ## Documentation
43
+ ## Related
162
44
 
163
- | Document | Description |
164
- |----------|-------------|
165
- | [Extensions Guide](https://github.com/rcrsr/rill/blob/main/docs/integration-extensions.md) | Extension contract and patterns |
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 RuntimeError4,
4
+ RuntimeError as RuntimeError5,
5
5
  emitExtensionEvent,
6
6
  createVector,
7
- isCallable as isCallable2,
7
+ isDict as isDict2,
8
8
  isVector
9
9
  } from "@rcrsr/rill";
10
10
 
@@ -76,29 +76,108 @@ function mapProviderError(providerName, error, detect) {
76
76
  }
77
77
 
78
78
  // ../../shared/ext-llm/dist/tool-loop.js
79
- import { isCallable, isDict, RuntimeError as RuntimeError3 } from "@rcrsr/rill";
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 buildJsonSchema(rillSchema) {
100
+ const properties = {};
101
+ const required = [];
102
+ for (const [key, value] of Object.entries(rillSchema)) {
103
+ if (typeof value === "string") {
104
+ properties[key] = buildProperty(value);
105
+ } else if (typeof value === "object" && value !== null) {
106
+ properties[key] = buildProperty(value);
107
+ } else {
108
+ throw new RuntimeError3("RILL-R004", `unsupported type: ${String(value)}`);
109
+ }
110
+ required.push(key);
111
+ }
112
+ return { type: "object", properties, required, additionalProperties: false };
113
+ }
114
+ function buildProperty(descriptor) {
115
+ if (typeof descriptor === "string") {
116
+ const jsonType2 = mapRillType(descriptor);
117
+ return { type: jsonType2 };
118
+ }
119
+ const rillType = descriptor["type"];
120
+ if (typeof rillType !== "string") {
121
+ throw new RuntimeError3("RILL-R004", `unsupported type: ${String(rillType)}`);
122
+ }
123
+ const jsonType = mapRillType(rillType);
124
+ const property = { type: jsonType };
125
+ const description = descriptor["description"];
126
+ if (typeof description === "string") {
127
+ property.description = description;
128
+ }
129
+ if ("enum" in descriptor) {
130
+ if (rillType !== "string") {
131
+ throw new RuntimeError3("RILL-R004", "enum is only valid for string type");
132
+ }
133
+ const enumValues = descriptor["enum"];
134
+ if (Array.isArray(enumValues)) {
135
+ property.enum = enumValues;
136
+ }
137
+ }
138
+ if (rillType === "list" && "items" in descriptor) {
139
+ const items = descriptor["items"];
140
+ if (typeof items === "string") {
141
+ property.items = buildProperty(items);
142
+ } else if (typeof items === "object" && items !== null) {
143
+ property.items = buildProperty(items);
144
+ }
145
+ }
146
+ if (rillType === "dict" && "properties" in descriptor) {
147
+ const nestedProps = descriptor["properties"];
148
+ if (typeof nestedProps === "object" && nestedProps !== null) {
149
+ const subSchema = buildJsonSchema(nestedProps);
150
+ property.properties = subSchema.properties;
151
+ property.required = subSchema.required;
152
+ property.additionalProperties = false;
153
+ }
154
+ }
155
+ return property;
156
+ }
157
+
158
+ // ../../shared/ext-llm/dist/tool-loop.js
80
159
  async function executeToolCall(toolName, toolInput, tools, context) {
81
160
  if (!isDict(tools)) {
82
- throw new RuntimeError3("RILL-R004", "tools must be a dict mapping tool names to functions");
161
+ throw new RuntimeError4("RILL-R004", "tool_loop: tools must be a dict of name \u2192 callable");
83
162
  }
84
163
  const toolsDict = tools;
85
164
  const toolFn = toolsDict[toolName];
86
165
  if (toolFn === void 0 || toolFn === null) {
87
- throw new RuntimeError3("RILL-R004", `Unknown tool: ${toolName}`);
166
+ throw new RuntimeError4("RILL-R004", `Unknown tool: ${toolName}`);
88
167
  }
89
168
  if (!isCallable(toolFn)) {
90
- throw new RuntimeError3("RILL-R004", `Invalid tool input for ${toolName}: tool must be callable`);
169
+ throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: tool must be callable`);
91
170
  }
92
171
  if (typeof toolInput !== "object" || toolInput === null) {
93
- throw new RuntimeError3("RILL-R004", `Invalid tool input for ${toolName}: input must be an object`);
172
+ throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: input must be an object`);
94
173
  }
95
174
  const callable = toolFn;
96
- if (callable.kind !== "runtime" && callable.kind !== "application") {
97
- throw new RuntimeError3("RILL-R004", `Invalid tool input for ${toolName}: tool must be application or runtime callable`);
175
+ if (callable.kind !== "runtime" && callable.kind !== "application" && callable.kind !== "script") {
176
+ throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: tool must be application, runtime, or script callable`);
98
177
  }
99
178
  try {
100
179
  let args;
101
- if (callable.kind === "application" && callable.params) {
180
+ if ((callable.kind === "application" || callable.kind === "script") && callable.params && callable.params.length > 0) {
102
181
  const params = callable.params;
103
182
  const inputDict = toolInput;
104
183
  args = params.map((param) => {
@@ -108,6 +187,12 @@ async function executeToolCall(toolName, toolInput, tools, context) {
108
187
  } else {
109
188
  args = [toolInput];
110
189
  }
190
+ if (callable.kind === "script") {
191
+ if (!context) {
192
+ throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: script callable requires a runtime context`);
193
+ }
194
+ return await invokeCallable(callable, args, context);
195
+ }
111
196
  const ctx = context ?? {
112
197
  parent: void 0,
113
198
  variables: /* @__PURE__ */ new Map(),
@@ -116,77 +201,146 @@ async function executeToolCall(toolName, toolInput, tools, context) {
116
201
  const result = callable.fn(args, ctx);
117
202
  return result instanceof Promise ? await result : result;
118
203
  } catch (error) {
119
- if (error instanceof RuntimeError3) {
204
+ if (error instanceof RuntimeError4) {
120
205
  throw error;
121
206
  }
122
207
  const message = error instanceof Error ? error.message : "Unknown error";
123
- throw new RuntimeError3("RILL-R004", `Invalid tool input for ${toolName}: ${message}`);
208
+ throw new RuntimeError4("RILL-R004", `Invalid tool input for ${toolName}: ${message}`);
209
+ }
210
+ }
211
+ function sanitizeToolName(name) {
212
+ const match = name.match(/^[a-zA-Z0-9_-]*/);
213
+ const sanitized = match ? match[0] : "";
214
+ return sanitized.length > 0 ? sanitized : name;
215
+ }
216
+ function patchResponseToolCallNames(response, nameMap) {
217
+ if (!nameMap.size || !response || typeof response !== "object")
218
+ return;
219
+ const resp = response;
220
+ if (Array.isArray(resp["choices"])) {
221
+ for (const choice of resp["choices"]) {
222
+ const msg = choice?.["message"];
223
+ const tcs = msg?.["tool_calls"];
224
+ if (Array.isArray(tcs)) {
225
+ for (const tc of tcs) {
226
+ const fn = tc?.["function"];
227
+ if (fn && typeof fn["name"] === "string") {
228
+ const orig = fn["name"];
229
+ const san = nameMap.get(orig);
230
+ if (san !== void 0)
231
+ fn["name"] = san;
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }
237
+ if (Array.isArray(resp["content"])) {
238
+ for (const block of resp["content"]) {
239
+ const b = block;
240
+ if (b?.["type"] === "tool_use" && typeof b?.["name"] === "string") {
241
+ const orig = b["name"];
242
+ const san = nameMap.get(orig);
243
+ if (san !== void 0)
244
+ b["name"] = san;
245
+ }
246
+ }
247
+ }
248
+ if (Array.isArray(resp["functionCalls"])) {
249
+ for (const fc of resp["functionCalls"]) {
250
+ const f = fc;
251
+ if (typeof f?.["name"] === "string") {
252
+ const orig = f["name"];
253
+ const san = nameMap.get(orig);
254
+ if (san !== void 0)
255
+ f["name"] = san;
256
+ }
257
+ }
258
+ }
259
+ if (Array.isArray(resp["candidates"])) {
260
+ for (const cand of resp["candidates"]) {
261
+ const content = cand?.["content"];
262
+ const parts = content?.["parts"];
263
+ if (Array.isArray(parts)) {
264
+ for (const part of parts) {
265
+ const fc = part?.["functionCall"];
266
+ if (fc && typeof fc["name"] === "string") {
267
+ const orig = fc["name"];
268
+ const san = nameMap.get(orig);
269
+ if (san !== void 0)
270
+ fc["name"] = san;
271
+ }
272
+ }
273
+ }
274
+ }
124
275
  }
125
276
  }
126
277
  async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent, maxTurns = 10, context) {
127
278
  if (tools === void 0) {
128
- throw new RuntimeError3("RILL-R004", "tools parameter is required");
279
+ throw new RuntimeError4("RILL-R004", "tools parameter is required");
129
280
  }
130
281
  if (!isDict(tools)) {
131
- throw new RuntimeError3("RILL-R004", "tools must be a dict mapping tool names to functions");
282
+ throw new RuntimeError4("RILL-R004", "tool_loop: tools must be a dict of name \u2192 callable");
132
283
  }
133
284
  const toolsDict = tools;
134
285
  const toolDescriptors = Object.entries(toolsDict).map(([name, fn]) => {
135
286
  const fnValue = fn;
287
+ if (isRuntimeCallable(fnValue)) {
288
+ throw new RuntimeError4("RILL-R004", `tool_loop: builtin "${name}" cannot be used as a tool \u2014 wrap in a closure`);
289
+ }
136
290
  if (!isCallable(fnValue)) {
137
- throw new RuntimeError3("RILL-R004", `tool '${name}' must be callable function`);
291
+ throw new RuntimeError4("RILL-R004", `tool_loop: tool "${name}" is not a callable`);
138
292
  }
139
293
  const callable = fnValue;
140
- const description = callable.kind === "application" && callable.description ? callable.description : "";
141
- const properties = {};
142
- const required = [];
143
- if (callable.kind === "application" && callable.params) {
144
- for (const param of callable.params) {
145
- let jsonSchemaType;
146
- switch (param.typeName) {
147
- case "string":
148
- jsonSchemaType = "string";
149
- break;
150
- case "number":
151
- jsonSchemaType = "number";
152
- break;
153
- case "bool":
154
- jsonSchemaType = "boolean";
155
- break;
156
- case "list":
157
- jsonSchemaType = "array";
158
- break;
159
- case "dict":
160
- case "vector":
161
- jsonSchemaType = "object";
162
- break;
163
- case null:
164
- jsonSchemaType = "string";
165
- break;
166
- default:
167
- jsonSchemaType = "string";
168
- break;
294
+ let description;
295
+ if (callable.kind === "script") {
296
+ description = callable.annotations["description"] ?? "";
297
+ } else {
298
+ description = callable.description ?? "";
299
+ }
300
+ let inputSchema;
301
+ const params = callable.kind === "application" ? callable.params ?? [] : callable.kind === "script" ? callable.params : [];
302
+ if (params.length > 0) {
303
+ const properties = {};
304
+ const required = [];
305
+ for (const param of params) {
306
+ const property = {};
307
+ if (param.typeName !== null) {
308
+ const descriptor = {
309
+ [param.name]: { type: param.typeName }
310
+ };
311
+ const schema = buildJsonSchema(descriptor);
312
+ const built = schema.properties[param.name];
313
+ if (built !== void 0) {
314
+ Object.assign(property, built);
315
+ }
316
+ }
317
+ let paramDesc;
318
+ if (callable.kind === "script") {
319
+ const annot = callable.paramAnnotations[param.name];
320
+ paramDesc = annot?.["description"] ?? "";
321
+ } else {
322
+ paramDesc = param.description ?? "";
169
323
  }
170
- const property = {
171
- type: jsonSchemaType
172
- };
173
- if (param.description) {
174
- property["description"] = param.description;
324
+ if (paramDesc) {
325
+ property["description"] = paramDesc;
175
326
  }
176
327
  properties[param.name] = property;
177
328
  if (param.defaultValue === null) {
178
329
  required.push(param.name);
179
330
  }
180
331
  }
332
+ inputSchema = {
333
+ type: "object",
334
+ properties,
335
+ required
336
+ };
337
+ } else {
338
+ inputSchema = { type: "object", properties: {}, required: [] };
181
339
  }
182
340
  return {
183
341
  name,
184
342
  description,
185
- input_schema: {
186
- type: "object",
187
- properties,
188
- required
189
- }
343
+ input_schema: inputSchema
190
344
  };
191
345
  });
192
346
  const providerTools = callbacks.buildTools(toolDescriptors);
@@ -203,7 +357,7 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
203
357
  response = await callbacks.callAPI(currentMessages, providerTools);
204
358
  } catch (error) {
205
359
  const message = error instanceof Error ? error.message : "Unknown error";
206
- throw new RuntimeError3("RILL-R004", `Provider API error: ${message}`, void 0, { cause: error });
360
+ throw new RuntimeError4("RILL-R004", `Provider API error: ${message}`, void 0, { cause: error });
207
361
  }
208
362
  if (typeof response === "object" && response !== null && "usage" in response) {
209
363
  const usage = response["usage"];
@@ -215,7 +369,17 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
215
369
  totalOutputTokens += outputTokens;
216
370
  }
217
371
  }
218
- const toolCalls = callbacks.extractToolCalls(response);
372
+ const rawToolCalls = callbacks.extractToolCalls(response);
373
+ const nameMap = /* @__PURE__ */ new Map();
374
+ const toolCalls = rawToolCalls?.map((tc) => {
375
+ const sanitized = sanitizeToolName(tc.name);
376
+ if (sanitized !== tc.name)
377
+ nameMap.set(tc.name, sanitized);
378
+ return sanitized !== tc.name ? { ...tc, name: sanitized } : tc;
379
+ }) ?? null;
380
+ if (nameMap.size > 0) {
381
+ patchResponseToolCallNames(response, nameMap);
382
+ }
219
383
  if (toolCalls === null || toolCalls.length === 0) {
220
384
  return {
221
385
  response,
@@ -240,7 +404,7 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
240
404
  const duration = Date.now() - toolStartTime;
241
405
  consecutiveErrors++;
242
406
  let originalError;
243
- if (error instanceof RuntimeError3) {
407
+ if (error instanceof RuntimeError4) {
244
408
  const prefix = `Invalid tool input for ${name}: `;
245
409
  if (error.message.startsWith(prefix)) {
246
410
  originalError = error.message.slice(prefix.length);
@@ -265,12 +429,20 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
265
429
  duration
266
430
  });
267
431
  if (consecutiveErrors >= maxErrors) {
268
- throw new RuntimeError3("RILL-R004", `Tool execution failed: ${maxErrors} consecutive errors`);
432
+ throw new RuntimeError4("RILL-R004", `Tool execution failed: ${maxErrors} consecutive errors (last: ${name}: ${originalError})`);
269
433
  }
270
434
  }
271
435
  }
436
+ const assistantMessage = callbacks.formatAssistantMessage(response);
437
+ if (assistantMessage != null) {
438
+ currentMessages.push(assistantMessage);
439
+ }
272
440
  const toolResultMessage = callbacks.formatToolResult(toolResults);
273
- currentMessages.push(toolResultMessage);
441
+ if (Array.isArray(toolResultMessage)) {
442
+ currentMessages.push(...toolResultMessage);
443
+ } else {
444
+ currentMessages.push(toolResultMessage);
445
+ }
274
446
  }
275
447
  return {
276
448
  response: null,
@@ -281,7 +453,7 @@ async function executeToolLoop(messages, tools, maxErrors, callbacks, emitEvent,
281
453
  }
282
454
 
283
455
  // src/factory.ts
284
- var DEFAULT_MAX_TOKENS = 4096;
456
+ var DEFAULT_MAX_COMPLETION_TOKENS = 4096;
285
457
  var detectOpenAIError = (error) => {
286
458
  if (error instanceof OpenAI.APIError) {
287
459
  return {
@@ -303,7 +475,7 @@ function createOpenAIExtension(config) {
303
475
  });
304
476
  const factoryModel = config.model;
305
477
  const factoryTemperature = config.temperature;
306
- const factoryMaxTokens = config.max_tokens ?? DEFAULT_MAX_TOKENS;
478
+ const factoryMaxTokens = config.max_tokens ?? DEFAULT_MAX_COMPLETION_TOKENS;
307
479
  const factorySystem = config.system;
308
480
  const factoryEmbedModel = config.embed_model;
309
481
  void factoryEmbedModel;
@@ -337,7 +509,7 @@ function createOpenAIExtension(config) {
337
509
  const text = args[0];
338
510
  const options = args[1] ?? {};
339
511
  if (text.trim().length === 0) {
340
- throw new RuntimeError4("RILL-R004", "prompt text cannot be empty");
512
+ throw new RuntimeError5("RILL-R004", "prompt text cannot be empty");
341
513
  }
342
514
  const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
343
515
  const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
@@ -354,7 +526,7 @@ function createOpenAIExtension(config) {
354
526
  });
355
527
  const apiParams = {
356
528
  model: factoryModel,
357
- max_tokens: maxTokens,
529
+ max_completion_tokens: maxTokens,
358
530
  messages: apiMessages
359
531
  };
360
532
  if (factoryTemperature !== void 0) {
@@ -383,7 +555,9 @@ function createOpenAIExtension(config) {
383
555
  subsystem: "extension:openai",
384
556
  duration,
385
557
  model: response.model,
386
- usage: result2.usage
558
+ usage: result2.usage,
559
+ request: apiMessages,
560
+ content
387
561
  });
388
562
  return result2;
389
563
  } catch (error) {
@@ -417,7 +591,7 @@ function createOpenAIExtension(config) {
417
591
  const messages = args[0];
418
592
  const options = args[1] ?? {};
419
593
  if (messages.length === 0) {
420
- throw new RuntimeError4(
594
+ throw new RuntimeError5(
421
595
  "RILL-R004",
422
596
  "messages list cannot be empty"
423
597
  );
@@ -434,18 +608,18 @@ function createOpenAIExtension(config) {
434
608
  for (let i = 0; i < messages.length; i++) {
435
609
  const msg = messages[i];
436
610
  if (!msg || typeof msg !== "object" || !("role" in msg)) {
437
- throw new RuntimeError4(
611
+ throw new RuntimeError5(
438
612
  "RILL-R004",
439
613
  "message missing required 'role' field"
440
614
  );
441
615
  }
442
616
  const role = msg["role"];
443
617
  if (role !== "user" && role !== "assistant" && role !== "tool") {
444
- throw new RuntimeError4("RILL-R004", `invalid role '${role}'`);
618
+ throw new RuntimeError5("RILL-R004", `invalid role '${role}'`);
445
619
  }
446
620
  if (role === "user" || role === "tool") {
447
621
  if (!("content" in msg) || typeof msg["content"] !== "string") {
448
- throw new RuntimeError4(
622
+ throw new RuntimeError5(
449
623
  "RILL-R004",
450
624
  `${role} message requires 'content'`
451
625
  );
@@ -458,7 +632,7 @@ function createOpenAIExtension(config) {
458
632
  const hasContent = "content" in msg && msg["content"];
459
633
  const hasToolCalls = "tool_calls" in msg && msg["tool_calls"];
460
634
  if (!hasContent && !hasToolCalls) {
461
- throw new RuntimeError4(
635
+ throw new RuntimeError5(
462
636
  "RILL-R004",
463
637
  "assistant message requires 'content' or 'tool_calls'"
464
638
  );
@@ -473,7 +647,7 @@ function createOpenAIExtension(config) {
473
647
  }
474
648
  const apiParams = {
475
649
  model: factoryModel,
476
- max_tokens: maxTokens,
650
+ max_completion_tokens: maxTokens,
477
651
  messages: apiMessages
478
652
  };
479
653
  if (factoryTemperature !== void 0) {
@@ -507,7 +681,9 @@ function createOpenAIExtension(config) {
507
681
  subsystem: "extension:openai",
508
682
  duration,
509
683
  model: response.model,
510
- usage: result2.usage
684
+ usage: result2.usage,
685
+ request: apiMessages,
686
+ content
511
687
  });
512
688
  return result2;
513
689
  } catch (error) {
@@ -545,7 +721,7 @@ function createOpenAIExtension(config) {
545
721
  });
546
722
  const embeddingData = response.data[0]?.embedding;
547
723
  if (!embeddingData || embeddingData.length === 0) {
548
- throw new RuntimeError4(
724
+ throw new RuntimeError5(
549
725
  "RILL-R004",
550
726
  "OpenAI: empty embedding returned"
551
727
  );
@@ -601,7 +777,7 @@ function createOpenAIExtension(config) {
601
777
  for (const embeddingItem of response.data) {
602
778
  const embeddingData = embeddingItem.embedding;
603
779
  if (!embeddingData || embeddingData.length === 0) {
604
- throw new RuntimeError4(
780
+ throw new RuntimeError5(
605
781
  "RILL-R004",
606
782
  "OpenAI: empty embedding returned"
607
783
  );
@@ -653,79 +829,14 @@ function createOpenAIExtension(config) {
653
829
  const prompt = args[0];
654
830
  const options = args[1] ?? {};
655
831
  if (prompt.trim().length === 0) {
656
- throw new RuntimeError4("RILL-R004", "prompt text cannot be empty");
832
+ throw new RuntimeError5("RILL-R004", "prompt text cannot be empty");
657
833
  }
658
- if (!("tools" in options) || !Array.isArray(options["tools"])) {
659
- throw new RuntimeError4(
834
+ if (!("tools" in options) || !isDict2(options["tools"])) {
835
+ throw new RuntimeError5(
660
836
  "RILL-R004",
661
837
  "tool_loop requires 'tools' option"
662
838
  );
663
839
  }
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
840
  const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
730
841
  const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
731
842
  const maxErrors = typeof options["max_errors"] === "number" ? options["max_errors"] : 3;
@@ -741,17 +852,17 @@ function createOpenAIExtension(config) {
741
852
  const prependedMessages = options["messages"];
742
853
  for (const msg of prependedMessages) {
743
854
  if (!msg || typeof msg !== "object" || !("role" in msg)) {
744
- throw new RuntimeError4(
855
+ throw new RuntimeError5(
745
856
  "RILL-R004",
746
857
  "message missing required 'role' field"
747
858
  );
748
859
  }
749
860
  const role = msg["role"];
750
861
  if (role !== "user" && role !== "assistant") {
751
- throw new RuntimeError4("RILL-R004", `invalid role '${role}'`);
862
+ throw new RuntimeError5("RILL-R004", `invalid role '${role}'`);
752
863
  }
753
864
  if (!("content" in msg) || typeof msg["content"] !== "string") {
754
- throw new RuntimeError4(
865
+ throw new RuntimeError5(
755
866
  "RILL-R004",
756
867
  `${role} message requires 'content'`
757
868
  );
@@ -782,7 +893,7 @@ function createOpenAIExtension(config) {
782
893
  callAPI: async (msgs, tools) => {
783
894
  const apiParams = {
784
895
  model: factoryModel,
785
- max_tokens: maxTokens,
896
+ max_completion_tokens: maxTokens,
786
897
  messages: msgs,
787
898
  tools,
788
899
  tool_choice: "auto"
@@ -839,6 +950,21 @@ function createOpenAIExtension(config) {
839
950
  };
840
951
  });
841
952
  },
953
+ // Extract assistant message (with tool_calls) from OpenAI response
954
+ formatAssistantMessage: (response2) => {
955
+ if (!response2 || typeof response2 !== "object" || !("choices" in response2)) {
956
+ return null;
957
+ }
958
+ const choices = response2.choices;
959
+ if (!Array.isArray(choices) || choices.length === 0) {
960
+ return null;
961
+ }
962
+ const choice = choices[0];
963
+ if (!choice || typeof choice !== "object" || !("message" in choice)) {
964
+ return null;
965
+ }
966
+ return choice.message;
967
+ },
842
968
  // Format tool results into OpenAI message format
843
969
  formatToolResult: (toolResults) => {
844
970
  return toolResults.map((tr) => ({
@@ -850,7 +976,7 @@ function createOpenAIExtension(config) {
850
976
  };
851
977
  const loopResult = await executeToolLoop(
852
978
  messages,
853
- toolsDict,
979
+ options["tools"],
854
980
  maxErrors,
855
981
  callbacks,
856
982
  (event, data) => {
@@ -908,7 +1034,9 @@ function createOpenAIExtension(config) {
908
1034
  subsystem: "extension:openai",
909
1035
  turns: loopResult.turns,
910
1036
  total_duration: duration,
911
- usage: result2.usage
1037
+ usage: result2.usage,
1038
+ request: messages,
1039
+ content
912
1040
  });
913
1041
  return result2;
914
1042
  } catch (error) {
@@ -929,6 +1057,125 @@ function createOpenAIExtension(config) {
929
1057
  },
930
1058
  description: "Execute tool-use loop with OpenAI API",
931
1059
  returnType: "dict"
1060
+ },
1061
+ // IR-3: openai::generate
1062
+ generate: {
1063
+ params: [
1064
+ { name: "prompt", type: "string" },
1065
+ { name: "options", type: "dict" }
1066
+ ],
1067
+ fn: async (args, ctx) => {
1068
+ const startTime = Date.now();
1069
+ try {
1070
+ const prompt = args[0];
1071
+ const options = args[1] ?? {};
1072
+ if (!("schema" in options) || options["schema"] === null || options["schema"] === void 0) {
1073
+ throw new RuntimeError5(
1074
+ "RILL-R004",
1075
+ "generate requires 'schema' option"
1076
+ );
1077
+ }
1078
+ const rillSchema = options["schema"];
1079
+ const jsonSchema = buildJsonSchema(rillSchema);
1080
+ const system = typeof options["system"] === "string" ? options["system"] : factorySystem;
1081
+ const maxTokens = typeof options["max_tokens"] === "number" ? options["max_tokens"] : factoryMaxTokens;
1082
+ const apiMessages = [];
1083
+ if (system !== void 0) {
1084
+ apiMessages.push({
1085
+ role: "system",
1086
+ content: system
1087
+ });
1088
+ }
1089
+ if ("messages" in options && Array.isArray(options["messages"])) {
1090
+ const prependedMessages = options["messages"];
1091
+ for (const msg of prependedMessages) {
1092
+ if (!msg || typeof msg !== "object" || !("role" in msg)) {
1093
+ throw new RuntimeError5(
1094
+ "RILL-R004",
1095
+ "message missing required 'role' field"
1096
+ );
1097
+ }
1098
+ const role = msg["role"];
1099
+ if (role !== "user" && role !== "assistant") {
1100
+ throw new RuntimeError5("RILL-R004", `invalid role '${role}'`);
1101
+ }
1102
+ if (!("content" in msg) || typeof msg["content"] !== "string") {
1103
+ throw new RuntimeError5(
1104
+ "RILL-R004",
1105
+ `${role} message requires 'content'`
1106
+ );
1107
+ }
1108
+ apiMessages.push({
1109
+ role,
1110
+ content: msg["content"]
1111
+ });
1112
+ }
1113
+ }
1114
+ apiMessages.push({ role: "user", content: prompt });
1115
+ const apiParams = {
1116
+ model: factoryModel,
1117
+ max_completion_tokens: maxTokens,
1118
+ messages: apiMessages,
1119
+ response_format: {
1120
+ type: "json_schema",
1121
+ json_schema: {
1122
+ name: "output",
1123
+ schema: jsonSchema,
1124
+ strict: true
1125
+ }
1126
+ }
1127
+ };
1128
+ if (factoryTemperature !== void 0) {
1129
+ apiParams.temperature = factoryTemperature;
1130
+ }
1131
+ const response = await client.chat.completions.create(apiParams);
1132
+ const raw = response.choices[0]?.message?.content ?? "";
1133
+ let data;
1134
+ try {
1135
+ data = JSON.parse(raw);
1136
+ } catch (parseError) {
1137
+ const detail = parseError instanceof Error ? parseError.message : String(parseError);
1138
+ throw new RuntimeError5(
1139
+ "RILL-R004",
1140
+ `generate: failed to parse response JSON: ${detail}`
1141
+ );
1142
+ }
1143
+ const result2 = {
1144
+ data,
1145
+ raw,
1146
+ model: response.model,
1147
+ usage: {
1148
+ input: response.usage?.prompt_tokens ?? 0,
1149
+ output: response.usage?.completion_tokens ?? 0
1150
+ },
1151
+ stop_reason: response.choices[0]?.finish_reason ?? "unknown",
1152
+ id: response.id
1153
+ };
1154
+ const duration = Date.now() - startTime;
1155
+ emitExtensionEvent(ctx, {
1156
+ event: "openai:generate",
1157
+ subsystem: "extension:openai",
1158
+ duration,
1159
+ model: response.model,
1160
+ usage: result2.usage,
1161
+ request: apiMessages,
1162
+ content: raw
1163
+ });
1164
+ return result2;
1165
+ } catch (error) {
1166
+ const duration = Date.now() - startTime;
1167
+ const rillError = error instanceof RuntimeError5 ? error : mapProviderError("OpenAI", error, detectOpenAIError);
1168
+ emitExtensionEvent(ctx, {
1169
+ event: "openai:error",
1170
+ subsystem: "extension:openai",
1171
+ error: rillError.message,
1172
+ duration
1173
+ });
1174
+ throw rillError;
1175
+ }
1176
+ },
1177
+ description: "Generate structured output from OpenAI API",
1178
+ returnType: "dict"
932
1179
  }
933
1180
  };
934
1181
  result.dispose = dispose;
@@ -937,7 +1184,19 @@ function createOpenAIExtension(config) {
937
1184
 
938
1185
  // src/index.ts
939
1186
  var VERSION = "0.0.1";
1187
+ var configSchema = {
1188
+ api_key: { type: "string", required: true, secret: true },
1189
+ model: { type: "string", required: true },
1190
+ base_url: { type: "string" },
1191
+ temperature: { type: "number" },
1192
+ max_tokens: { type: "number" },
1193
+ timeout: { type: "number" },
1194
+ max_retries: { type: "number" },
1195
+ system: { type: "string" },
1196
+ embed_model: { type: "string" }
1197
+ };
940
1198
  export {
941
1199
  VERSION,
1200
+ configSchema,
942
1201
  createOpenAIExtension
943
1202
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rcrsr/rill-ext-openai",
3
- "version": "0.8.6",
3
+ "version": "0.9.0",
4
4
  "description": "rill extension for OpenAI API integration",
5
5
  "license": "MIT",
6
6
  "author": "Andre Bremer",
@@ -17,33 +17,33 @@
17
17
  "scripting"
18
18
  ],
19
19
  "peerDependencies": {
20
- "@rcrsr/rill": "^0.8.6"
20
+ "@rcrsr/rill": "^0.9.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@types/node": "^25.2.3",
23
+ "@rcrsr/rill": "^0.9.0",
24
+ "@types/node": "^25.3.0",
24
25
  "dts-bundle-generator": "^9.5.1",
25
- "tsup": "^8.5.0",
26
- "undici-types": "^7.21.0",
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://rill.run/docs/extensions/openai/",
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.18.0"
46
+ "openai": "^6.25.0"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsup && dts-bundle-generator --config dts-bundle-generator.config.cjs",