@warlock.js/ai-openai 4.1.1
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 +99 -0
- package/cjs/index.cjs +838 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/config.type.d.mts +113 -0
- package/esm/config.type.d.mts.map +1 -0
- package/esm/embedder.d.mts +56 -0
- package/esm/embedder.d.mts.map +1 -0
- package/esm/embedder.mjs +105 -0
- package/esm/embedder.mjs.map +1 -0
- package/esm/index.d.mts +4 -0
- package/esm/index.mjs +4 -0
- package/esm/known-vision-models.mjs +42 -0
- package/esm/known-vision-models.mjs.map +1 -0
- package/esm/model.mjs +309 -0
- package/esm/model.mjs.map +1 -0
- package/esm/sdk.d.mts +79 -0
- package/esm/sdk.d.mts.map +1 -0
- package/esm/sdk.mjs +97 -0
- package/esm/sdk.mjs.map +1 -0
- package/esm/utils/index.mjs +6 -0
- package/esm/utils/map-finish-reason.mjs +22 -0
- package/esm/utils/map-finish-reason.mjs.map +1 -0
- package/esm/utils/to-openai-messages.mjs +78 -0
- package/esm/utils/to-openai-messages.mjs.map +1 -0
- package/esm/utils/to-openai-tools.mjs +41 -0
- package/esm/utils/to-openai-tools.mjs.map +1 -0
- package/esm/utils/wrap-openai-error.mjs +147 -0
- package/esm/utils/wrap-openai-error.mjs.map +1 -0
- package/llms-full.txt +145 -0
- package/llms.txt +9 -0
- package/package.json +38 -0
- package/skills/README.md +9 -0
- package/skills/setup-openai/SKILL.md +135 -0
package/cjs/index.cjs
ADDED
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
14
|
+
__defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
24
|
+
value: mod,
|
|
25
|
+
enumerable: true
|
|
26
|
+
}) : target, mod));
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
let openai = require("openai");
|
|
30
|
+
openai = __toESM(openai, 1);
|
|
31
|
+
let _warlock_js_ai = require("@warlock.js/ai");
|
|
32
|
+
let _warlock_js_logger = require("@warlock.js/logger");
|
|
33
|
+
|
|
34
|
+
//#region ../../@warlock.js/ai-openai/src/utils/map-finish-reason.ts
|
|
35
|
+
const finishReasonMap = {
|
|
36
|
+
stop: "stop",
|
|
37
|
+
tool_calls: "tool_calls",
|
|
38
|
+
length: "length"
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Map the raw OpenAI `finish_reason` string to the normalized FinishReason union.
|
|
42
|
+
* Unknown/unexpected values fall through to "error".
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* mapFinishReason("stop"); // "stop"
|
|
46
|
+
* mapFinishReason("tool_calls"); // "tool_calls"
|
|
47
|
+
* mapFinishReason(null); // "error"
|
|
48
|
+
*/
|
|
49
|
+
function mapFinishReason(raw) {
|
|
50
|
+
return finishReasonMap[raw ?? ""] ?? "error";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region ../../@warlock.js/ai-openai/src/utils/to-openai-messages.ts
|
|
55
|
+
/**
|
|
56
|
+
* Convert vendor-neutral Message[] to OpenAI's chat message shape.
|
|
57
|
+
* Handles the `tool` role (requires `tool_call_id`) and assistant messages
|
|
58
|
+
* that carry `toolCalls` from a prior model response.
|
|
59
|
+
*
|
|
60
|
+
* Multipart `content` (a `ContentPart[]`) is mapped into OpenAI's user-message
|
|
61
|
+
* content-parts shape: text becomes `{ type: "text", text }`, images become
|
|
62
|
+
* `{ type: "image_url", image_url: { url } }` — with base64 sources rendered
|
|
63
|
+
* as `data:` URLs inline.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* const openaiMessages = toOpenAIMessages([
|
|
67
|
+
* { role: "user", content: "Hi" },
|
|
68
|
+
* { role: "tool", toolCallId: "call_1", content: '{"ok":true}' },
|
|
69
|
+
* ]);
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* toOpenAIMessages([
|
|
73
|
+
* { role: "user", content: [
|
|
74
|
+
* { type: "text", text: "What is this?" },
|
|
75
|
+
* { type: "image", source: { url: "https://example.com/cat.jpg" } },
|
|
76
|
+
* ]},
|
|
77
|
+
* ]);
|
|
78
|
+
*/
|
|
79
|
+
function toOpenAIMessages(messages) {
|
|
80
|
+
return messages.map((m) => {
|
|
81
|
+
if (m.role === "tool") return {
|
|
82
|
+
role: "tool",
|
|
83
|
+
content: stringifyContent(m.content),
|
|
84
|
+
tool_call_id: m.toolCallId ?? ""
|
|
85
|
+
};
|
|
86
|
+
if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0) return {
|
|
87
|
+
role: "assistant",
|
|
88
|
+
content: stringifyContent(m.content),
|
|
89
|
+
tool_calls: m.toolCalls.map((tc) => ({
|
|
90
|
+
id: tc.id,
|
|
91
|
+
type: "function",
|
|
92
|
+
function: {
|
|
93
|
+
name: tc.name,
|
|
94
|
+
arguments: JSON.stringify(tc.input ?? {})
|
|
95
|
+
}
|
|
96
|
+
}))
|
|
97
|
+
};
|
|
98
|
+
if (m.role === "user" && Array.isArray(m.content)) return {
|
|
99
|
+
role: "user",
|
|
100
|
+
content: m.content.map(toOpenAIContentPart)
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
role: m.role,
|
|
104
|
+
content: stringifyContent(m.content)
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Multipart content is only meaningful on user messages — for any other
|
|
110
|
+
* role (system / assistant text / tool), collapse a `ContentPart[]` to
|
|
111
|
+
* its concatenated text so OpenAI's wire format stays valid. Plain
|
|
112
|
+
* strings pass through unchanged.
|
|
113
|
+
*/
|
|
114
|
+
function stringifyContent(content) {
|
|
115
|
+
if (typeof content === "string") return content;
|
|
116
|
+
return content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
117
|
+
}
|
|
118
|
+
function toOpenAIContentPart(part) {
|
|
119
|
+
if (part.type === "text") return {
|
|
120
|
+
type: "text",
|
|
121
|
+
text: part.text
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
type: "image_url",
|
|
125
|
+
image_url: { url: "url" in part.source ? part.source.url : `data:${part.source.mediaType};base64,${part.source.base64}` }
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region ../../@warlock.js/ai-openai/src/utils/to-openai-tools.ts
|
|
131
|
+
/**
|
|
132
|
+
* Convert vendor-neutral ToolConfig[] to OpenAI's tools array.
|
|
133
|
+
* Uses the shared `extractJsonSchema` helper; falls back to an empty-object
|
|
134
|
+
* schema when extraction fails so the tool still registers with the provider.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* const tools = toOpenAITools([weatherTool, calculatorTool]);
|
|
138
|
+
* await client.chat.completions.create({ model, messages, tools });
|
|
139
|
+
*/
|
|
140
|
+
function toOpenAITools(tools) {
|
|
141
|
+
if (!tools || tools.length === 0) return;
|
|
142
|
+
return tools.map((tool) => ({
|
|
143
|
+
type: "function",
|
|
144
|
+
function: {
|
|
145
|
+
name: tool.name,
|
|
146
|
+
description: tool.description,
|
|
147
|
+
parameters: toParameters(tool.input)
|
|
148
|
+
}
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Resolve a tool's input schema to a JSON-Schema object. OpenAI's
|
|
153
|
+
* function `parameters` expects an object root; anything else (or a
|
|
154
|
+
* failed extraction) degrades to an empty-object schema so the tool
|
|
155
|
+
* still registers and the model simply sees no parameters.
|
|
156
|
+
*/
|
|
157
|
+
function toParameters(input) {
|
|
158
|
+
const schema = (0, _warlock_js_ai.extractJsonSchema)(input);
|
|
159
|
+
if (schema && schema.type === "object") return schema;
|
|
160
|
+
return {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region ../../@warlock.js/ai-openai/src/utils/wrap-openai-error.ts
|
|
168
|
+
/**
|
|
169
|
+
* Wrap any thrown value caught inside the OpenAI adapter into the
|
|
170
|
+
* appropriate `@warlock.js/ai` `AIError` subclass.
|
|
171
|
+
*
|
|
172
|
+
* **Dispatch strategy.** Prefers `APIError.code` when present (stable
|
|
173
|
+
* machine identifier across SDK versions), falls back to `status` when
|
|
174
|
+
* `code` is missing (common with proxied deployments that strip the
|
|
175
|
+
* field). Name-based detection (`APIConnectionTimeoutError`) catches
|
|
176
|
+
* transport-layer errors that never produced an HTTP response.
|
|
177
|
+
*
|
|
178
|
+
* `AIError` instances are returned unchanged — callers can pass the
|
|
179
|
+
* error through `try/catch/throw wrap(e)` pipelines without accidental
|
|
180
|
+
* double-wrapping.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* try {
|
|
184
|
+
* return await this.client.chat.completions.create(...);
|
|
185
|
+
* } catch (thrown) {
|
|
186
|
+
* throw wrapOpenAIError(thrown);
|
|
187
|
+
* }
|
|
188
|
+
*/
|
|
189
|
+
function wrapOpenAIError(thrown) {
|
|
190
|
+
if (thrown instanceof _warlock_js_ai.AIError) return thrown;
|
|
191
|
+
const shape = toShape(thrown);
|
|
192
|
+
const context = buildContext(thrown, shape);
|
|
193
|
+
const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));
|
|
194
|
+
if (isTimeout(thrown, shape)) return new _warlock_js_ai.ProviderTimeoutError(message, {
|
|
195
|
+
cause: thrown,
|
|
196
|
+
context
|
|
197
|
+
});
|
|
198
|
+
if (shape.status === 401 || shape.code === "invalid_api_key") return new _warlock_js_ai.ProviderAuthError(message, {
|
|
199
|
+
cause: thrown,
|
|
200
|
+
context
|
|
201
|
+
});
|
|
202
|
+
if (shape.code === "insufficient_quota") return new _warlock_js_ai.QuotaExceededError(message, {
|
|
203
|
+
cause: thrown,
|
|
204
|
+
context
|
|
205
|
+
});
|
|
206
|
+
if (shape.status === 429 || shape.code === "rate_limit_exceeded") return new _warlock_js_ai.ProviderRateLimitError(message, {
|
|
207
|
+
cause: thrown,
|
|
208
|
+
context,
|
|
209
|
+
retryAfter: parseRetryAfter(shape.headers)
|
|
210
|
+
});
|
|
211
|
+
if (shape.code === "context_length_exceeded") return new _warlock_js_ai.ContextLengthExceededError(message, {
|
|
212
|
+
cause: thrown,
|
|
213
|
+
context
|
|
214
|
+
});
|
|
215
|
+
if (shape.code === "content_filter") return new _warlock_js_ai.ContentFilterError(message, {
|
|
216
|
+
cause: thrown,
|
|
217
|
+
context,
|
|
218
|
+
reason: message
|
|
219
|
+
});
|
|
220
|
+
if (typeof shape.status === "number" && shape.status >= 400 && shape.status < 500) return new _warlock_js_ai.InvalidRequestError(message, {
|
|
221
|
+
cause: thrown,
|
|
222
|
+
context
|
|
223
|
+
});
|
|
224
|
+
return new _warlock_js_ai.ProviderError(message, {
|
|
225
|
+
cause: thrown,
|
|
226
|
+
context
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Read the raw error shape without depending on `instanceof APIError`
|
|
231
|
+
* — some consumers wrap the SDK, and proxies sometimes strip the
|
|
232
|
+
* prototype chain. Duck-typing on the visible fields is resilient to
|
|
233
|
+
* both.
|
|
234
|
+
*/
|
|
235
|
+
function toShape(thrown) {
|
|
236
|
+
if (thrown instanceof openai.default.APIError) return {
|
|
237
|
+
status: thrown.status,
|
|
238
|
+
code: thrown.code,
|
|
239
|
+
message: thrown.message,
|
|
240
|
+
type: thrown.type,
|
|
241
|
+
headers: thrown.headers,
|
|
242
|
+
name: thrown.name
|
|
243
|
+
};
|
|
244
|
+
if (typeof thrown === "object" && thrown !== null) {
|
|
245
|
+
const raw = thrown;
|
|
246
|
+
return {
|
|
247
|
+
status: typeof raw.status === "number" ? raw.status : void 0,
|
|
248
|
+
code: typeof raw.code === "string" ? raw.code : void 0,
|
|
249
|
+
message: typeof raw.message === "string" ? raw.message : void 0,
|
|
250
|
+
type: typeof raw.type === "string" ? raw.type : void 0,
|
|
251
|
+
headers: typeof raw.headers === "object" && raw.headers !== null ? raw.headers : void 0,
|
|
252
|
+
name: typeof raw.name === "string" ? raw.name : void 0
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return {};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Decide whether the thrown value represents a timeout. OpenAI's SDK
|
|
259
|
+
* throws `APIConnectionTimeoutError` for transport-level timeouts, and
|
|
260
|
+
* Node surfaces `ETIMEDOUT` / `ECONNABORTED` on the lower socket
|
|
261
|
+
* layer. Either signal counts.
|
|
262
|
+
*/
|
|
263
|
+
function isTimeout(thrown, shape) {
|
|
264
|
+
if (thrown instanceof openai.default.APIConnectionTimeoutError) return true;
|
|
265
|
+
if (shape.name === "APIConnectionTimeoutError") return true;
|
|
266
|
+
if (shape.code === "ETIMEDOUT" || shape.code === "ECONNABORTED") return true;
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Attach the raw diagnostic fields to `error.context` so consumers
|
|
271
|
+
* have everything the provider surfaced without each subclass having
|
|
272
|
+
* to redeclare them. Never includes `cause` — that lives on
|
|
273
|
+
* `error.cause`.
|
|
274
|
+
*/
|
|
275
|
+
function buildContext(thrown, shape) {
|
|
276
|
+
const context = {};
|
|
277
|
+
if (shape.status !== void 0) context.status = shape.status;
|
|
278
|
+
if (shape.code) context.code = shape.code;
|
|
279
|
+
if (shape.type) context.type = shape.type;
|
|
280
|
+
const requestId = readRequestId(thrown);
|
|
281
|
+
if (requestId) context.requestId = requestId;
|
|
282
|
+
return context;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* OpenAI puts the request id on `APIError.request_id`. Extract
|
|
286
|
+
* defensively — both camel and snake keys exist across SDK versions.
|
|
287
|
+
*/
|
|
288
|
+
function readRequestId(thrown) {
|
|
289
|
+
if (typeof thrown !== "object" || thrown === null) return;
|
|
290
|
+
const raw = thrown;
|
|
291
|
+
if (typeof raw.request_id === "string") return raw.request_id;
|
|
292
|
+
if (typeof raw.requestId === "string") return raw.requestId;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Parse the `Retry-After` response header (seconds per HTTP spec)
|
|
296
|
+
* into milliseconds so consumers can feed it straight to `setTimeout`.
|
|
297
|
+
* Returns `undefined` when missing or unparseable.
|
|
298
|
+
*/
|
|
299
|
+
function parseRetryAfter(headers) {
|
|
300
|
+
if (!headers) return;
|
|
301
|
+
const raw = headers["retry-after"] ?? headers["Retry-After"];
|
|
302
|
+
if (!raw) return;
|
|
303
|
+
const seconds = Number(raw);
|
|
304
|
+
if (!Number.isFinite(seconds) || seconds < 0) return;
|
|
305
|
+
return Math.round(seconds * 1e3);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region ../../@warlock.js/ai-openai/src/embedder.ts
|
|
310
|
+
const LOG_MODULE$1 = "ai.openai";
|
|
311
|
+
/**
|
|
312
|
+
* OpenAI-backed implementation of `EmbedderContract`.
|
|
313
|
+
*
|
|
314
|
+
* **Role.** Converts text (or a batch of texts) into floating-point
|
|
315
|
+
* vectors via OpenAI's Embeddings API. Standalone primitive — no
|
|
316
|
+
* relationship to chat completions, tools, or the agent loop.
|
|
317
|
+
*
|
|
318
|
+
* **Dimensions.** When no `dimensions` override is supplied in config,
|
|
319
|
+
* `this.dimensions` starts at `0` and is populated from the first
|
|
320
|
+
* response's vector length, then cached for all subsequent calls —
|
|
321
|
+
* even if a later response were to return a different length, the
|
|
322
|
+
* first value wins so batches stay dimensionally consistent. Passing
|
|
323
|
+
* `dimensions` in config both forwards the truncation hint to the API
|
|
324
|
+
* (for models like `text-embedding-3-*`) and sets the initial value.
|
|
325
|
+
*
|
|
326
|
+
* **Error handling.** Raw OpenAI SDK errors are wrapped into the
|
|
327
|
+
* typed `@warlock.js/ai` `AIError` hierarchy via `wrapOpenAIError` —
|
|
328
|
+
* callers catch `AIError` subclasses (`ProviderRateLimitError`,
|
|
329
|
+
* `ProviderAuthError`, etc.) instead of OpenAI's own classes.
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* const embedder = new OpenAIEmbedder(client, { name: "text-embedding-3-small" });
|
|
333
|
+
* const { vector, dimensions, usage } = await embedder.embed("Hello world");
|
|
334
|
+
* const { vectors } = await embedder.embedMany(["doc 1", "doc 2"]);
|
|
335
|
+
*/
|
|
336
|
+
var OpenAIEmbedder = class {
|
|
337
|
+
constructor(client, config) {
|
|
338
|
+
this.provider = "openai";
|
|
339
|
+
this.logger = _warlock_js_logger.log;
|
|
340
|
+
this.client = client;
|
|
341
|
+
this.name = config.name;
|
|
342
|
+
this.configuredDimensions = config.dimensions;
|
|
343
|
+
this.dimensions = config.dimensions ?? 0;
|
|
344
|
+
}
|
|
345
|
+
async embed(input) {
|
|
346
|
+
const { response, usage } = await this.request(input);
|
|
347
|
+
return {
|
|
348
|
+
vector: response.data[0].embedding,
|
|
349
|
+
dimensions: this.dimensions,
|
|
350
|
+
usage
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
async embedMany(inputs) {
|
|
354
|
+
const { response, usage } = await this.request(inputs);
|
|
355
|
+
return {
|
|
356
|
+
vectors: response.data.map((d) => d.embedding),
|
|
357
|
+
dimensions: this.dimensions,
|
|
358
|
+
usage
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Shared transport for both `embed()` and `embedMany()` — issues the
|
|
363
|
+
* `embeddings.create` call, wraps provider errors, caches dimensions
|
|
364
|
+
* on the first successful response, and returns the raw response
|
|
365
|
+
* plus a camelCase usage object for the caller to shape.
|
|
366
|
+
*/
|
|
367
|
+
async request(input) {
|
|
368
|
+
this.logger.debug(LOG_MODULE$1, "embedder.request", "embeddings.create", {
|
|
369
|
+
model: this.name,
|
|
370
|
+
batch: Array.isArray(input),
|
|
371
|
+
count: Array.isArray(input) ? input.length : 1
|
|
372
|
+
});
|
|
373
|
+
let response;
|
|
374
|
+
try {
|
|
375
|
+
response = await this.client.embeddings.create({
|
|
376
|
+
model: this.name,
|
|
377
|
+
input,
|
|
378
|
+
...this.configuredDimensions !== void 0 ? { dimensions: this.configuredDimensions } : {}
|
|
379
|
+
});
|
|
380
|
+
} catch (thrown) {
|
|
381
|
+
const wrapped = wrapOpenAIError(thrown);
|
|
382
|
+
this.logger.error(LOG_MODULE$1, "embedder.error", wrapped.message, {
|
|
383
|
+
code: wrapped.code,
|
|
384
|
+
context: wrapped.context
|
|
385
|
+
});
|
|
386
|
+
throw wrapped;
|
|
387
|
+
}
|
|
388
|
+
this.logger.debug(LOG_MODULE$1, "embedder.response", "embeddings.create returned", {
|
|
389
|
+
dimensions: response.data[0]?.embedding.length,
|
|
390
|
+
usage: {
|
|
391
|
+
promptTokens: response.usage.prompt_tokens,
|
|
392
|
+
totalTokens: response.usage.total_tokens
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
if (this.dimensions === 0) this.dimensions = response.data[0].embedding.length;
|
|
396
|
+
const usage = {
|
|
397
|
+
promptTokens: response.usage.prompt_tokens,
|
|
398
|
+
totalTokens: response.usage.total_tokens
|
|
399
|
+
};
|
|
400
|
+
return {
|
|
401
|
+
response,
|
|
402
|
+
usage
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region ../../@warlock.js/ai-openai/src/known-vision-models.ts
|
|
409
|
+
/**
|
|
410
|
+
* Model-name prefixes for OpenAI families that support vision input
|
|
411
|
+
* (image attachments) on the Chat Completions API.
|
|
412
|
+
*
|
|
413
|
+
* Matched as a prefix so dated variants (`gpt-4o-2024-08-06`) and
|
|
414
|
+
* `-mini` / `-preview` suffixes (`gpt-4o-mini`, `gpt-4-turbo-preview`)
|
|
415
|
+
* are covered without listing every release tag explicitly.
|
|
416
|
+
*
|
|
417
|
+
* Maintenance: append a new prefix when OpenAI ships a vision-capable
|
|
418
|
+
* model family that doesn't already match. Devs can always override
|
|
419
|
+
* per-model via `openai.model({ name, vision: true | false })` —
|
|
420
|
+
* explicit config wins over inference in either direction.
|
|
421
|
+
*/
|
|
422
|
+
const VISION_CAPABLE_PREFIXES = [
|
|
423
|
+
"gpt-4o",
|
|
424
|
+
"gpt-4-turbo",
|
|
425
|
+
"gpt-4.1",
|
|
426
|
+
"o1",
|
|
427
|
+
"o3",
|
|
428
|
+
"chatgpt-4o"
|
|
429
|
+
];
|
|
430
|
+
/**
|
|
431
|
+
* Infer whether a given OpenAI model name supports vision based on the
|
|
432
|
+
* known-prefix list. Unknown models default to `false` so that passing
|
|
433
|
+
* an image attachment to an unsupported model surfaces a clear,
|
|
434
|
+
* agent-side capability error instead of an opaque OpenAI 400.
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* inferVisionCapability("gpt-4o-mini"); // → true
|
|
438
|
+
* inferVisionCapability("gpt-4o-2024-08-06"); // → true
|
|
439
|
+
* inferVisionCapability("gpt-3.5-turbo"); // → false
|
|
440
|
+
* inferVisionCapability("custom-llm"); // → false
|
|
441
|
+
*/
|
|
442
|
+
function inferVisionCapability(modelName) {
|
|
443
|
+
const normalized = modelName.toLowerCase();
|
|
444
|
+
return VISION_CAPABLE_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region ../../@warlock.js/ai-openai/src/model.ts
|
|
449
|
+
const LOG_MODULE = "ai.openai";
|
|
450
|
+
/**
|
|
451
|
+
* Map an explicit `responseFormat` override to the default
|
|
452
|
+
* `structuredOutput` capability. Loose wire modes (`"json_object"`,
|
|
453
|
+
* `"text"`) don't enforce shape, so the agent needs to see the soft
|
|
454
|
+
* schema hint in the system prompt — that only happens when the
|
|
455
|
+
* capability is `false`. Default (no override) stays `true` to
|
|
456
|
+
* preserve the prior assumption that OpenAI models support strict
|
|
457
|
+
* structured output.
|
|
458
|
+
*/
|
|
459
|
+
function inferStructuredOutput(responseFormat) {
|
|
460
|
+
if (responseFormat === "json_object" || responseFormat === "text") return false;
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* OpenAI-backed implementation of `ModelContract`.
|
|
465
|
+
*
|
|
466
|
+
* **Role.** The provider-facing bridge between the vendor-neutral
|
|
467
|
+
* `@warlock.js/ai` agent runtime and the official `openai` SDK. Agents,
|
|
468
|
+
* workflows, and supervisors never talk to OpenAI directly — they hold a
|
|
469
|
+
* `ModelContract`, and this class is what makes that contract concrete for
|
|
470
|
+
* any OpenAI-compatible endpoint (OpenAI, Azure OpenAI, OpenRouter, local
|
|
471
|
+
* gateways that speak the Chat Completions protocol).
|
|
472
|
+
*
|
|
473
|
+
* **Responsibility.**
|
|
474
|
+
* - Owns: a long-lived `OpenAI` client + frozen `ModelConfig` (name,
|
|
475
|
+
* temperature, maxTokens) used as defaults for every call.
|
|
476
|
+
* - Owns: translating vendor-neutral `Message[]` and
|
|
477
|
+
* `ToolContract[]` into OpenAI wire shapes on the way out, and
|
|
478
|
+
* translating OpenAI's response (content, finish reason, tool calls,
|
|
479
|
+
* usage) back into the neutral shapes on the way in.
|
|
480
|
+
* - Does NOT own: dispatching tools, deciding whether to loop, tracking
|
|
481
|
+
* conversation history, or retrying on failure — those are agent
|
|
482
|
+
* concerns. The model is a stateless (per-call) protocol adapter.
|
|
483
|
+
*
|
|
484
|
+
* Because it holds a live client and shared defaults, it is modeled as a
|
|
485
|
+
* class (see §4.2 of code-style.md — "long-lived state across calls").
|
|
486
|
+
*
|
|
487
|
+
* @example
|
|
488
|
+
* import OpenAI from "openai";
|
|
489
|
+
* const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
490
|
+
* const model = new OpenAIModel(client, { name: "gpt-4o", temperature: 0.3 });
|
|
491
|
+
*
|
|
492
|
+
* const myAgent = agent({
|
|
493
|
+
* model,
|
|
494
|
+
* systemPrompt: "You are a helpful assistant.",
|
|
495
|
+
* tools: [searchTool],
|
|
496
|
+
* });
|
|
497
|
+
*
|
|
498
|
+
* const result = await myAgent.execute("Summarize today's news.");
|
|
499
|
+
*/
|
|
500
|
+
var OpenAIModel = class {
|
|
501
|
+
constructor(client, config, provider = "openai") {
|
|
502
|
+
this.logger = _warlock_js_logger.log;
|
|
503
|
+
this.client = client;
|
|
504
|
+
this.config = config;
|
|
505
|
+
this.name = config.name;
|
|
506
|
+
this.provider = provider;
|
|
507
|
+
this.pricing = config.pricing;
|
|
508
|
+
this.capabilities = {
|
|
509
|
+
structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),
|
|
510
|
+
vision: config.vision ?? inferVisionCapability(config.name)
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Single-shot completion. Sends the full message list to the Chat
|
|
515
|
+
* Completions endpoint, waits for the terminal response, and reshapes it
|
|
516
|
+
* into a vendor-neutral `ModelResponse`. Per-call `options` override the
|
|
517
|
+
* instance's `ModelConfig` defaults for this call only.
|
|
518
|
+
*/
|
|
519
|
+
async complete(messages, options) {
|
|
520
|
+
this.logger.debug(LOG_MODULE, "request", "Starting call to chat.completions", {
|
|
521
|
+
model: this.name,
|
|
522
|
+
messageCount: messages.length,
|
|
523
|
+
streaming: false,
|
|
524
|
+
toolCount: options?.tools?.length ?? 0
|
|
525
|
+
});
|
|
526
|
+
let response;
|
|
527
|
+
try {
|
|
528
|
+
response = await this.client.chat.completions.create({
|
|
529
|
+
model: this.name,
|
|
530
|
+
messages: toOpenAIMessages(messages),
|
|
531
|
+
temperature: options?.temperature ?? this.config.temperature,
|
|
532
|
+
max_tokens: options?.maxTokens ?? this.config.maxTokens,
|
|
533
|
+
tools: toOpenAITools(options?.tools),
|
|
534
|
+
...this.buildResponseFormat(options?.responseSchema)
|
|
535
|
+
}, options?.signal ? { signal: options.signal } : void 0);
|
|
536
|
+
} catch (thrown) {
|
|
537
|
+
const wrapped = wrapOpenAIError(thrown);
|
|
538
|
+
this.logger.error(LOG_MODULE, "error", wrapped.message, {
|
|
539
|
+
code: wrapped.code,
|
|
540
|
+
context: wrapped.context
|
|
541
|
+
});
|
|
542
|
+
throw wrapped;
|
|
543
|
+
}
|
|
544
|
+
const choice = response.choices[0];
|
|
545
|
+
const finishReason = mapFinishReason(choice.finish_reason);
|
|
546
|
+
const usage = this.extractUsage(response.usage);
|
|
547
|
+
this.logger.debug(LOG_MODULE, "response", "call to chat.completions succeeded", {
|
|
548
|
+
finishReason,
|
|
549
|
+
usage
|
|
550
|
+
});
|
|
551
|
+
return {
|
|
552
|
+
content: choice.message.content ?? "",
|
|
553
|
+
finishReason,
|
|
554
|
+
usage,
|
|
555
|
+
toolCalls: this.extractToolCalls(choice.message.tool_calls)
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Incremental streaming completion. Yields neutral `ModelStreamChunk`s —
|
|
560
|
+
* `delta` for text tokens, `tool-call` when the model requests a tool,
|
|
561
|
+
* and a terminal `done` carrying the final finish reason + usage totals.
|
|
562
|
+
* Callers consume it with `for await`.
|
|
563
|
+
*/
|
|
564
|
+
async *stream(messages, options) {
|
|
565
|
+
this.logger.debug(LOG_MODULE, "request", "Starting streaming call to chat.completions", {
|
|
566
|
+
model: this.name,
|
|
567
|
+
messageCount: messages.length,
|
|
568
|
+
streaming: true,
|
|
569
|
+
toolCount: options?.tools?.length ?? 0
|
|
570
|
+
});
|
|
571
|
+
let stream;
|
|
572
|
+
try {
|
|
573
|
+
stream = await this.client.chat.completions.create({
|
|
574
|
+
model: this.name,
|
|
575
|
+
messages: toOpenAIMessages(messages),
|
|
576
|
+
temperature: options?.temperature ?? this.config.temperature,
|
|
577
|
+
max_tokens: options?.maxTokens ?? this.config.maxTokens,
|
|
578
|
+
tools: toOpenAITools(options?.tools),
|
|
579
|
+
stream: true,
|
|
580
|
+
stream_options: { include_usage: true },
|
|
581
|
+
...this.buildResponseFormat(options?.responseSchema)
|
|
582
|
+
}, options?.signal ? { signal: options.signal } : void 0);
|
|
583
|
+
} catch (thrown) {
|
|
584
|
+
const wrapped = wrapOpenAIError(thrown);
|
|
585
|
+
this.logger.error(LOG_MODULE, "error", wrapped.message, {
|
|
586
|
+
code: wrapped.code,
|
|
587
|
+
context: wrapped.context
|
|
588
|
+
});
|
|
589
|
+
throw wrapped;
|
|
590
|
+
}
|
|
591
|
+
let rawFinishReason = "stop";
|
|
592
|
+
const usage = {
|
|
593
|
+
input: 0,
|
|
594
|
+
output: 0,
|
|
595
|
+
total: 0
|
|
596
|
+
};
|
|
597
|
+
const toolCallAccum = /* @__PURE__ */ new Map();
|
|
598
|
+
try {
|
|
599
|
+
for await (const chunk of stream) {
|
|
600
|
+
const delta = chunk.choices[0]?.delta;
|
|
601
|
+
const finish = chunk.choices[0]?.finish_reason;
|
|
602
|
+
if (delta?.content) yield {
|
|
603
|
+
type: "delta",
|
|
604
|
+
content: delta.content
|
|
605
|
+
};
|
|
606
|
+
if (delta?.tool_calls) for (const toolCall of delta.tool_calls) {
|
|
607
|
+
const idx = toolCall.index ?? 0;
|
|
608
|
+
if (!toolCallAccum.has(idx)) toolCallAccum.set(idx, {
|
|
609
|
+
id: "",
|
|
610
|
+
name: "",
|
|
611
|
+
arguments: ""
|
|
612
|
+
});
|
|
613
|
+
const acc = toolCallAccum.get(idx);
|
|
614
|
+
if (toolCall.id) acc.id = toolCall.id;
|
|
615
|
+
if (toolCall.function?.name) acc.name = toolCall.function.name;
|
|
616
|
+
if (toolCall.function?.arguments) acc.arguments += toolCall.function.arguments;
|
|
617
|
+
}
|
|
618
|
+
if (finish) rawFinishReason = finish;
|
|
619
|
+
if (chunk.usage) {
|
|
620
|
+
usage.input = chunk.usage.prompt_tokens ?? 0;
|
|
621
|
+
usage.output = chunk.usage.completion_tokens ?? 0;
|
|
622
|
+
usage.total = chunk.usage.total_tokens ?? 0;
|
|
623
|
+
const cached = chunk.usage.prompt_tokens_details?.cached_tokens;
|
|
624
|
+
if (cached !== void 0 && cached > 0) usage.cachedTokens = cached;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
for (const acc of toolCallAccum.values()) {
|
|
628
|
+
if (!acc.name) continue;
|
|
629
|
+
yield {
|
|
630
|
+
type: "tool-call",
|
|
631
|
+
id: acc.id,
|
|
632
|
+
name: acc.name,
|
|
633
|
+
input: (0, _warlock_js_ai.safeJsonParse)(acc.arguments, {})
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
} catch (thrown) {
|
|
637
|
+
const wrapped = wrapOpenAIError(thrown);
|
|
638
|
+
this.logger.error(LOG_MODULE, "error", wrapped.message, {
|
|
639
|
+
code: wrapped.code,
|
|
640
|
+
context: wrapped.context
|
|
641
|
+
});
|
|
642
|
+
throw wrapped;
|
|
643
|
+
}
|
|
644
|
+
const finishReason = mapFinishReason(rawFinishReason);
|
|
645
|
+
this.logger.debug(LOG_MODULE, "response", "Streaming call to chat.completions succeeded", {
|
|
646
|
+
finishReason,
|
|
647
|
+
usage
|
|
648
|
+
});
|
|
649
|
+
yield {
|
|
650
|
+
type: "done",
|
|
651
|
+
finishReason,
|
|
652
|
+
usage
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Translate the neutral `responseSchema` option into OpenAI's
|
|
657
|
+
* `response_format` parameter.
|
|
658
|
+
*
|
|
659
|
+
* When `config.responseFormat` is set, it wins: `"text"` emits no
|
|
660
|
+
* `response_format` at all, `"json_object"` always picks the loose
|
|
661
|
+
* mode, and `"json_schema"` picks strict mode (with the same
|
|
662
|
+
* `isStrictCompatible` safety check — a malformed schema still
|
|
663
|
+
* degrades to `json_object` rather than 400). The override exists
|
|
664
|
+
* because some targets (older OpenAI models, OpenRouter routes,
|
|
665
|
+
* Ollama OpenAI-compat) reject strict `json_schema` outright.
|
|
666
|
+
*
|
|
667
|
+
* When the override is omitted, uses strict `json_schema` mode
|
|
668
|
+
* (token-level enforcement) only when the schema is a proper
|
|
669
|
+
* root-object JSON Schema (`{ type: "object", properties: ... }`).
|
|
670
|
+
* For anything else — malformed extractor output, non-object
|
|
671
|
+
* schemas, or future shapes we haven't tested — falls back to loose
|
|
672
|
+
* `json_object` mode, which guarantees *some* valid JSON without
|
|
673
|
+
* enforcing shape. The agent's soft instruction already embeds the
|
|
674
|
+
* schema text in the system prompt when the model declares no
|
|
675
|
+
* native structured-output capability, so shape validation still
|
|
676
|
+
* runs client-side via the Standard Schema `validate()` call.
|
|
677
|
+
*
|
|
678
|
+
* Returns an empty spread when no schema was supplied, so the caller
|
|
679
|
+
* can unconditionally `...buildResponseFormat(...)` into the request.
|
|
680
|
+
*/
|
|
681
|
+
buildResponseFormat(responseSchema) {
|
|
682
|
+
if (!responseSchema) return {};
|
|
683
|
+
const override = this.config.responseFormat;
|
|
684
|
+
if (override === "text") return {};
|
|
685
|
+
if (override === "json_object") return { response_format: { type: "json_object" } };
|
|
686
|
+
if (this.isStrictCompatible(responseSchema)) return { response_format: {
|
|
687
|
+
type: "json_schema",
|
|
688
|
+
json_schema: {
|
|
689
|
+
name: "response",
|
|
690
|
+
schema: responseSchema,
|
|
691
|
+
strict: true
|
|
692
|
+
}
|
|
693
|
+
} };
|
|
694
|
+
return { response_format: { type: "json_object" } };
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* OpenAI strict `json_schema` mode requires the root to be a JSON
|
|
698
|
+
* Schema object type (`{ type: "object", properties: ... }`). Anything
|
|
699
|
+
* else (top-level arrays, primitives, unknown shapes) is rejected with
|
|
700
|
+
* a 400 before a token is sampled. We check structurally here so the
|
|
701
|
+
* first call doesn't crash on a malformed extraction — loose
|
|
702
|
+
* `json_object` mode is a safe degradation.
|
|
703
|
+
*/
|
|
704
|
+
isStrictCompatible(schema) {
|
|
705
|
+
return schema.type === "object" && typeof schema.properties === "object" && schema.properties !== null;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Normalize OpenAI's `usage` block (which may be absent on some responses
|
|
709
|
+
* or partials) into the neutral `Usage` shape. Missing usage collapses to
|
|
710
|
+
* zeros rather than propagating `undefined`, so downstream aggregation
|
|
711
|
+
* math stays safe.
|
|
712
|
+
*/
|
|
713
|
+
extractUsage(raw) {
|
|
714
|
+
if (!raw) return {
|
|
715
|
+
input: 0,
|
|
716
|
+
output: 0,
|
|
717
|
+
total: 0
|
|
718
|
+
};
|
|
719
|
+
const cachedTokens = raw.prompt_tokens_details?.cached_tokens;
|
|
720
|
+
return {
|
|
721
|
+
input: raw.prompt_tokens,
|
|
722
|
+
output: raw.completion_tokens,
|
|
723
|
+
total: raw.total_tokens,
|
|
724
|
+
...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {}
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Reshape OpenAI's `tool_calls` array into the neutral
|
|
729
|
+
* `ModelToolCallRequest[]`. The raw `arguments` field is a JSON string
|
|
730
|
+
* per OpenAI's protocol — we parse it defensively via `safeJsonParse` so
|
|
731
|
+
* malformed or empty arguments yield an empty object instead of crashing
|
|
732
|
+
* the trip. Returns `undefined` when no tools were requested so callers
|
|
733
|
+
* can branch on presence.
|
|
734
|
+
*/
|
|
735
|
+
extractToolCalls(rawToolCalls) {
|
|
736
|
+
if (!rawToolCalls || rawToolCalls.length === 0) return;
|
|
737
|
+
return rawToolCalls.map((toolCall) => ({
|
|
738
|
+
id: toolCall.id,
|
|
739
|
+
name: toolCall.function.name,
|
|
740
|
+
input: (0, _warlock_js_ai.safeJsonParse)(toolCall.function.arguments, {})
|
|
741
|
+
}));
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
//#endregion
|
|
746
|
+
//#region ../../@warlock.js/ai-openai/src/sdk.ts
|
|
747
|
+
/**
|
|
748
|
+
* OpenAI-backed implementation of `SDKAdapterContract`.
|
|
749
|
+
*
|
|
750
|
+
* **Role.** The package entry point for any OpenAI-compatible provider
|
|
751
|
+
* (OpenAI, Azure OpenAI, OpenRouter, local gateways speaking the Chat
|
|
752
|
+
* Completions protocol). A single `OpenAISDK` instance holds one live
|
|
753
|
+
* `OpenAI` client, shared by every `ModelContract` it produces via
|
|
754
|
+
* `model()`. Users construct one SDK per provider/account and reuse it
|
|
755
|
+
* across all agents, workflows, and supervisors that target that
|
|
756
|
+
* provider.
|
|
757
|
+
*
|
|
758
|
+
* **Responsibility.**
|
|
759
|
+
* - Owns: a long-lived `OpenAI` client (authentication, base URL) and
|
|
760
|
+
* its lifetime scope. Factory for `OpenAIModel` instances — each
|
|
761
|
+
* model call gets a reference to the same client.
|
|
762
|
+
* - Does NOT own: anything per-call (tool execution, message history,
|
|
763
|
+
* streaming loop) — those live in `OpenAIModel` and the agent runtime.
|
|
764
|
+
*
|
|
765
|
+
* Modeled as a class (see §4.2 of code-style.md — "long-lived state
|
|
766
|
+
* across many calls"): the `OpenAI` client is heavy to construct and
|
|
767
|
+
* designed to be reused; keeping it on `this` makes that reuse
|
|
768
|
+
* explicit and aligns with the PascalCase naming convention readers
|
|
769
|
+
* expect from a constructor.
|
|
770
|
+
*
|
|
771
|
+
* @example
|
|
772
|
+
* const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });
|
|
773
|
+
* const model = openai.model({ name: "gpt-4o", temperature: 0.7 });
|
|
774
|
+
* const tokens = await openai.count("Hello world");
|
|
775
|
+
*
|
|
776
|
+
* @example
|
|
777
|
+
* // Compose into an `ai.openai` namespace for ergonomic agent wiring
|
|
778
|
+
* const ai = { agent, tool, systemPrompt, persona, instruction, openai: new OpenAISDK({ apiKey }) };
|
|
779
|
+
* const myAgent = ai.agent({ model: ai.openai.model({ name: "gpt-4o-mini" }) });
|
|
780
|
+
*/
|
|
781
|
+
var OpenAISDK = class {
|
|
782
|
+
constructor(config) {
|
|
783
|
+
this.client = new openai.default({
|
|
784
|
+
apiKey: config.apiKey,
|
|
785
|
+
baseURL: config.baseURL
|
|
786
|
+
});
|
|
787
|
+
this.provider = config.provider ?? "openai";
|
|
788
|
+
this.pricing = config.pricing;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Build an `OpenAIModel` bound to this SDK's client. Each call returns
|
|
792
|
+
* a fresh model instance, but all instances share the underlying
|
|
793
|
+
* `OpenAI` client — connection pools, rate limits, and authentication
|
|
794
|
+
* state stay unified across every model produced here. The SDK's
|
|
795
|
+
* `provider` label is forwarded so every model self-identifies as
|
|
796
|
+
* coming from the same upstream.
|
|
797
|
+
*
|
|
798
|
+
* Pricing resolution: per-model `config.pricing` wins; otherwise the
|
|
799
|
+
* SDK-level registry entry keyed by `config.name`; otherwise
|
|
800
|
+
* `undefined` (no cost computed).
|
|
801
|
+
*/
|
|
802
|
+
model(config) {
|
|
803
|
+
const resolvedPricing = config.pricing ?? this.pricing?.[config.name];
|
|
804
|
+
const resolvedConfig = resolvedPricing === config.pricing ? config : {
|
|
805
|
+
...config,
|
|
806
|
+
pricing: resolvedPricing
|
|
807
|
+
};
|
|
808
|
+
return new OpenAIModel(this.client, resolvedConfig, this.provider);
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Rough token-count estimate for a given text. Uses a
|
|
812
|
+
* character-heuristic (`approximateTokenCount`) from the core package
|
|
813
|
+
* — good enough for budgeting and quota guards, not for billing.
|
|
814
|
+
* Accepts an optional model id for future per-model tokenizer
|
|
815
|
+
* dispatch; currently ignored.
|
|
816
|
+
*/
|
|
817
|
+
async count(text, _model) {
|
|
818
|
+
return (0, _warlock_js_ai.approximateTokenCount)(text);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Build an `OpenAIEmbedder` bound to this SDK's client. Each call
|
|
822
|
+
* returns a fresh embedder instance sharing the same underlying
|
|
823
|
+
* `OpenAI` client — connection pools and authentication stay unified
|
|
824
|
+
* across every embedder produced here.
|
|
825
|
+
*
|
|
826
|
+
* @example
|
|
827
|
+
* const embedder = openai.embedder({ name: "text-embedding-3-small" });
|
|
828
|
+
* const { vector } = await embedder.embed("Hello world");
|
|
829
|
+
*/
|
|
830
|
+
embedder(config) {
|
|
831
|
+
return new OpenAIEmbedder(this.client, config);
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
//#endregion
|
|
836
|
+
exports.OpenAIEmbedder = OpenAIEmbedder;
|
|
837
|
+
exports.OpenAISDK = OpenAISDK;
|
|
838
|
+
//# sourceMappingURL=index.cjs.map
|