@nymbalhq/ai-openai 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +9 -2
- package/dist/index.d.ts +37 -0
- package/dist/index.js +476 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nymbal contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nymbalhq/ai-openai
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OpenAI AI for Nymbal: the `openai` ai provider, for OpenAI and any OpenAI-compatible server.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx nymbal add ai openai
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`nymbal add` installs it, sets it in `nymbal.config.ts` and lists the variables it needs in `.env.example`.
|
|
10
|
+
See [Adapters](https://github.com/nymbalhq/nymbal/blob/main/apps/docs/src/content/docs/adapters.md).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Logger, AiAdapter, AdapterContext, AdapterSetting } from '@nymbalhq/types';
|
|
2
|
+
|
|
3
|
+
declare const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
4
|
+
/** The request field that bounds the answer's length. */
|
|
5
|
+
type TokenField = 'max_tokens' | 'max_completion_tokens';
|
|
6
|
+
interface OpenAiAdapterConfig {
|
|
7
|
+
apiKey: string;
|
|
8
|
+
/** The model the server serves, sent with every request. */
|
|
9
|
+
model: string;
|
|
10
|
+
/** Where the chat completions API is, its path prefix included; OpenAI's own when omitted. */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
/**
|
|
13
|
+
* `max_completion_tokens` on OpenAI's own host, whose newer models refuse `max_tokens`; `max_tokens` on any other
|
|
14
|
+
* (a compatible server may know only that one) when omitted.
|
|
15
|
+
*/
|
|
16
|
+
tokenField?: TokenField;
|
|
17
|
+
/** How long one chat() waits for its answer, or one chatStream() for its end; 60 000 when omitted. */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
logger: Logger;
|
|
20
|
+
}
|
|
21
|
+
declare function createOpenAiAdapter(config: OpenAiAdapterConfig): AiAdapter;
|
|
22
|
+
|
|
23
|
+
/** What this package serves: the kernel refuses it on any other slot or provider. */
|
|
24
|
+
declare const slot = "ai";
|
|
25
|
+
declare const provider = "openai";
|
|
26
|
+
|
|
27
|
+
/** The fields the admin's Settings may fill when the config (and, for the key, OPENAI_API_KEY) leaves them unset. */
|
|
28
|
+
declare const settings: AdapterSetting[];
|
|
29
|
+
/**
|
|
30
|
+
* The kernel's entry: `ai: { provider: 'openai', config: { model, apiKey?, baseUrl?, tokenField?, timeoutMs? } }`, the
|
|
31
|
+
* key falling back to OPENAI_API_KEY. A `baseUrl` reaches any OpenAI-compatible server (a gateway, Ollama, vLLM);
|
|
32
|
+
* `tokenField` names the length field for one that takes the other.
|
|
33
|
+
*/
|
|
34
|
+
declare function createAdapter(ctx: AdapterContext): AiAdapter;
|
|
35
|
+
|
|
36
|
+
export { DEFAULT_BASE_URL, createAdapter, createOpenAiAdapter, provider, settings, slot };
|
|
37
|
+
export type { OpenAiAdapterConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
ConfigError as ConfigError2
|
|
4
|
+
} from "@nymbalhq/types";
|
|
5
|
+
|
|
6
|
+
// src/chat.ts
|
|
7
|
+
import {
|
|
8
|
+
AdapterError,
|
|
9
|
+
ConfigError
|
|
10
|
+
} from "@nymbalhq/types";
|
|
11
|
+
var DEFAULT_MAX_TOKENS = 1024;
|
|
12
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
13
|
+
var TOOL_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
14
|
+
function timeoutOf(value) {
|
|
15
|
+
if (value === void 0) return DEFAULT_TIMEOUT_MS;
|
|
16
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
17
|
+
throw new ConfigError("commerce.ai.config.timeoutMs is not a positive number of milliseconds", {
|
|
18
|
+
context: { slot: "ai" }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
var recOf = (value) => isRecord(value) ? value : {};
|
|
25
|
+
function checkChat(provider2, messages, opts) {
|
|
26
|
+
const refuse = (why) => new AdapterError("invalid_request", `${provider2} chat refused: ${why}`, {
|
|
27
|
+
context: { provider: provider2 }
|
|
28
|
+
});
|
|
29
|
+
if (!Array.isArray(messages) || messages.length === 0) throw refuse("no messages");
|
|
30
|
+
let open = [];
|
|
31
|
+
for (const [at, message] of messages.entries()) {
|
|
32
|
+
const turn = recOf(message);
|
|
33
|
+
const role = turn["role"];
|
|
34
|
+
if (role !== "user" && role !== "assistant" && role !== "tool") {
|
|
35
|
+
throw refuse(`messages[${at}].role is not user, assistant or tool`);
|
|
36
|
+
}
|
|
37
|
+
if (at === 0 && role !== "user") throw refuse("the first message is not the user\u2019s");
|
|
38
|
+
const content = turn["content"];
|
|
39
|
+
if (typeof content !== "string") throw refuse(`messages[${at}].content is not text`);
|
|
40
|
+
if (role === "tool") {
|
|
41
|
+
const id = turn["toolCallId"];
|
|
42
|
+
if (typeof id !== "string" || !open.includes(id)) {
|
|
43
|
+
throw refuse(`messages[${at}] answers no open call of the assistant turn before it`);
|
|
44
|
+
}
|
|
45
|
+
if (content === "") throw refuse(`messages[${at}].content is empty`);
|
|
46
|
+
open = open.filter((o) => o !== id);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (open.length > 0) throw refuse(`messages[${at}] starts with a call left unanswered`);
|
|
50
|
+
const calls = role === "assistant" ? callIdsOf(turn["toolCalls"], at, refuse) : [];
|
|
51
|
+
if (content === "" && calls.length === 0) throw refuse(`messages[${at}].content is empty`);
|
|
52
|
+
open = calls;
|
|
53
|
+
}
|
|
54
|
+
checkTools(opts?.tools, refuse);
|
|
55
|
+
const maxTokens = opts?.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
56
|
+
if (!Number.isInteger(maxTokens) || maxTokens <= 0) {
|
|
57
|
+
throw refuse("maxTokens is not a positive integer");
|
|
58
|
+
}
|
|
59
|
+
return maxTokens;
|
|
60
|
+
}
|
|
61
|
+
function callIdsOf(value, at, refuse) {
|
|
62
|
+
if (value === void 0) return [];
|
|
63
|
+
if (!Array.isArray(value)) throw refuse(`messages[${at}].toolCalls is not a list`);
|
|
64
|
+
const ids = [];
|
|
65
|
+
for (const [n, call] of value.entries()) {
|
|
66
|
+
const c = recOf(call);
|
|
67
|
+
const id = c["id"];
|
|
68
|
+
if (typeof id !== "string" || id === "" || ids.includes(id)) {
|
|
69
|
+
throw refuse(`messages[${at}].toolCalls[${n}].id is empty or repeated`);
|
|
70
|
+
}
|
|
71
|
+
if (typeof c["name"] !== "string" || !TOOL_NAME.test(c["name"])) {
|
|
72
|
+
throw refuse(`messages[${at}].toolCalls[${n}].name is outside ${TOOL_NAME.source}`);
|
|
73
|
+
}
|
|
74
|
+
if (!isRecord(c["args"])) throw refuse(`messages[${at}].toolCalls[${n}].args is not an object`);
|
|
75
|
+
ids.push(id);
|
|
76
|
+
}
|
|
77
|
+
return ids;
|
|
78
|
+
}
|
|
79
|
+
function checkTools(value, refuse) {
|
|
80
|
+
if (value === void 0) return;
|
|
81
|
+
if (!Array.isArray(value)) throw refuse("tools is not a list");
|
|
82
|
+
const names = /* @__PURE__ */ new Set();
|
|
83
|
+
for (const [at, tool] of value.entries()) {
|
|
84
|
+
const t = recOf(tool);
|
|
85
|
+
const name = t["name"];
|
|
86
|
+
if (typeof name !== "string" || !TOOL_NAME.test(name)) {
|
|
87
|
+
throw refuse(`tools[${at}].name is outside ${TOOL_NAME.source}`);
|
|
88
|
+
}
|
|
89
|
+
if (names.has(name)) throw refuse(`tools[${at}].name is taken by an earlier tool`);
|
|
90
|
+
names.add(name);
|
|
91
|
+
if (typeof t["description"] !== "string") throw refuse(`tools[${at}].description is not text`);
|
|
92
|
+
if (!isRecord(t["inputSchema"])) throw refuse(`tools[${at}].inputSchema is not an object`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function callOf(id, name, args) {
|
|
96
|
+
if (typeof id !== "string" || id === "" || typeof name !== "string" || name === "")
|
|
97
|
+
return void 0;
|
|
98
|
+
return isRecord(args) ? { id, name, args } : void 0;
|
|
99
|
+
}
|
|
100
|
+
var timedOut = (provider2, timeoutMs) => new AdapterError("timeout", `${provider2} gave no answer within ${timeoutMs} ms`, {
|
|
101
|
+
context: { provider: provider2 }
|
|
102
|
+
});
|
|
103
|
+
var cancelled = (provider2) => new AdapterError("cancelled", `${provider2} request cancelled by the caller`, {
|
|
104
|
+
context: { provider: provider2 }
|
|
105
|
+
});
|
|
106
|
+
var unreachable = (provider2) => new AdapterError("unavailable", `${provider2} could not be reached`, { context: { provider: provider2 } });
|
|
107
|
+
var brokenOff = (provider2) => new AdapterError("unavailable", `${provider2} broke off its answer`, { context: { provider: provider2 } });
|
|
108
|
+
var unreadable = (provider2, status, what) => new AdapterError("unexpected_response", `${provider2} answered ${status} ${what}`, {
|
|
109
|
+
context: { provider: provider2, status }
|
|
110
|
+
});
|
|
111
|
+
function codeOf(status) {
|
|
112
|
+
if (status === 401 || status === 403) return "auth_failed";
|
|
113
|
+
if (status === 429) return "rate_limited";
|
|
114
|
+
if (status >= 400 && status < 500) return "invalid_request";
|
|
115
|
+
return "unavailable";
|
|
116
|
+
}
|
|
117
|
+
var answered = (provider2, status) => new AdapterError(codeOf(status), `${provider2} answered ${status}`, {
|
|
118
|
+
context: { provider: provider2, status }
|
|
119
|
+
});
|
|
120
|
+
function jsonOf(text) {
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(text);
|
|
123
|
+
} catch {
|
|
124
|
+
return void 0;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function lifetime(provider2, timeoutMs, caller) {
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
let reason;
|
|
130
|
+
const end = (error) => {
|
|
131
|
+
if (reason !== void 0) return;
|
|
132
|
+
reason = error;
|
|
133
|
+
controller.abort();
|
|
134
|
+
};
|
|
135
|
+
const timer = setTimeout(() => end(timedOut(provider2, timeoutMs)), timeoutMs);
|
|
136
|
+
const onAbort = () => end(cancelled(provider2));
|
|
137
|
+
if (caller?.aborted === true) onAbort();
|
|
138
|
+
else caller?.addEventListener("abort", onAbort, { once: true });
|
|
139
|
+
return {
|
|
140
|
+
signal: controller.signal,
|
|
141
|
+
ended: () => reason,
|
|
142
|
+
close: () => {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
caller?.removeEventListener("abort", onAbort);
|
|
145
|
+
controller.abort();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function post(provider2, url, headers, body, life) {
|
|
150
|
+
let response;
|
|
151
|
+
try {
|
|
152
|
+
response = await globalThis.fetch(url, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: { ...headers, "content-type": "application/json" },
|
|
155
|
+
body,
|
|
156
|
+
signal: life.signal
|
|
157
|
+
});
|
|
158
|
+
} catch {
|
|
159
|
+
throw life.ended() ?? unreachable(provider2);
|
|
160
|
+
}
|
|
161
|
+
if (response.status < 200 || response.status > 299) throw answered(provider2, response.status);
|
|
162
|
+
return response;
|
|
163
|
+
}
|
|
164
|
+
async function bodyText(provider2, response, life) {
|
|
165
|
+
try {
|
|
166
|
+
return await response.text();
|
|
167
|
+
} catch {
|
|
168
|
+
throw life.ended() ?? brokenOff(provider2);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/openai-adapter.ts
|
|
173
|
+
import { AdapterError as AdapterError2 } from "@nymbalhq/types";
|
|
174
|
+
|
|
175
|
+
// src/sse.ts
|
|
176
|
+
async function* sseEvents(body) {
|
|
177
|
+
const reader = body.getReader();
|
|
178
|
+
const decoder = new TextDecoder();
|
|
179
|
+
let buffer = "";
|
|
180
|
+
let event = "";
|
|
181
|
+
let data = [];
|
|
182
|
+
try {
|
|
183
|
+
for (; ; ) {
|
|
184
|
+
const { done, value } = await reader.read();
|
|
185
|
+
let text = buffer + (done ? decoder.decode() : decoder.decode(value, { stream: true }));
|
|
186
|
+
const held = !done && text.endsWith("\r") ? "\r" : "";
|
|
187
|
+
if (held !== "") text = text.slice(0, -1);
|
|
188
|
+
const lines = text.split(/\r\n|\r|\n/);
|
|
189
|
+
buffer = (lines.pop() ?? "") + held;
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
if (line === "") {
|
|
192
|
+
if (data.length > 0)
|
|
193
|
+
yield { event: event === "" ? "message" : event, data: data.join("\n") };
|
|
194
|
+
event = "";
|
|
195
|
+
data = [];
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (line.startsWith(":")) continue;
|
|
199
|
+
const colon = line.indexOf(":");
|
|
200
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
201
|
+
const raw = colon === -1 ? "" : line.slice(colon + 1);
|
|
202
|
+
const value2 = raw.startsWith(" ") ? raw.slice(1) : raw;
|
|
203
|
+
if (field === "event") event = value2;
|
|
204
|
+
else if (field === "data") data.push(value2);
|
|
205
|
+
}
|
|
206
|
+
if (done) return;
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
reader.releaseLock();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/openai-adapter.ts
|
|
214
|
+
var DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
215
|
+
var OPENAI_HOST = "api.openai.com";
|
|
216
|
+
var PROVIDER = "openai";
|
|
217
|
+
function stopReasonOf(wire) {
|
|
218
|
+
if (wire === "stop") return "end";
|
|
219
|
+
if (wire === "length") return "max_tokens";
|
|
220
|
+
if (wire === "tool_calls") return "tool_use";
|
|
221
|
+
return "other";
|
|
222
|
+
}
|
|
223
|
+
var count = (value) => typeof value === "number" ? value : 0;
|
|
224
|
+
var hostOf = (url) => URL.canParse(url) ? new URL(url).hostname : "";
|
|
225
|
+
function wireMessages(messages) {
|
|
226
|
+
return messages.map((m) => {
|
|
227
|
+
if (m.role === "tool") return { role: "tool", tool_call_id: m.toolCallId, content: m.content };
|
|
228
|
+
const calls = m.role === "assistant" ? m.toolCalls ?? [] : [];
|
|
229
|
+
if (calls.length === 0) return { role: m.role, content: m.content };
|
|
230
|
+
return {
|
|
231
|
+
role: m.role,
|
|
232
|
+
content: m.content === "" ? null : m.content,
|
|
233
|
+
tool_calls: calls.map((c) => ({
|
|
234
|
+
id: c.id,
|
|
235
|
+
type: "function",
|
|
236
|
+
function: { name: c.name, arguments: JSON.stringify(c.args) }
|
|
237
|
+
}))
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function wireCallOf(id, name, args) {
|
|
242
|
+
return callOf(id, name, typeof args === "string" ? jsonOf(args) : void 0);
|
|
243
|
+
}
|
|
244
|
+
function createOpenAiAdapter(config) {
|
|
245
|
+
const base = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
246
|
+
const url = `${base.endsWith("/") ? base.slice(0, -1) : base}/chat/completions`;
|
|
247
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
248
|
+
const ownHost = hostOf(base) === OPENAI_HOST;
|
|
249
|
+
const tokenField = config.tokenField ?? (ownHost ? "max_completion_tokens" : "max_tokens");
|
|
250
|
+
const headers = { authorization: `Bearer ${config.apiKey}` };
|
|
251
|
+
function requestOf(messages, opts, stream) {
|
|
252
|
+
const maxTokens = checkChat(PROVIDER, messages, opts);
|
|
253
|
+
const system = opts?.system;
|
|
254
|
+
const tools = opts?.tools ?? [];
|
|
255
|
+
return JSON.stringify({
|
|
256
|
+
model: config.model,
|
|
257
|
+
[tokenField]: maxTokens,
|
|
258
|
+
messages: [
|
|
259
|
+
...system !== void 0 && system !== "" ? [{ role: "system", content: system }] : [],
|
|
260
|
+
...wireMessages(messages)
|
|
261
|
+
],
|
|
262
|
+
...tools.length > 0 && {
|
|
263
|
+
tools: tools.map((t) => ({
|
|
264
|
+
type: "function",
|
|
265
|
+
function: { name: t.name, description: t.description, parameters: t.inputSchema }
|
|
266
|
+
}))
|
|
267
|
+
},
|
|
268
|
+
// Usage on a stream is OpenAI's own option; a compatible server may refuse it.
|
|
269
|
+
...stream && { stream: true, ...ownHost && { stream_options: { include_usage: true } } }
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function resultOf(status, text, toolCalls, finish, usage, reported) {
|
|
273
|
+
if (text === "" && toolCalls.length === 0) throw unreadable(PROVIDER, status, "with no text");
|
|
274
|
+
return {
|
|
275
|
+
text,
|
|
276
|
+
stopReason: stopReasonOf(finish),
|
|
277
|
+
usage,
|
|
278
|
+
model: typeof reported === "string" && reported !== "" ? reported : config.model,
|
|
279
|
+
toolCalls
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
async function* streamed(status, body, life) {
|
|
283
|
+
const events = sseEvents(body);
|
|
284
|
+
let reported;
|
|
285
|
+
let inputTokens = 0;
|
|
286
|
+
let outputTokens = 0;
|
|
287
|
+
let finish;
|
|
288
|
+
let text = "";
|
|
289
|
+
const toolCalls = [];
|
|
290
|
+
const building = /* @__PURE__ */ new Map();
|
|
291
|
+
try {
|
|
292
|
+
for (; ; ) {
|
|
293
|
+
let next;
|
|
294
|
+
try {
|
|
295
|
+
next = await events.next();
|
|
296
|
+
} catch {
|
|
297
|
+
throw life.ended() ?? brokenOff(PROVIDER);
|
|
298
|
+
}
|
|
299
|
+
if (next.done === true || next.value.data === "[DONE]") break;
|
|
300
|
+
const chunk = jsonOf(next.value.data);
|
|
301
|
+
if (!isRecord(chunk)) throw unreadable(PROVIDER, status, "with a chunk that is not JSON");
|
|
302
|
+
if (chunk["error"] !== void 0) throw brokenOff(PROVIDER);
|
|
303
|
+
if (chunk["model"] !== void 0) reported = chunk["model"];
|
|
304
|
+
if (isRecord(chunk["usage"])) {
|
|
305
|
+
inputTokens = count(chunk["usage"]["prompt_tokens"]);
|
|
306
|
+
outputTokens = count(chunk["usage"]["completion_tokens"]);
|
|
307
|
+
}
|
|
308
|
+
const choice = recOf(Array.isArray(chunk["choices"]) ? chunk["choices"][0] : void 0);
|
|
309
|
+
const delta = recOf(choice["delta"]);
|
|
310
|
+
const content = delta["content"];
|
|
311
|
+
if (typeof content === "string" && content !== "") {
|
|
312
|
+
text += content;
|
|
313
|
+
yield { type: "text", delta: content };
|
|
314
|
+
}
|
|
315
|
+
if (Array.isArray(delta["tool_calls"])) {
|
|
316
|
+
for (const piece of delta["tool_calls"].map(recOf)) {
|
|
317
|
+
const index = typeof piece["index"] === "number" ? piece["index"] : 0;
|
|
318
|
+
const fn = recOf(piece["function"]);
|
|
319
|
+
const call = building.get(index) ?? { id: void 0, name: void 0, json: "" };
|
|
320
|
+
if (piece["id"] !== void 0) call.id = piece["id"];
|
|
321
|
+
if (fn["name"] !== void 0) call.name = fn["name"];
|
|
322
|
+
if (typeof fn["arguments"] === "string") call.json += fn["arguments"];
|
|
323
|
+
building.set(index, call);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (choice["finish_reason"] !== void 0 && choice["finish_reason"] !== null) {
|
|
327
|
+
finish = choice["finish_reason"];
|
|
328
|
+
for (const index of [...building.keys()].sort((a, b) => a - b)) {
|
|
329
|
+
const built = building.get(index);
|
|
330
|
+
const call = wireCallOf(built.id, built.name, built.json);
|
|
331
|
+
if (call === void 0) {
|
|
332
|
+
throw unreadable(
|
|
333
|
+
PROVIDER,
|
|
334
|
+
status,
|
|
335
|
+
"with a tool call whose arguments are not an object"
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
toolCalls.push(call);
|
|
339
|
+
yield { type: "tool_call", call };
|
|
340
|
+
}
|
|
341
|
+
building.clear();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (finish === void 0) throw life.ended() ?? brokenOff(PROVIDER);
|
|
345
|
+
yield {
|
|
346
|
+
type: "done",
|
|
347
|
+
result: resultOf(status, text, toolCalls, finish, { inputTokens, outputTokens }, reported)
|
|
348
|
+
};
|
|
349
|
+
} finally {
|
|
350
|
+
await events.return(void 0);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const adapter = {
|
|
354
|
+
kind: "ai",
|
|
355
|
+
providerName: PROVIDER,
|
|
356
|
+
capabilities: () => ["complete", "chat", "tools", "stream"],
|
|
357
|
+
producesEvents: [],
|
|
358
|
+
consumesEvents: [],
|
|
359
|
+
async initialize() {
|
|
360
|
+
},
|
|
361
|
+
async healthCheck() {
|
|
362
|
+
return { status: "healthy", detail: `OpenAI AI adapter using model ${config.model}` };
|
|
363
|
+
},
|
|
364
|
+
async embed() {
|
|
365
|
+
throw new AdapterError2("not_configured", "Embedding not yet supported by the OpenAI adapter");
|
|
366
|
+
},
|
|
367
|
+
async complete(prompt, opts) {
|
|
368
|
+
const maxTokens = opts?.maxTokens;
|
|
369
|
+
const result = await adapter.chat(
|
|
370
|
+
[{ role: "user", content: prompt }],
|
|
371
|
+
maxTokens === void 0 ? {} : { maxTokens }
|
|
372
|
+
);
|
|
373
|
+
return result.text;
|
|
374
|
+
},
|
|
375
|
+
async chat(messages, opts) {
|
|
376
|
+
const request = requestOf(messages, opts, false);
|
|
377
|
+
const life = lifetime(PROVIDER, timeoutMs, opts?.signal);
|
|
378
|
+
let status;
|
|
379
|
+
let text;
|
|
380
|
+
try {
|
|
381
|
+
const response = await post(PROVIDER, url, headers, request, life);
|
|
382
|
+
status = response.status;
|
|
383
|
+
text = await bodyText(PROVIDER, response, life);
|
|
384
|
+
} finally {
|
|
385
|
+
life.close();
|
|
386
|
+
}
|
|
387
|
+
const body = jsonOf(text);
|
|
388
|
+
if (!isRecord(body)) throw unreadable(PROVIDER, status, "with a body that is not JSON");
|
|
389
|
+
const choice = recOf(Array.isArray(body["choices"]) ? body["choices"][0] : void 0);
|
|
390
|
+
const message = recOf(choice["message"]);
|
|
391
|
+
const toolCalls = [];
|
|
392
|
+
const wireCalls = Array.isArray(message["tool_calls"]) ? message["tool_calls"] : [];
|
|
393
|
+
for (const wire of wireCalls.map(recOf)) {
|
|
394
|
+
const fn = recOf(wire["function"]);
|
|
395
|
+
const call = wireCallOf(wire["id"], fn["name"], fn["arguments"]);
|
|
396
|
+
if (call === void 0) {
|
|
397
|
+
throw unreadable(PROVIDER, status, "with a tool call whose arguments are not an object");
|
|
398
|
+
}
|
|
399
|
+
toolCalls.push(call);
|
|
400
|
+
}
|
|
401
|
+
const usage = recOf(body["usage"]);
|
|
402
|
+
return resultOf(
|
|
403
|
+
status,
|
|
404
|
+
typeof message["content"] === "string" ? message["content"] : "",
|
|
405
|
+
toolCalls,
|
|
406
|
+
choice["finish_reason"],
|
|
407
|
+
{
|
|
408
|
+
inputTokens: count(usage["prompt_tokens"]),
|
|
409
|
+
outputTokens: count(usage["completion_tokens"])
|
|
410
|
+
},
|
|
411
|
+
body["model"]
|
|
412
|
+
);
|
|
413
|
+
},
|
|
414
|
+
async *chatStream(messages, opts) {
|
|
415
|
+
const request = requestOf(messages, opts, true);
|
|
416
|
+
const life = lifetime(PROVIDER, timeoutMs, opts?.signal);
|
|
417
|
+
try {
|
|
418
|
+
const response = await post(PROVIDER, url, headers, request, life);
|
|
419
|
+
if (response.body === null) throw unreadable(PROVIDER, response.status, "with no body");
|
|
420
|
+
yield* streamed(response.status, response.body, life);
|
|
421
|
+
} finally {
|
|
422
|
+
life.close();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
return adapter;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/index.ts
|
|
430
|
+
var slot = "ai";
|
|
431
|
+
var provider = "openai";
|
|
432
|
+
var settings = [
|
|
433
|
+
{ path: "apiKey", label: "API key", secret: true, env: "OPENAI_API_KEY" },
|
|
434
|
+
{ path: "baseUrl", label: "API base URL", secret: false }
|
|
435
|
+
];
|
|
436
|
+
function createAdapter(ctx) {
|
|
437
|
+
if (ctx.slot !== "ai") {
|
|
438
|
+
throw new ConfigError2(`@nymbalhq/ai-openai serves ai, not ${ctx.slot}`, {
|
|
439
|
+
code: "adapter.wrong_slot",
|
|
440
|
+
context: { slot: ctx.slot }
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
const apiKey = ctx.config["apiKey"];
|
|
444
|
+
const model = ctx.config["model"];
|
|
445
|
+
const baseUrl = ctx.config["baseUrl"];
|
|
446
|
+
if (typeof model !== "string" || model === "") {
|
|
447
|
+
throw new ConfigError2(
|
|
448
|
+
"commerce.ai.config.model is not set: name the model the provider serves",
|
|
449
|
+
{ context: { slot: ctx.slot } }
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
const tokenField = ctx.config["tokenField"];
|
|
453
|
+
if (tokenField !== void 0 && tokenField !== "max_tokens" && tokenField !== "max_completion_tokens") {
|
|
454
|
+
throw new ConfigError2(
|
|
455
|
+
"commerce.ai.config.tokenField is neither 'max_tokens' nor 'max_completion_tokens'",
|
|
456
|
+
{ context: { slot: ctx.slot } }
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
return createOpenAiAdapter({
|
|
460
|
+
apiKey: typeof apiKey === "string" && apiKey !== "" ? apiKey : ctx.env["OPENAI_API_KEY"] ?? "",
|
|
461
|
+
model,
|
|
462
|
+
...typeof baseUrl === "string" && baseUrl !== "" && { baseUrl },
|
|
463
|
+
...tokenField !== void 0 && { tokenField },
|
|
464
|
+
timeoutMs: timeoutOf(ctx.config["timeoutMs"]),
|
|
465
|
+
logger: ctx.logger
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
export {
|
|
469
|
+
DEFAULT_BASE_URL,
|
|
470
|
+
createAdapter,
|
|
471
|
+
createOpenAiAdapter,
|
|
472
|
+
provider,
|
|
473
|
+
settings,
|
|
474
|
+
slot
|
|
475
|
+
};
|
|
476
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/chat.ts","../src/openai-adapter.ts","../src/sse.ts"],"sourcesContent":["import {\n ConfigError,\n type AdapterContext,\n type AdapterSetting,\n type AiAdapter,\n} from '@nymbalhq/types'\nimport { timeoutOf } from './chat.js'\nimport {\n createOpenAiAdapter,\n DEFAULT_BASE_URL,\n type OpenAiAdapterConfig,\n} from './openai-adapter.js'\n\n/** What this package serves: the kernel refuses it on any other slot or provider. */\nexport const slot = 'ai'\nexport const provider = 'openai'\n\nexport { createOpenAiAdapter, DEFAULT_BASE_URL, type OpenAiAdapterConfig }\n\n/** The fields the admin's Settings may fill when the config (and, for the key, OPENAI_API_KEY) leaves them unset. */\nexport const settings: AdapterSetting[] = [\n { path: 'apiKey', label: 'API key', secret: true, env: 'OPENAI_API_KEY' },\n { path: 'baseUrl', label: 'API base URL', secret: false },\n]\n\n/**\n * The kernel's entry: `ai: { provider: 'openai', config: { model, apiKey?, baseUrl?, tokenField?, timeoutMs? } }`, the\n * key falling back to OPENAI_API_KEY. A `baseUrl` reaches any OpenAI-compatible server (a gateway, Ollama, vLLM);\n * `tokenField` names the length field for one that takes the other.\n */\nexport function createAdapter(ctx: AdapterContext): AiAdapter {\n if (ctx.slot !== 'ai') {\n throw new ConfigError(`@nymbalhq/ai-openai serves ai, not ${ctx.slot}`, {\n code: 'adapter.wrong_slot',\n context: { slot: ctx.slot },\n })\n }\n const apiKey = ctx.config['apiKey']\n const model = ctx.config['model']\n const baseUrl = ctx.config['baseUrl']\n if (typeof model !== 'string' || model === '') {\n throw new ConfigError(\n 'commerce.ai.config.model is not set: name the model the provider serves',\n { context: { slot: ctx.slot } },\n )\n }\n const tokenField = ctx.config['tokenField']\n if (\n tokenField !== undefined &&\n tokenField !== 'max_tokens' &&\n tokenField !== 'max_completion_tokens'\n ) {\n throw new ConfigError(\n \"commerce.ai.config.tokenField is neither 'max_tokens' nor 'max_completion_tokens'\",\n { context: { slot: ctx.slot } },\n )\n }\n return createOpenAiAdapter({\n apiKey:\n typeof apiKey === 'string' && apiKey !== '' ? apiKey : (ctx.env['OPENAI_API_KEY'] ?? ''),\n model,\n ...(typeof baseUrl === 'string' && baseUrl !== '' && { baseUrl }),\n ...(tokenField !== undefined && { tokenField }),\n timeoutMs: timeoutOf(ctx.config['timeoutMs']),\n logger: ctx.logger,\n })\n}\n","import {\n AdapterError,\n ConfigError,\n type ChatMessage,\n type ChatOptions,\n type ChatToolCall,\n} from '@nymbalhq/types'\n\n// What every ai provider's chat() does alike: the input it refuses before a request, the time it waits, and the\n// errors it throws. An error carries the provider and the HTTP status and nothing else: no cause, no headers, no\n// request and no provider message, which may echo the key or the conversation.\n\nexport const DEFAULT_MAX_TOKENS = 1024\nexport const DEFAULT_TIMEOUT_MS = 60_000\nconst TOOL_NAME = /^[A-Za-z0-9_-]{1,64}$/\n\n/** The block's `timeoutMs`, or the default when it is unset. */\nexport function timeoutOf(value: unknown): number {\n if (value === undefined) return DEFAULT_TIMEOUT_MS\n if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {\n throw new ConfigError('commerce.ai.config.timeoutMs is not a positive number of milliseconds', {\n context: { slot: 'ai' },\n })\n }\n return value\n}\n\ntype Rec = Record<string, unknown>\n\nexport const isRecord = (value: unknown): value is Rec =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nexport const recOf = (value: unknown): Rec => (isRecord(value) ? value : {})\n\n/** The maxTokens to send, once the input is one the provider would take. */\nexport function checkChat(\n provider: string,\n messages: readonly ChatMessage[],\n opts: ChatOptions | undefined,\n): number {\n const refuse = (why: string) =>\n new AdapterError('invalid_request', `${provider} chat refused: ${why}`, {\n context: { provider },\n })\n if (!Array.isArray(messages) || messages.length === 0) throw refuse('no messages')\n // The calls of the nearest assistant turn that no tool turn has answered yet.\n let open: string[] = []\n for (const [at, message] of messages.entries()) {\n const turn = recOf(message)\n const role = turn['role']\n if (role !== 'user' && role !== 'assistant' && role !== 'tool') {\n throw refuse(`messages[${at}].role is not user, assistant or tool`)\n }\n if (at === 0 && role !== 'user') throw refuse('the first message is not the user’s')\n const content = turn['content']\n if (typeof content !== 'string') throw refuse(`messages[${at}].content is not text`)\n if (role === 'tool') {\n const id = turn['toolCallId']\n if (typeof id !== 'string' || !open.includes(id)) {\n throw refuse(`messages[${at}] answers no open call of the assistant turn before it`)\n }\n if (content === '') throw refuse(`messages[${at}].content is empty`)\n open = open.filter((o) => o !== id)\n continue\n }\n if (open.length > 0) throw refuse(`messages[${at}] starts with a call left unanswered`)\n const calls = role === 'assistant' ? callIdsOf(turn['toolCalls'], at, refuse) : []\n if (content === '' && calls.length === 0) throw refuse(`messages[${at}].content is empty`)\n open = calls\n }\n checkTools(opts?.tools, refuse)\n const maxTokens = opts?.maxTokens ?? DEFAULT_MAX_TOKENS\n if (!Number.isInteger(maxTokens) || maxTokens <= 0) {\n throw refuse('maxTokens is not a positive integer')\n }\n return maxTokens\n}\n\n/** An assistant turn's call ids, once each call is a well-formed one. */\nfunction callIdsOf(value: unknown, at: number, refuse: (why: string) => AdapterError): string[] {\n if (value === undefined) return []\n if (!Array.isArray(value)) throw refuse(`messages[${at}].toolCalls is not a list`)\n const ids: string[] = []\n for (const [n, call] of (value as unknown[]).entries()) {\n const c = recOf(call)\n const id = c['id']\n if (typeof id !== 'string' || id === '' || ids.includes(id)) {\n throw refuse(`messages[${at}].toolCalls[${n}].id is empty or repeated`)\n }\n if (typeof c['name'] !== 'string' || !TOOL_NAME.test(c['name'])) {\n throw refuse(`messages[${at}].toolCalls[${n}].name is outside ${TOOL_NAME.source}`)\n }\n if (!isRecord(c['args'])) throw refuse(`messages[${at}].toolCalls[${n}].args is not an object`)\n ids.push(id)\n }\n return ids\n}\n\nfunction checkTools(value: unknown, refuse: (why: string) => AdapterError): void {\n if (value === undefined) return\n if (!Array.isArray(value)) throw refuse('tools is not a list')\n const names = new Set<string>()\n for (const [at, tool] of (value as unknown[]).entries()) {\n const t = recOf(tool)\n const name = t['name']\n if (typeof name !== 'string' || !TOOL_NAME.test(name)) {\n throw refuse(`tools[${at}].name is outside ${TOOL_NAME.source}`)\n }\n if (names.has(name)) throw refuse(`tools[${at}].name is taken by an earlier tool`)\n names.add(name)\n if (typeof t['description'] !== 'string') throw refuse(`tools[${at}].description is not text`)\n if (!isRecord(t['inputSchema'])) throw refuse(`tools[${at}].inputSchema is not an object`)\n }\n}\n\n/** A call the provider made, or undefined when its id or name is missing or its arguments are not a JSON object. */\nexport function callOf(id: unknown, name: unknown, args: unknown): ChatToolCall | undefined {\n if (typeof id !== 'string' || id === '' || typeof name !== 'string' || name === '')\n return undefined\n return isRecord(args) ? { id, name, args } : undefined\n}\n\nexport const timedOut = (provider: string, timeoutMs: number): AdapterError =>\n new AdapterError('timeout', `${provider} gave no answer within ${timeoutMs} ms`, {\n context: { provider },\n })\n\nexport const cancelled = (provider: string): AdapterError =>\n new AdapterError('cancelled', `${provider} request cancelled by the caller`, {\n context: { provider },\n })\n\nexport const unreachable = (provider: string): AdapterError =>\n new AdapterError('unavailable', `${provider} could not be reached`, { context: { provider } })\n\n/** A stream that ended, or reported an error, before its answer was complete. */\nexport const brokenOff = (provider: string): AdapterError =>\n new AdapterError('unavailable', `${provider} broke off its answer`, { context: { provider } })\n\n/** A 2xx the adapter cannot read an answer from. */\nexport const unreadable = (provider: string, status: number, what: string): AdapterError =>\n new AdapterError('unexpected_response', `${provider} answered ${status} ${what}`, {\n context: { provider, status },\n })\n\nfunction codeOf(status: number): string {\n if (status === 401 || status === 403) return 'auth_failed'\n if (status === 429) return 'rate_limited'\n if (status >= 400 && status < 500) return 'invalid_request'\n return 'unavailable'\n}\n\n/** A non-2xx answer, classified by its status. */\nexport const answered = (provider: string, status: number): AdapterError =>\n new AdapterError(codeOf(status), `${provider} answered ${status}`, {\n context: { provider, status },\n })\n\n/** A JSON body, or undefined when the text is not JSON. */\nexport function jsonOf(text: string): unknown {\n try {\n return JSON.parse(text) as unknown\n } catch {\n return undefined\n }\n}\n\n/** One request from its send to its last byte: the signal it is sent with, and why it was ended early. */\nexport interface Lifetime {\n signal: AbortSignal\n /** The timeout or the caller's abort, once either has ended the request. */\n ended(): AdapterError | undefined\n /** Clears the timer and closes the connection if it is still open. */\n close(): void\n}\n\n/** Bounds a request by `timeoutMs` (`adapter.timeout`) and by the caller's `signal` (`adapter.cancelled`). */\nexport function lifetime(provider: string, timeoutMs: number, caller?: AbortSignal): Lifetime {\n const controller = new AbortController()\n let reason: AdapterError | undefined\n // The reason is set before the abort, so the request's own abort error is read as it.\n const end = (error: AdapterError) => {\n if (reason !== undefined) return\n reason = error\n controller.abort()\n }\n const timer = setTimeout(() => end(timedOut(provider, timeoutMs)), timeoutMs)\n const onAbort = () => end(cancelled(provider))\n if (caller?.aborted === true) onAbort()\n else caller?.addEventListener('abort', onAbort, { once: true })\n return {\n signal: controller.signal,\n ended: () => reason,\n close: () => {\n clearTimeout(timer)\n caller?.removeEventListener('abort', onAbort)\n controller.abort()\n },\n }\n}\n\n/** POSTs `body` as JSON: the 2xx response, or the error a failure or a non-2xx answer is. */\nexport async function post(\n provider: string,\n url: string,\n headers: Record<string, string>,\n body: string,\n life: Lifetime,\n): Promise<Response> {\n let response: Response\n try {\n response = await globalThis.fetch(url, {\n method: 'POST',\n headers: { ...headers, 'content-type': 'application/json' },\n body,\n signal: life.signal,\n })\n } catch {\n // fetch's own error may quote the request's headers, the key among them: none of it goes on.\n throw life.ended() ?? unreachable(provider)\n }\n // The body is left unread (it may echo the key or the conversation); close() drops the connection.\n if (response.status < 200 || response.status > 299) throw answered(provider, response.status)\n return response\n}\n\n/** The whole body as text, or the error a body cut short is. */\nexport async function bodyText(\n provider: string,\n response: Response,\n life: Lifetime,\n): Promise<string> {\n try {\n return await response.text()\n } catch {\n throw life.ended() ?? brokenOff(provider)\n }\n}\n","import type {\n AiAdapter,\n ChatMessage,\n ChatOptions,\n ChatResult,\n ChatStopReason,\n ChatStreamEvent,\n ChatToolCall,\n Logger,\n} from '@nymbalhq/types'\nimport { AdapterError } from '@nymbalhq/types'\nimport {\n bodyText,\n brokenOff,\n callOf,\n checkChat,\n DEFAULT_TIMEOUT_MS,\n isRecord,\n jsonOf,\n lifetime,\n post,\n recOf,\n unreadable,\n type Lifetime,\n} from './chat.js'\nimport { sseEvents, type SseEvent } from './sse.js'\n\nexport const DEFAULT_BASE_URL = 'https://api.openai.com/v1'\nconst OPENAI_HOST = 'api.openai.com'\n\n/** The request field that bounds the answer's length. */\nexport type TokenField = 'max_tokens' | 'max_completion_tokens'\n\nexport interface OpenAiAdapterConfig {\n apiKey: string\n /** The model the server serves, sent with every request. */\n model: string\n /** Where the chat completions API is, its path prefix included; OpenAI's own when omitted. */\n baseUrl?: string\n /**\n * `max_completion_tokens` on OpenAI's own host, whose newer models refuse `max_tokens`; `max_tokens` on any other\n * (a compatible server may know only that one) when omitted.\n */\n tokenField?: TokenField\n /** How long one chat() waits for its answer, or one chatStream() for its end; 60 000 when omitted. */\n timeoutMs?: number\n logger: Logger\n}\n\nconst PROVIDER = 'openai'\n\nfunction stopReasonOf(wire: unknown): ChatStopReason {\n if (wire === 'stop') return 'end'\n if (wire === 'length') return 'max_tokens'\n if (wire === 'tool_calls') return 'tool_use'\n return 'other'\n}\n\nconst count = (value: unknown): number => (typeof value === 'number' ? value : 0)\n\nconst hostOf = (url: string): string => (URL.canParse(url) ? new URL(url).hostname : '')\n\n/** The conversation as OpenAI's messages: calls as `tool_calls` with JSON-string arguments, tool turns as `tool`. */\nfunction wireMessages(messages: readonly ChatMessage[]): unknown[] {\n return messages.map((m) => {\n if (m.role === 'tool') return { role: 'tool', tool_call_id: m.toolCallId, content: m.content }\n const calls = m.role === 'assistant' ? (m.toolCalls ?? []) : []\n if (calls.length === 0) return { role: m.role, content: m.content }\n return {\n role: m.role,\n content: m.content === '' ? null : m.content,\n tool_calls: calls.map((c) => ({\n id: c.id,\n type: 'function',\n function: { name: c.name, arguments: JSON.stringify(c.args) },\n })),\n }\n })\n}\n\n/** A call from the wire, its arguments a JSON text; undefined when it is not one the adapter can read. */\nfunction wireCallOf(id: unknown, name: unknown, args: unknown): ChatToolCall | undefined {\n return callOf(id, name, typeof args === 'string' ? jsonOf(args) : undefined)\n}\n\nexport function createOpenAiAdapter(config: OpenAiAdapterConfig): AiAdapter {\n const base = config.baseUrl ?? DEFAULT_BASE_URL\n const url = `${base.endsWith('/') ? base.slice(0, -1) : base}/chat/completions`\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const ownHost = hostOf(base) === OPENAI_HOST\n const tokenField = config.tokenField ?? (ownHost ? 'max_completion_tokens' : 'max_tokens')\n const headers = { authorization: `Bearer ${config.apiKey}` }\n\n function requestOf(\n messages: readonly ChatMessage[],\n opts: ChatOptions | undefined,\n stream: boolean,\n ): string {\n const maxTokens = checkChat(PROVIDER, messages, opts)\n const system = opts?.system\n const tools = opts?.tools ?? []\n return JSON.stringify({\n model: config.model,\n [tokenField]: maxTokens,\n messages: [\n ...(system !== undefined && system !== '' ? [{ role: 'system', content: system }] : []),\n ...wireMessages(messages),\n ],\n ...(tools.length > 0 && {\n tools: tools.map((t) => ({\n type: 'function',\n function: { name: t.name, description: t.description, parameters: t.inputSchema },\n })),\n }),\n // Usage on a stream is OpenAI's own option; a compatible server may refuse it.\n ...(stream && { stream: true, ...(ownHost && { stream_options: { include_usage: true } }) }),\n })\n }\n\n function resultOf(\n status: number,\n text: string,\n toolCalls: ChatToolCall[],\n finish: unknown,\n usage: { inputTokens: number; outputTokens: number },\n reported: unknown,\n ): ChatResult {\n if (text === '' && toolCalls.length === 0) throw unreadable(PROVIDER, status, 'with no text')\n return {\n text,\n stopReason: stopReasonOf(finish),\n usage,\n model: typeof reported === 'string' && reported !== '' ? reported : config.model,\n toolCalls,\n }\n }\n\n async function* streamed(\n status: number,\n body: ReadableStream<Uint8Array>,\n life: Lifetime,\n ): AsyncGenerator<ChatStreamEvent> {\n const events = sseEvents(body)\n let reported: unknown\n let inputTokens = 0\n let outputTokens = 0\n let finish: unknown\n let text = ''\n const toolCalls: ChatToolCall[] = []\n // A call's pieces by its index, until the finish reason says every call is complete.\n const building = new Map<number, { id: unknown; name: unknown; json: string }>()\n try {\n for (;;) {\n let next: IteratorResult<SseEvent>\n try {\n next = await events.next()\n } catch {\n throw life.ended() ?? brokenOff(PROVIDER)\n }\n if (next.done === true || next.value.data === '[DONE]') break\n const chunk = jsonOf(next.value.data)\n if (!isRecord(chunk)) throw unreadable(PROVIDER, status, 'with a chunk that is not JSON')\n if (chunk['error'] !== undefined) throw brokenOff(PROVIDER)\n if (chunk['model'] !== undefined) reported = chunk['model']\n if (isRecord(chunk['usage'])) {\n inputTokens = count(chunk['usage']['prompt_tokens'])\n outputTokens = count(chunk['usage']['completion_tokens'])\n }\n const choice = recOf(Array.isArray(chunk['choices']) ? chunk['choices'][0] : undefined)\n const delta = recOf(choice['delta'])\n const content = delta['content']\n if (typeof content === 'string' && content !== '') {\n text += content\n yield { type: 'text', delta: content }\n }\n if (Array.isArray(delta['tool_calls'])) {\n for (const piece of (delta['tool_calls'] as unknown[]).map(recOf)) {\n const index = typeof piece['index'] === 'number' ? piece['index'] : 0\n const fn = recOf(piece['function'])\n const call = building.get(index) ?? { id: undefined, name: undefined, json: '' }\n if (piece['id'] !== undefined) call.id = piece['id']\n if (fn['name'] !== undefined) call.name = fn['name']\n if (typeof fn['arguments'] === 'string') call.json += fn['arguments']\n building.set(index, call)\n }\n }\n if (choice['finish_reason'] !== undefined && choice['finish_reason'] !== null) {\n finish = choice['finish_reason']\n for (const index of [...building.keys()].sort((a, b) => a - b)) {\n const built = building.get(index)!\n const call = wireCallOf(built.id, built.name, built.json)\n if (call === undefined) {\n throw unreadable(\n PROVIDER,\n status,\n 'with a tool call whose arguments are not an object',\n )\n }\n toolCalls.push(call)\n yield { type: 'tool_call', call }\n }\n building.clear()\n }\n }\n // Ended with no finish reason: the answer is not complete.\n if (finish === undefined) throw life.ended() ?? brokenOff(PROVIDER)\n yield {\n type: 'done',\n result: resultOf(status, text, toolCalls, finish, { inputTokens, outputTokens }, reported),\n }\n } finally {\n await events.return(undefined)\n }\n }\n\n const adapter: AiAdapter = {\n kind: 'ai',\n providerName: PROVIDER,\n capabilities: () => ['complete', 'chat', 'tools', 'stream'],\n producesEvents: [],\n consumesEvents: [],\n async initialize() {\n // no-op: each chat() is one request\n },\n async healthCheck() {\n return { status: 'healthy', detail: `OpenAI AI adapter using model ${config.model}` }\n },\n\n async embed() {\n throw new AdapterError('not_configured', 'Embedding not yet supported by the OpenAI adapter')\n },\n\n async complete(prompt, opts) {\n const maxTokens = opts?.maxTokens\n const result = await adapter.chat(\n [{ role: 'user', content: prompt }],\n maxTokens === undefined ? {} : { maxTokens },\n )\n return result.text\n },\n\n async chat(messages, opts): Promise<ChatResult> {\n const request = requestOf(messages, opts, false)\n const life = lifetime(PROVIDER, timeoutMs, opts?.signal)\n let status: number\n let text: string\n try {\n const response = await post(PROVIDER, url, headers, request, life)\n status = response.status\n text = await bodyText(PROVIDER, response, life)\n } finally {\n life.close()\n }\n const body = jsonOf(text)\n if (!isRecord(body)) throw unreadable(PROVIDER, status, 'with a body that is not JSON')\n const choice = recOf(Array.isArray(body['choices']) ? body['choices'][0] : undefined)\n const message = recOf(choice['message'])\n const toolCalls: ChatToolCall[] = []\n const wireCalls = Array.isArray(message['tool_calls'])\n ? (message['tool_calls'] as unknown[])\n : []\n for (const wire of wireCalls.map(recOf)) {\n const fn = recOf(wire['function'])\n const call = wireCallOf(wire['id'], fn['name'], fn['arguments'])\n if (call === undefined) {\n throw unreadable(PROVIDER, status, 'with a tool call whose arguments are not an object')\n }\n toolCalls.push(call)\n }\n const usage = recOf(body['usage'])\n return resultOf(\n status,\n typeof message['content'] === 'string' ? message['content'] : '',\n toolCalls,\n choice['finish_reason'],\n {\n inputTokens: count(usage['prompt_tokens']),\n outputTokens: count(usage['completion_tokens']),\n },\n body['model'],\n )\n },\n\n async *chatStream(messages, opts) {\n const request = requestOf(messages, opts, true)\n const life = lifetime(PROVIDER, timeoutMs, opts?.signal)\n try {\n const response = await post(PROVIDER, url, headers, request, life)\n if (response.body === null) throw unreadable(PROVIDER, response.status, 'with no body')\n yield* streamed(response.status, response.body, life)\n } finally {\n life.close()\n }\n },\n }\n return adapter\n}\n","// A server-sent events reader for a provider's streamed answer, the same in every ai provider package. It yields each\n// event once its blank line has arrived; a read that fails (a broken connection, an abort) rejects as it is, for the\n// adapter to classify.\n\n/** One event: its `event` field (`message` when it has none) and its `data` lines joined by a newline. */\nexport interface SseEvent {\n event: string\n data: string\n}\n\nexport async function* sseEvents(body: ReadableStream<Uint8Array>): AsyncGenerator<SseEvent> {\n const reader = body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n let event = ''\n let data: string[] = []\n try {\n for (;;) {\n const { done, value } = await reader.read()\n let text = buffer + (done ? decoder.decode() : decoder.decode(value, { stream: true }))\n // A CR at the end is held back, as the LF that may make it one line end can be in the next chunk.\n const held = !done && text.endsWith('\\r') ? '\\r' : ''\n if (held !== '') text = text.slice(0, -1)\n const lines = text.split(/\\r\\n|\\r|\\n/)\n // The last piece is a line not yet ended.\n buffer = (lines.pop() ?? '') + held\n for (const line of lines) {\n if (line === '') {\n if (data.length > 0)\n yield { event: event === '' ? 'message' : event, data: data.join('\\n') }\n event = ''\n data = []\n continue\n }\n if (line.startsWith(':')) continue\n const colon = line.indexOf(':')\n const field = colon === -1 ? line : line.slice(0, colon)\n const raw = colon === -1 ? '' : line.slice(colon + 1)\n const value = raw.startsWith(' ') ? raw.slice(1) : raw\n if (field === 'event') event = value\n else if (field === 'data') data.push(value)\n }\n // An event whose blank line never came is incomplete, as the standard has it, and is dropped.\n if (done) return\n }\n } finally {\n reader.releaseLock()\n }\n}\n"],"mappings":";AAAA;AAAA,EACE,eAAAA;AAAA,OAIK;;;ACLP;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AAMA,IAAM,qBAAqB;AAC3B,IAAM,qBAAqB;AAClC,IAAM,YAAY;AAGX,SAAS,UAAU,OAAwB;AAChD,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,UAAU,KAAK,KAAK,SAAS,GAAG;AACvE,UAAM,IAAI,YAAY,yEAAyE;AAAA,MAC7F,SAAS,EAAE,MAAM,KAAK;AAAA,IACxB,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAIO,IAAM,WAAW,CAAC,UACvB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAE9D,IAAM,QAAQ,CAAC,UAAyB,SAAS,KAAK,IAAI,QAAQ,CAAC;AAGnE,SAAS,UACdC,WACA,UACA,MACQ;AACR,QAAM,SAAS,CAAC,QACd,IAAI,aAAa,mBAAmB,GAAGA,SAAQ,kBAAkB,GAAG,IAAI;AAAA,IACtE,SAAS,EAAE,UAAAA,UAAS;AAAA,EACtB,CAAC;AACH,MAAI,CAAC,MAAM,QAAQ,QAAQ,KAAK,SAAS,WAAW,EAAG,OAAM,OAAO,aAAa;AAEjF,MAAI,OAAiB,CAAC;AACtB,aAAW,CAAC,IAAI,OAAO,KAAK,SAAS,QAAQ,GAAG;AAC9C,UAAM,OAAO,MAAM,OAAO;AAC1B,UAAM,OAAO,KAAK,MAAM;AACxB,QAAI,SAAS,UAAU,SAAS,eAAe,SAAS,QAAQ;AAC9D,YAAM,OAAO,YAAY,EAAE,uCAAuC;AAAA,IACpE;AACA,QAAI,OAAO,KAAK,SAAS,OAAQ,OAAM,OAAO,0CAAqC;AACnF,UAAM,UAAU,KAAK,SAAS;AAC9B,QAAI,OAAO,YAAY,SAAU,OAAM,OAAO,YAAY,EAAE,uBAAuB;AACnF,QAAI,SAAS,QAAQ;AACnB,YAAM,KAAK,KAAK,YAAY;AAC5B,UAAI,OAAO,OAAO,YAAY,CAAC,KAAK,SAAS,EAAE,GAAG;AAChD,cAAM,OAAO,YAAY,EAAE,wDAAwD;AAAA,MACrF;AACA,UAAI,YAAY,GAAI,OAAM,OAAO,YAAY,EAAE,oBAAoB;AACnE,aAAO,KAAK,OAAO,CAAC,MAAM,MAAM,EAAE;AAClC;AAAA,IACF;AACA,QAAI,KAAK,SAAS,EAAG,OAAM,OAAO,YAAY,EAAE,sCAAsC;AACtF,UAAM,QAAQ,SAAS,cAAc,UAAU,KAAK,WAAW,GAAG,IAAI,MAAM,IAAI,CAAC;AACjF,QAAI,YAAY,MAAM,MAAM,WAAW,EAAG,OAAM,OAAO,YAAY,EAAE,oBAAoB;AACzF,WAAO;AAAA,EACT;AACA,aAAW,MAAM,OAAO,MAAM;AAC9B,QAAM,YAAY,MAAM,aAAa;AACrC,MAAI,CAAC,OAAO,UAAU,SAAS,KAAK,aAAa,GAAG;AAClD,UAAM,OAAO,qCAAqC;AAAA,EACpD;AACA,SAAO;AACT;AAGA,SAAS,UAAU,OAAgB,IAAY,QAAiD;AAC9F,MAAI,UAAU,OAAW,QAAO,CAAC;AACjC,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,OAAM,OAAO,YAAY,EAAE,2BAA2B;AACjF,QAAM,MAAgB,CAAC;AACvB,aAAW,CAAC,GAAG,IAAI,KAAM,MAAoB,QAAQ,GAAG;AACtD,UAAM,IAAI,MAAM,IAAI;AACpB,UAAM,KAAK,EAAE,IAAI;AACjB,QAAI,OAAO,OAAO,YAAY,OAAO,MAAM,IAAI,SAAS,EAAE,GAAG;AAC3D,YAAM,OAAO,YAAY,EAAE,eAAe,CAAC,2BAA2B;AAAA,IACxE;AACA,QAAI,OAAO,EAAE,MAAM,MAAM,YAAY,CAAC,UAAU,KAAK,EAAE,MAAM,CAAC,GAAG;AAC/D,YAAM,OAAO,YAAY,EAAE,eAAe,CAAC,qBAAqB,UAAU,MAAM,EAAE;AAAA,IACpF;AACA,QAAI,CAAC,SAAS,EAAE,MAAM,CAAC,EAAG,OAAM,OAAO,YAAY,EAAE,eAAe,CAAC,yBAAyB;AAC9F,QAAI,KAAK,EAAE;AAAA,EACb;AACA,SAAO;AACT;AAEA,SAAS,WAAW,OAAgB,QAA6C;AAC/E,MAAI,UAAU,OAAW;AACzB,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,OAAM,OAAO,qBAAqB;AAC7D,QAAM,QAAQ,oBAAI,IAAY;AAC9B,aAAW,CAAC,IAAI,IAAI,KAAM,MAAoB,QAAQ,GAAG;AACvD,UAAM,IAAI,MAAM,IAAI;AACpB,UAAM,OAAO,EAAE,MAAM;AACrB,QAAI,OAAO,SAAS,YAAY,CAAC,UAAU,KAAK,IAAI,GAAG;AACrD,YAAM,OAAO,SAAS,EAAE,qBAAqB,UAAU,MAAM,EAAE;AAAA,IACjE;AACA,QAAI,MAAM,IAAI,IAAI,EAAG,OAAM,OAAO,SAAS,EAAE,oCAAoC;AACjF,UAAM,IAAI,IAAI;AACd,QAAI,OAAO,EAAE,aAAa,MAAM,SAAU,OAAM,OAAO,SAAS,EAAE,2BAA2B;AAC7F,QAAI,CAAC,SAAS,EAAE,aAAa,CAAC,EAAG,OAAM,OAAO,SAAS,EAAE,gCAAgC;AAAA,EAC3F;AACF;AAGO,SAAS,OAAO,IAAa,MAAe,MAAyC;AAC1F,MAAI,OAAO,OAAO,YAAY,OAAO,MAAM,OAAO,SAAS,YAAY,SAAS;AAC9E,WAAO;AACT,SAAO,SAAS,IAAI,IAAI,EAAE,IAAI,MAAM,KAAK,IAAI;AAC/C;AAEO,IAAM,WAAW,CAACA,WAAkB,cACzC,IAAI,aAAa,WAAW,GAAGA,SAAQ,0BAA0B,SAAS,OAAO;AAAA,EAC/E,SAAS,EAAE,UAAAA,UAAS;AACtB,CAAC;AAEI,IAAM,YAAY,CAACA,cACxB,IAAI,aAAa,aAAa,GAAGA,SAAQ,oCAAoC;AAAA,EAC3E,SAAS,EAAE,UAAAA,UAAS;AACtB,CAAC;AAEI,IAAM,cAAc,CAACA,cAC1B,IAAI,aAAa,eAAe,GAAGA,SAAQ,yBAAyB,EAAE,SAAS,EAAE,UAAAA,UAAS,EAAE,CAAC;AAGxF,IAAM,YAAY,CAACA,cACxB,IAAI,aAAa,eAAe,GAAGA,SAAQ,yBAAyB,EAAE,SAAS,EAAE,UAAAA,UAAS,EAAE,CAAC;AAGxF,IAAM,aAAa,CAACA,WAAkB,QAAgB,SAC3D,IAAI,aAAa,uBAAuB,GAAGA,SAAQ,aAAa,MAAM,IAAI,IAAI,IAAI;AAAA,EAChF,SAAS,EAAE,UAAAA,WAAU,OAAO;AAC9B,CAAC;AAEH,SAAS,OAAO,QAAwB;AACtC,MAAI,WAAW,OAAO,WAAW,IAAK,QAAO;AAC7C,MAAI,WAAW,IAAK,QAAO;AAC3B,MAAI,UAAU,OAAO,SAAS,IAAK,QAAO;AAC1C,SAAO;AACT;AAGO,IAAM,WAAW,CAACA,WAAkB,WACzC,IAAI,aAAa,OAAO,MAAM,GAAG,GAAGA,SAAQ,aAAa,MAAM,IAAI;AAAA,EACjE,SAAS,EAAE,UAAAA,WAAU,OAAO;AAC9B,CAAC;AAGI,SAAS,OAAO,MAAuB;AAC5C,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAYO,SAAS,SAASA,WAAkB,WAAmB,QAAgC;AAC5F,QAAM,aAAa,IAAI,gBAAgB;AACvC,MAAI;AAEJ,QAAM,MAAM,CAAC,UAAwB;AACnC,QAAI,WAAW,OAAW;AAC1B,aAAS;AACT,eAAW,MAAM;AAAA,EACnB;AACA,QAAM,QAAQ,WAAW,MAAM,IAAI,SAASA,WAAU,SAAS,CAAC,GAAG,SAAS;AAC5E,QAAM,UAAU,MAAM,IAAI,UAAUA,SAAQ,CAAC;AAC7C,MAAI,QAAQ,YAAY,KAAM,SAAQ;AAAA,MACjC,SAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAC9D,SAAO;AAAA,IACL,QAAQ,WAAW;AAAA,IACnB,OAAO,MAAM;AAAA,IACb,OAAO,MAAM;AACX,mBAAa,KAAK;AAClB,cAAQ,oBAAoB,SAAS,OAAO;AAC5C,iBAAW,MAAM;AAAA,IACnB;AAAA,EACF;AACF;AAGA,eAAsB,KACpBA,WACA,KACA,SACA,MACA,MACmB;AACnB,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,WAAW,MAAM,KAAK;AAAA,MACrC,QAAQ;AAAA,MACR,SAAS,EAAE,GAAG,SAAS,gBAAgB,mBAAmB;AAAA,MAC1D;AAAA,MACA,QAAQ,KAAK;AAAA,IACf,CAAC;AAAA,EACH,QAAQ;AAEN,UAAM,KAAK,MAAM,KAAK,YAAYA,SAAQ;AAAA,EAC5C;AAEA,MAAI,SAAS,SAAS,OAAO,SAAS,SAAS,IAAK,OAAM,SAASA,WAAU,SAAS,MAAM;AAC5F,SAAO;AACT;AAGA,eAAsB,SACpBA,WACA,UACA,MACiB;AACjB,MAAI;AACF,WAAO,MAAM,SAAS,KAAK;AAAA,EAC7B,QAAQ;AACN,UAAM,KAAK,MAAM,KAAK,UAAUA,SAAQ;AAAA,EAC1C;AACF;;;ACnOA,SAAS,gBAAAC,qBAAoB;;;ACA7B,gBAAuB,UAAU,MAA4D;AAC3F,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,UAAU,IAAI,YAAY;AAChC,MAAI,SAAS;AACb,MAAI,QAAQ;AACZ,MAAI,OAAiB,CAAC;AACtB,MAAI;AACF,eAAS;AACP,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,UAAI,OAAO,UAAU,OAAO,QAAQ,OAAO,IAAI,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAErF,YAAM,OAAO,CAAC,QAAQ,KAAK,SAAS,IAAI,IAAI,OAAO;AACnD,UAAI,SAAS,GAAI,QAAO,KAAK,MAAM,GAAG,EAAE;AACxC,YAAM,QAAQ,KAAK,MAAM,YAAY;AAErC,gBAAU,MAAM,IAAI,KAAK,MAAM;AAC/B,iBAAW,QAAQ,OAAO;AACxB,YAAI,SAAS,IAAI;AACf,cAAI,KAAK,SAAS;AAChB,kBAAM,EAAE,OAAO,UAAU,KAAK,YAAY,OAAO,MAAM,KAAK,KAAK,IAAI,EAAE;AACzE,kBAAQ;AACR,iBAAO,CAAC;AACR;AAAA,QACF;AACA,YAAI,KAAK,WAAW,GAAG,EAAG;AAC1B,cAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,cAAM,QAAQ,UAAU,KAAK,OAAO,KAAK,MAAM,GAAG,KAAK;AACvD,cAAM,MAAM,UAAU,KAAK,KAAK,KAAK,MAAM,QAAQ,CAAC;AACpD,cAAMC,SAAQ,IAAI,WAAW,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI;AACnD,YAAI,UAAU,QAAS,SAAQA;AAAA,iBACtB,UAAU,OAAQ,MAAK,KAAKA,MAAK;AAAA,MAC5C;AAEA,UAAI,KAAM;AAAA,IACZ;AAAA,EACF,UAAE;AACA,WAAO,YAAY;AAAA,EACrB;AACF;;;ADrBO,IAAM,mBAAmB;AAChC,IAAM,cAAc;AAqBpB,IAAM,WAAW;AAEjB,SAAS,aAAa,MAA+B;AACnD,MAAI,SAAS,OAAQ,QAAO;AAC5B,MAAI,SAAS,SAAU,QAAO;AAC9B,MAAI,SAAS,aAAc,QAAO;AAClC,SAAO;AACT;AAEA,IAAM,QAAQ,CAAC,UAA4B,OAAO,UAAU,WAAW,QAAQ;AAE/E,IAAM,SAAS,CAAC,QAAyB,IAAI,SAAS,GAAG,IAAI,IAAI,IAAI,GAAG,EAAE,WAAW;AAGrF,SAAS,aAAa,UAA6C;AACjE,SAAO,SAAS,IAAI,CAAC,MAAM;AACzB,QAAI,EAAE,SAAS,OAAQ,QAAO,EAAE,MAAM,QAAQ,cAAc,EAAE,YAAY,SAAS,EAAE,QAAQ;AAC7F,UAAM,QAAQ,EAAE,SAAS,cAAe,EAAE,aAAa,CAAC,IAAK,CAAC;AAC9D,QAAI,MAAM,WAAW,EAAG,QAAO,EAAE,MAAM,EAAE,MAAM,SAAS,EAAE,QAAQ;AAClE,WAAO;AAAA,MACL,MAAM,EAAE;AAAA,MACR,SAAS,EAAE,YAAY,KAAK,OAAO,EAAE;AAAA,MACrC,YAAY,MAAM,IAAI,CAAC,OAAO;AAAA,QAC5B,IAAI,EAAE;AAAA,QACN,MAAM;AAAA,QACN,UAAU,EAAE,MAAM,EAAE,MAAM,WAAW,KAAK,UAAU,EAAE,IAAI,EAAE;AAAA,MAC9D,EAAE;AAAA,IACJ;AAAA,EACF,CAAC;AACH;AAGA,SAAS,WAAW,IAAa,MAAe,MAAyC;AACvF,SAAO,OAAO,IAAI,MAAM,OAAO,SAAS,WAAW,OAAO,IAAI,IAAI,MAAS;AAC7E;AAEO,SAAS,oBAAoB,QAAwC;AAC1E,QAAM,OAAO,OAAO,WAAW;AAC/B,QAAM,MAAM,GAAG,KAAK,SAAS,GAAG,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI,IAAI;AAC5D,QAAM,YAAY,OAAO,aAAa;AACtC,QAAM,UAAU,OAAO,IAAI,MAAM;AACjC,QAAM,aAAa,OAAO,eAAe,UAAU,0BAA0B;AAC7E,QAAM,UAAU,EAAE,eAAe,UAAU,OAAO,MAAM,GAAG;AAE3D,WAAS,UACP,UACA,MACA,QACQ;AACR,UAAM,YAAY,UAAU,UAAU,UAAU,IAAI;AACpD,UAAM,SAAS,MAAM;AACrB,UAAM,QAAQ,MAAM,SAAS,CAAC;AAC9B,WAAO,KAAK,UAAU;AAAA,MACpB,OAAO,OAAO;AAAA,MACd,CAAC,UAAU,GAAG;AAAA,MACd,UAAU;AAAA,QACR,GAAI,WAAW,UAAa,WAAW,KAAK,CAAC,EAAE,MAAM,UAAU,SAAS,OAAO,CAAC,IAAI,CAAC;AAAA,QACrF,GAAG,aAAa,QAAQ;AAAA,MAC1B;AAAA,MACA,GAAI,MAAM,SAAS,KAAK;AAAA,QACtB,OAAO,MAAM,IAAI,CAAC,OAAO;AAAA,UACvB,MAAM;AAAA,UACN,UAAU,EAAE,MAAM,EAAE,MAAM,aAAa,EAAE,aAAa,YAAY,EAAE,YAAY;AAAA,QAClF,EAAE;AAAA,MACJ;AAAA;AAAA,MAEA,GAAI,UAAU,EAAE,QAAQ,MAAM,GAAI,WAAW,EAAE,gBAAgB,EAAE,eAAe,KAAK,EAAE,EAAG;AAAA,IAC5F,CAAC;AAAA,EACH;AAEA,WAAS,SACP,QACA,MACA,WACA,QACA,OACA,UACY;AACZ,QAAI,SAAS,MAAM,UAAU,WAAW,EAAG,OAAM,WAAW,UAAU,QAAQ,cAAc;AAC5F,WAAO;AAAA,MACL;AAAA,MACA,YAAY,aAAa,MAAM;AAAA,MAC/B;AAAA,MACA,OAAO,OAAO,aAAa,YAAY,aAAa,KAAK,WAAW,OAAO;AAAA,MAC3E;AAAA,IACF;AAAA,EACF;AAEA,kBAAgB,SACd,QACA,MACA,MACiC;AACjC,UAAM,SAAS,UAAU,IAAI;AAC7B,QAAI;AACJ,QAAI,cAAc;AAClB,QAAI,eAAe;AACnB,QAAI;AACJ,QAAI,OAAO;AACX,UAAM,YAA4B,CAAC;AAEnC,UAAM,WAAW,oBAAI,IAA0D;AAC/E,QAAI;AACF,iBAAS;AACP,YAAI;AACJ,YAAI;AACF,iBAAO,MAAM,OAAO,KAAK;AAAA,QAC3B,QAAQ;AACN,gBAAM,KAAK,MAAM,KAAK,UAAU,QAAQ;AAAA,QAC1C;AACA,YAAI,KAAK,SAAS,QAAQ,KAAK,MAAM,SAAS,SAAU;AACxD,cAAM,QAAQ,OAAO,KAAK,MAAM,IAAI;AACpC,YAAI,CAAC,SAAS,KAAK,EAAG,OAAM,WAAW,UAAU,QAAQ,+BAA+B;AACxF,YAAI,MAAM,OAAO,MAAM,OAAW,OAAM,UAAU,QAAQ;AAC1D,YAAI,MAAM,OAAO,MAAM,OAAW,YAAW,MAAM,OAAO;AAC1D,YAAI,SAAS,MAAM,OAAO,CAAC,GAAG;AAC5B,wBAAc,MAAM,MAAM,OAAO,EAAE,eAAe,CAAC;AACnD,yBAAe,MAAM,MAAM,OAAO,EAAE,mBAAmB,CAAC;AAAA,QAC1D;AACA,cAAM,SAAS,MAAM,MAAM,QAAQ,MAAM,SAAS,CAAC,IAAI,MAAM,SAAS,EAAE,CAAC,IAAI,MAAS;AACtF,cAAM,QAAQ,MAAM,OAAO,OAAO,CAAC;AACnC,cAAM,UAAU,MAAM,SAAS;AAC/B,YAAI,OAAO,YAAY,YAAY,YAAY,IAAI;AACjD,kBAAQ;AACR,gBAAM,EAAE,MAAM,QAAQ,OAAO,QAAQ;AAAA,QACvC;AACA,YAAI,MAAM,QAAQ,MAAM,YAAY,CAAC,GAAG;AACtC,qBAAW,SAAU,MAAM,YAAY,EAAgB,IAAI,KAAK,GAAG;AACjE,kBAAM,QAAQ,OAAO,MAAM,OAAO,MAAM,WAAW,MAAM,OAAO,IAAI;AACpE,kBAAM,KAAK,MAAM,MAAM,UAAU,CAAC;AAClC,kBAAM,OAAO,SAAS,IAAI,KAAK,KAAK,EAAE,IAAI,QAAW,MAAM,QAAW,MAAM,GAAG;AAC/E,gBAAI,MAAM,IAAI,MAAM,OAAW,MAAK,KAAK,MAAM,IAAI;AACnD,gBAAI,GAAG,MAAM,MAAM,OAAW,MAAK,OAAO,GAAG,MAAM;AACnD,gBAAI,OAAO,GAAG,WAAW,MAAM,SAAU,MAAK,QAAQ,GAAG,WAAW;AACpE,qBAAS,IAAI,OAAO,IAAI;AAAA,UAC1B;AAAA,QACF;AACA,YAAI,OAAO,eAAe,MAAM,UAAa,OAAO,eAAe,MAAM,MAAM;AAC7E,mBAAS,OAAO,eAAe;AAC/B,qBAAW,SAAS,CAAC,GAAG,SAAS,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG;AAC9D,kBAAM,QAAQ,SAAS,IAAI,KAAK;AAChC,kBAAM,OAAO,WAAW,MAAM,IAAI,MAAM,MAAM,MAAM,IAAI;AACxD,gBAAI,SAAS,QAAW;AACtB,oBAAM;AAAA,gBACJ;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA,YACF;AACA,sBAAU,KAAK,IAAI;AACnB,kBAAM,EAAE,MAAM,aAAa,KAAK;AAAA,UAClC;AACA,mBAAS,MAAM;AAAA,QACjB;AAAA,MACF;AAEA,UAAI,WAAW,OAAW,OAAM,KAAK,MAAM,KAAK,UAAU,QAAQ;AAClE,YAAM;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ,SAAS,QAAQ,MAAM,WAAW,QAAQ,EAAE,aAAa,aAAa,GAAG,QAAQ;AAAA,MAC3F;AAAA,IACF,UAAE;AACA,YAAM,OAAO,OAAO,MAAS;AAAA,IAC/B;AAAA,EACF;AAEA,QAAM,UAAqB;AAAA,IACzB,MAAM;AAAA,IACN,cAAc;AAAA,IACd,cAAc,MAAM,CAAC,YAAY,QAAQ,SAAS,QAAQ;AAAA,IAC1D,gBAAgB,CAAC;AAAA,IACjB,gBAAgB,CAAC;AAAA,IACjB,MAAM,aAAa;AAAA,IAEnB;AAAA,IACA,MAAM,cAAc;AAClB,aAAO,EAAE,QAAQ,WAAW,QAAQ,iCAAiC,OAAO,KAAK,GAAG;AAAA,IACtF;AAAA,IAEA,MAAM,QAAQ;AACZ,YAAM,IAAIC,cAAa,kBAAkB,mDAAmD;AAAA,IAC9F;AAAA,IAEA,MAAM,SAAS,QAAQ,MAAM;AAC3B,YAAM,YAAY,MAAM;AACxB,YAAM,SAAS,MAAM,QAAQ;AAAA,QAC3B,CAAC,EAAE,MAAM,QAAQ,SAAS,OAAO,CAAC;AAAA,QAClC,cAAc,SAAY,CAAC,IAAI,EAAE,UAAU;AAAA,MAC7C;AACA,aAAO,OAAO;AAAA,IAChB;AAAA,IAEA,MAAM,KAAK,UAAU,MAA2B;AAC9C,YAAM,UAAU,UAAU,UAAU,MAAM,KAAK;AAC/C,YAAM,OAAO,SAAS,UAAU,WAAW,MAAM,MAAM;AACvD,UAAI;AACJ,UAAI;AACJ,UAAI;AACF,cAAM,WAAW,MAAM,KAAK,UAAU,KAAK,SAAS,SAAS,IAAI;AACjE,iBAAS,SAAS;AAClB,eAAO,MAAM,SAAS,UAAU,UAAU,IAAI;AAAA,MAChD,UAAE;AACA,aAAK,MAAM;AAAA,MACb;AACA,YAAM,OAAO,OAAO,IAAI;AACxB,UAAI,CAAC,SAAS,IAAI,EAAG,OAAM,WAAW,UAAU,QAAQ,8BAA8B;AACtF,YAAM,SAAS,MAAM,MAAM,QAAQ,KAAK,SAAS,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC,IAAI,MAAS;AACpF,YAAM,UAAU,MAAM,OAAO,SAAS,CAAC;AACvC,YAAM,YAA4B,CAAC;AACnC,YAAM,YAAY,MAAM,QAAQ,QAAQ,YAAY,CAAC,IAChD,QAAQ,YAAY,IACrB,CAAC;AACL,iBAAW,QAAQ,UAAU,IAAI,KAAK,GAAG;AACvC,cAAM,KAAK,MAAM,KAAK,UAAU,CAAC;AACjC,cAAM,OAAO,WAAW,KAAK,IAAI,GAAG,GAAG,MAAM,GAAG,GAAG,WAAW,CAAC;AAC/D,YAAI,SAAS,QAAW;AACtB,gBAAM,WAAW,UAAU,QAAQ,oDAAoD;AAAA,QACzF;AACA,kBAAU,KAAK,IAAI;AAAA,MACrB;AACA,YAAM,QAAQ,MAAM,KAAK,OAAO,CAAC;AACjC,aAAO;AAAA,QACL;AAAA,QACA,OAAO,QAAQ,SAAS,MAAM,WAAW,QAAQ,SAAS,IAAI;AAAA,QAC9D;AAAA,QACA,OAAO,eAAe;AAAA,QACtB;AAAA,UACE,aAAa,MAAM,MAAM,eAAe,CAAC;AAAA,UACzC,cAAc,MAAM,MAAM,mBAAmB,CAAC;AAAA,QAChD;AAAA,QACA,KAAK,OAAO;AAAA,MACd;AAAA,IACF;AAAA,IAEA,OAAO,WAAW,UAAU,MAAM;AAChC,YAAM,UAAU,UAAU,UAAU,MAAM,IAAI;AAC9C,YAAM,OAAO,SAAS,UAAU,WAAW,MAAM,MAAM;AACvD,UAAI;AACF,cAAM,WAAW,MAAM,KAAK,UAAU,KAAK,SAAS,SAAS,IAAI;AACjE,YAAI,SAAS,SAAS,KAAM,OAAM,WAAW,UAAU,SAAS,QAAQ,cAAc;AACtF,eAAO,SAAS,SAAS,QAAQ,SAAS,MAAM,IAAI;AAAA,MACtD,UAAE;AACA,aAAK,MAAM;AAAA,MACb;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;;;AF1RO,IAAM,OAAO;AACb,IAAM,WAAW;AAKjB,IAAM,WAA6B;AAAA,EACxC,EAAE,MAAM,UAAU,OAAO,WAAW,QAAQ,MAAM,KAAK,iBAAiB;AAAA,EACxE,EAAE,MAAM,WAAW,OAAO,gBAAgB,QAAQ,MAAM;AAC1D;AAOO,SAAS,cAAc,KAAgC;AAC5D,MAAI,IAAI,SAAS,MAAM;AACrB,UAAM,IAAIC,aAAY,sCAAsC,IAAI,IAAI,IAAI;AAAA,MACtE,MAAM;AAAA,MACN,SAAS,EAAE,MAAM,IAAI,KAAK;AAAA,IAC5B,CAAC;AAAA,EACH;AACA,QAAM,SAAS,IAAI,OAAO,QAAQ;AAClC,QAAM,QAAQ,IAAI,OAAO,OAAO;AAChC,QAAM,UAAU,IAAI,OAAO,SAAS;AACpC,MAAI,OAAO,UAAU,YAAY,UAAU,IAAI;AAC7C,UAAM,IAAIA;AAAA,MACR;AAAA,MACA,EAAE,SAAS,EAAE,MAAM,IAAI,KAAK,EAAE;AAAA,IAChC;AAAA,EACF;AACA,QAAM,aAAa,IAAI,OAAO,YAAY;AAC1C,MACE,eAAe,UACf,eAAe,gBACf,eAAe,yBACf;AACA,UAAM,IAAIA;AAAA,MACR;AAAA,MACA,EAAE,SAAS,EAAE,MAAM,IAAI,KAAK,EAAE;AAAA,IAChC;AAAA,EACF;AACA,SAAO,oBAAoB;AAAA,IACzB,QACE,OAAO,WAAW,YAAY,WAAW,KAAK,SAAU,IAAI,IAAI,gBAAgB,KAAK;AAAA,IACvF;AAAA,IACA,GAAI,OAAO,YAAY,YAAY,YAAY,MAAM,EAAE,QAAQ;AAAA,IAC/D,GAAI,eAAe,UAAa,EAAE,WAAW;AAAA,IAC7C,WAAW,UAAU,IAAI,OAAO,WAAW,CAAC;AAAA,IAC5C,QAAQ,IAAI;AAAA,EACd,CAAC;AACH;","names":["ConfigError","provider","AdapterError","value","AdapterError","ConfigError"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nymbalhq/ai-openai",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenAI AI for Nymbal: the `openai` ai provider, for OpenAI and any OpenAI-compatible server",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/nymbalhq/nymbal.git",
|
|
9
|
+
"directory": "packages/adapters/ai/openai"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@nymbalhq/types": "^1.5.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@nymbalhq/types": "1.6.0",
|
|
34
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
35
|
+
"tsup": "^9.0.0",
|
|
36
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
37
|
+
"vitest": "^3.0.0"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"dev": "tsup --watch",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json --noEmit --rootDir ../../../.."
|
|
43
|
+
}
|
|
6
44
|
}
|