@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/esm/model.mjs
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { mapFinishReason } from "./utils/map-finish-reason.mjs";
|
|
2
|
+
import { toOpenAIMessages } from "./utils/to-openai-messages.mjs";
|
|
3
|
+
import { toOpenAITools } from "./utils/to-openai-tools.mjs";
|
|
4
|
+
import { wrapOpenAIError } from "./utils/wrap-openai-error.mjs";
|
|
5
|
+
import "./utils/index.mjs";
|
|
6
|
+
import { inferVisionCapability } from "./known-vision-models.mjs";
|
|
7
|
+
import { safeJsonParse } from "@warlock.js/ai";
|
|
8
|
+
import { log } from "@warlock.js/logger";
|
|
9
|
+
|
|
10
|
+
//#region ../../@warlock.js/ai-openai/src/model.ts
|
|
11
|
+
const LOG_MODULE = "ai.openai";
|
|
12
|
+
/**
|
|
13
|
+
* Map an explicit `responseFormat` override to the default
|
|
14
|
+
* `structuredOutput` capability. Loose wire modes (`"json_object"`,
|
|
15
|
+
* `"text"`) don't enforce shape, so the agent needs to see the soft
|
|
16
|
+
* schema hint in the system prompt — that only happens when the
|
|
17
|
+
* capability is `false`. Default (no override) stays `true` to
|
|
18
|
+
* preserve the prior assumption that OpenAI models support strict
|
|
19
|
+
* structured output.
|
|
20
|
+
*/
|
|
21
|
+
function inferStructuredOutput(responseFormat) {
|
|
22
|
+
if (responseFormat === "json_object" || responseFormat === "text") return false;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* OpenAI-backed implementation of `ModelContract`.
|
|
27
|
+
*
|
|
28
|
+
* **Role.** The provider-facing bridge between the vendor-neutral
|
|
29
|
+
* `@warlock.js/ai` agent runtime and the official `openai` SDK. Agents,
|
|
30
|
+
* workflows, and supervisors never talk to OpenAI directly — they hold a
|
|
31
|
+
* `ModelContract`, and this class is what makes that contract concrete for
|
|
32
|
+
* any OpenAI-compatible endpoint (OpenAI, Azure OpenAI, OpenRouter, local
|
|
33
|
+
* gateways that speak the Chat Completions protocol).
|
|
34
|
+
*
|
|
35
|
+
* **Responsibility.**
|
|
36
|
+
* - Owns: a long-lived `OpenAI` client + frozen `ModelConfig` (name,
|
|
37
|
+
* temperature, maxTokens) used as defaults for every call.
|
|
38
|
+
* - Owns: translating vendor-neutral `Message[]` and
|
|
39
|
+
* `ToolContract[]` into OpenAI wire shapes on the way out, and
|
|
40
|
+
* translating OpenAI's response (content, finish reason, tool calls,
|
|
41
|
+
* usage) back into the neutral shapes on the way in.
|
|
42
|
+
* - Does NOT own: dispatching tools, deciding whether to loop, tracking
|
|
43
|
+
* conversation history, or retrying on failure — those are agent
|
|
44
|
+
* concerns. The model is a stateless (per-call) protocol adapter.
|
|
45
|
+
*
|
|
46
|
+
* Because it holds a live client and shared defaults, it is modeled as a
|
|
47
|
+
* class (see §4.2 of code-style.md — "long-lived state across calls").
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* import OpenAI from "openai";
|
|
51
|
+
* const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
52
|
+
* const model = new OpenAIModel(client, { name: "gpt-4o", temperature: 0.3 });
|
|
53
|
+
*
|
|
54
|
+
* const myAgent = agent({
|
|
55
|
+
* model,
|
|
56
|
+
* systemPrompt: "You are a helpful assistant.",
|
|
57
|
+
* tools: [searchTool],
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* const result = await myAgent.execute("Summarize today's news.");
|
|
61
|
+
*/
|
|
62
|
+
var OpenAIModel = class {
|
|
63
|
+
constructor(client, config, provider = "openai") {
|
|
64
|
+
this.logger = log;
|
|
65
|
+
this.client = client;
|
|
66
|
+
this.config = config;
|
|
67
|
+
this.name = config.name;
|
|
68
|
+
this.provider = provider;
|
|
69
|
+
this.pricing = config.pricing;
|
|
70
|
+
this.capabilities = {
|
|
71
|
+
structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),
|
|
72
|
+
vision: config.vision ?? inferVisionCapability(config.name)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Single-shot completion. Sends the full message list to the Chat
|
|
77
|
+
* Completions endpoint, waits for the terminal response, and reshapes it
|
|
78
|
+
* into a vendor-neutral `ModelResponse`. Per-call `options` override the
|
|
79
|
+
* instance's `ModelConfig` defaults for this call only.
|
|
80
|
+
*/
|
|
81
|
+
async complete(messages, options) {
|
|
82
|
+
this.logger.debug(LOG_MODULE, "request", "Starting call to chat.completions", {
|
|
83
|
+
model: this.name,
|
|
84
|
+
messageCount: messages.length,
|
|
85
|
+
streaming: false,
|
|
86
|
+
toolCount: options?.tools?.length ?? 0
|
|
87
|
+
});
|
|
88
|
+
let response;
|
|
89
|
+
try {
|
|
90
|
+
response = await this.client.chat.completions.create({
|
|
91
|
+
model: this.name,
|
|
92
|
+
messages: toOpenAIMessages(messages),
|
|
93
|
+
temperature: options?.temperature ?? this.config.temperature,
|
|
94
|
+
max_tokens: options?.maxTokens ?? this.config.maxTokens,
|
|
95
|
+
tools: toOpenAITools(options?.tools),
|
|
96
|
+
...this.buildResponseFormat(options?.responseSchema)
|
|
97
|
+
}, options?.signal ? { signal: options.signal } : void 0);
|
|
98
|
+
} catch (thrown) {
|
|
99
|
+
const wrapped = wrapOpenAIError(thrown);
|
|
100
|
+
this.logger.error(LOG_MODULE, "error", wrapped.message, {
|
|
101
|
+
code: wrapped.code,
|
|
102
|
+
context: wrapped.context
|
|
103
|
+
});
|
|
104
|
+
throw wrapped;
|
|
105
|
+
}
|
|
106
|
+
const choice = response.choices[0];
|
|
107
|
+
const finishReason = mapFinishReason(choice.finish_reason);
|
|
108
|
+
const usage = this.extractUsage(response.usage);
|
|
109
|
+
this.logger.debug(LOG_MODULE, "response", "call to chat.completions succeeded", {
|
|
110
|
+
finishReason,
|
|
111
|
+
usage
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
content: choice.message.content ?? "",
|
|
115
|
+
finishReason,
|
|
116
|
+
usage,
|
|
117
|
+
toolCalls: this.extractToolCalls(choice.message.tool_calls)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Incremental streaming completion. Yields neutral `ModelStreamChunk`s —
|
|
122
|
+
* `delta` for text tokens, `tool-call` when the model requests a tool,
|
|
123
|
+
* and a terminal `done` carrying the final finish reason + usage totals.
|
|
124
|
+
* Callers consume it with `for await`.
|
|
125
|
+
*/
|
|
126
|
+
async *stream(messages, options) {
|
|
127
|
+
this.logger.debug(LOG_MODULE, "request", "Starting streaming call to chat.completions", {
|
|
128
|
+
model: this.name,
|
|
129
|
+
messageCount: messages.length,
|
|
130
|
+
streaming: true,
|
|
131
|
+
toolCount: options?.tools?.length ?? 0
|
|
132
|
+
});
|
|
133
|
+
let stream;
|
|
134
|
+
try {
|
|
135
|
+
stream = await this.client.chat.completions.create({
|
|
136
|
+
model: this.name,
|
|
137
|
+
messages: toOpenAIMessages(messages),
|
|
138
|
+
temperature: options?.temperature ?? this.config.temperature,
|
|
139
|
+
max_tokens: options?.maxTokens ?? this.config.maxTokens,
|
|
140
|
+
tools: toOpenAITools(options?.tools),
|
|
141
|
+
stream: true,
|
|
142
|
+
stream_options: { include_usage: true },
|
|
143
|
+
...this.buildResponseFormat(options?.responseSchema)
|
|
144
|
+
}, options?.signal ? { signal: options.signal } : void 0);
|
|
145
|
+
} catch (thrown) {
|
|
146
|
+
const wrapped = wrapOpenAIError(thrown);
|
|
147
|
+
this.logger.error(LOG_MODULE, "error", wrapped.message, {
|
|
148
|
+
code: wrapped.code,
|
|
149
|
+
context: wrapped.context
|
|
150
|
+
});
|
|
151
|
+
throw wrapped;
|
|
152
|
+
}
|
|
153
|
+
let rawFinishReason = "stop";
|
|
154
|
+
const usage = {
|
|
155
|
+
input: 0,
|
|
156
|
+
output: 0,
|
|
157
|
+
total: 0
|
|
158
|
+
};
|
|
159
|
+
const toolCallAccum = /* @__PURE__ */ new Map();
|
|
160
|
+
try {
|
|
161
|
+
for await (const chunk of stream) {
|
|
162
|
+
const delta = chunk.choices[0]?.delta;
|
|
163
|
+
const finish = chunk.choices[0]?.finish_reason;
|
|
164
|
+
if (delta?.content) yield {
|
|
165
|
+
type: "delta",
|
|
166
|
+
content: delta.content
|
|
167
|
+
};
|
|
168
|
+
if (delta?.tool_calls) for (const toolCall of delta.tool_calls) {
|
|
169
|
+
const idx = toolCall.index ?? 0;
|
|
170
|
+
if (!toolCallAccum.has(idx)) toolCallAccum.set(idx, {
|
|
171
|
+
id: "",
|
|
172
|
+
name: "",
|
|
173
|
+
arguments: ""
|
|
174
|
+
});
|
|
175
|
+
const acc = toolCallAccum.get(idx);
|
|
176
|
+
if (toolCall.id) acc.id = toolCall.id;
|
|
177
|
+
if (toolCall.function?.name) acc.name = toolCall.function.name;
|
|
178
|
+
if (toolCall.function?.arguments) acc.arguments += toolCall.function.arguments;
|
|
179
|
+
}
|
|
180
|
+
if (finish) rawFinishReason = finish;
|
|
181
|
+
if (chunk.usage) {
|
|
182
|
+
usage.input = chunk.usage.prompt_tokens ?? 0;
|
|
183
|
+
usage.output = chunk.usage.completion_tokens ?? 0;
|
|
184
|
+
usage.total = chunk.usage.total_tokens ?? 0;
|
|
185
|
+
const cached = chunk.usage.prompt_tokens_details?.cached_tokens;
|
|
186
|
+
if (cached !== void 0 && cached > 0) usage.cachedTokens = cached;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const acc of toolCallAccum.values()) {
|
|
190
|
+
if (!acc.name) continue;
|
|
191
|
+
yield {
|
|
192
|
+
type: "tool-call",
|
|
193
|
+
id: acc.id,
|
|
194
|
+
name: acc.name,
|
|
195
|
+
input: safeJsonParse(acc.arguments, {})
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
} catch (thrown) {
|
|
199
|
+
const wrapped = wrapOpenAIError(thrown);
|
|
200
|
+
this.logger.error(LOG_MODULE, "error", wrapped.message, {
|
|
201
|
+
code: wrapped.code,
|
|
202
|
+
context: wrapped.context
|
|
203
|
+
});
|
|
204
|
+
throw wrapped;
|
|
205
|
+
}
|
|
206
|
+
const finishReason = mapFinishReason(rawFinishReason);
|
|
207
|
+
this.logger.debug(LOG_MODULE, "response", "Streaming call to chat.completions succeeded", {
|
|
208
|
+
finishReason,
|
|
209
|
+
usage
|
|
210
|
+
});
|
|
211
|
+
yield {
|
|
212
|
+
type: "done",
|
|
213
|
+
finishReason,
|
|
214
|
+
usage
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Translate the neutral `responseSchema` option into OpenAI's
|
|
219
|
+
* `response_format` parameter.
|
|
220
|
+
*
|
|
221
|
+
* When `config.responseFormat` is set, it wins: `"text"` emits no
|
|
222
|
+
* `response_format` at all, `"json_object"` always picks the loose
|
|
223
|
+
* mode, and `"json_schema"` picks strict mode (with the same
|
|
224
|
+
* `isStrictCompatible` safety check — a malformed schema still
|
|
225
|
+
* degrades to `json_object` rather than 400). The override exists
|
|
226
|
+
* because some targets (older OpenAI models, OpenRouter routes,
|
|
227
|
+
* Ollama OpenAI-compat) reject strict `json_schema` outright.
|
|
228
|
+
*
|
|
229
|
+
* When the override is omitted, uses strict `json_schema` mode
|
|
230
|
+
* (token-level enforcement) only when the schema is a proper
|
|
231
|
+
* root-object JSON Schema (`{ type: "object", properties: ... }`).
|
|
232
|
+
* For anything else — malformed extractor output, non-object
|
|
233
|
+
* schemas, or future shapes we haven't tested — falls back to loose
|
|
234
|
+
* `json_object` mode, which guarantees *some* valid JSON without
|
|
235
|
+
* enforcing shape. The agent's soft instruction already embeds the
|
|
236
|
+
* schema text in the system prompt when the model declares no
|
|
237
|
+
* native structured-output capability, so shape validation still
|
|
238
|
+
* runs client-side via the Standard Schema `validate()` call.
|
|
239
|
+
*
|
|
240
|
+
* Returns an empty spread when no schema was supplied, so the caller
|
|
241
|
+
* can unconditionally `...buildResponseFormat(...)` into the request.
|
|
242
|
+
*/
|
|
243
|
+
buildResponseFormat(responseSchema) {
|
|
244
|
+
if (!responseSchema) return {};
|
|
245
|
+
const override = this.config.responseFormat;
|
|
246
|
+
if (override === "text") return {};
|
|
247
|
+
if (override === "json_object") return { response_format: { type: "json_object" } };
|
|
248
|
+
if (this.isStrictCompatible(responseSchema)) return { response_format: {
|
|
249
|
+
type: "json_schema",
|
|
250
|
+
json_schema: {
|
|
251
|
+
name: "response",
|
|
252
|
+
schema: responseSchema,
|
|
253
|
+
strict: true
|
|
254
|
+
}
|
|
255
|
+
} };
|
|
256
|
+
return { response_format: { type: "json_object" } };
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* OpenAI strict `json_schema` mode requires the root to be a JSON
|
|
260
|
+
* Schema object type (`{ type: "object", properties: ... }`). Anything
|
|
261
|
+
* else (top-level arrays, primitives, unknown shapes) is rejected with
|
|
262
|
+
* a 400 before a token is sampled. We check structurally here so the
|
|
263
|
+
* first call doesn't crash on a malformed extraction — loose
|
|
264
|
+
* `json_object` mode is a safe degradation.
|
|
265
|
+
*/
|
|
266
|
+
isStrictCompatible(schema) {
|
|
267
|
+
return schema.type === "object" && typeof schema.properties === "object" && schema.properties !== null;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Normalize OpenAI's `usage` block (which may be absent on some responses
|
|
271
|
+
* or partials) into the neutral `Usage` shape. Missing usage collapses to
|
|
272
|
+
* zeros rather than propagating `undefined`, so downstream aggregation
|
|
273
|
+
* math stays safe.
|
|
274
|
+
*/
|
|
275
|
+
extractUsage(raw) {
|
|
276
|
+
if (!raw) return {
|
|
277
|
+
input: 0,
|
|
278
|
+
output: 0,
|
|
279
|
+
total: 0
|
|
280
|
+
};
|
|
281
|
+
const cachedTokens = raw.prompt_tokens_details?.cached_tokens;
|
|
282
|
+
return {
|
|
283
|
+
input: raw.prompt_tokens,
|
|
284
|
+
output: raw.completion_tokens,
|
|
285
|
+
total: raw.total_tokens,
|
|
286
|
+
...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Reshape OpenAI's `tool_calls` array into the neutral
|
|
291
|
+
* `ModelToolCallRequest[]`. The raw `arguments` field is a JSON string
|
|
292
|
+
* per OpenAI's protocol — we parse it defensively via `safeJsonParse` so
|
|
293
|
+
* malformed or empty arguments yield an empty object instead of crashing
|
|
294
|
+
* the trip. Returns `undefined` when no tools were requested so callers
|
|
295
|
+
* can branch on presence.
|
|
296
|
+
*/
|
|
297
|
+
extractToolCalls(rawToolCalls) {
|
|
298
|
+
if (!rawToolCalls || rawToolCalls.length === 0) return;
|
|
299
|
+
return rawToolCalls.map((toolCall) => ({
|
|
300
|
+
id: toolCall.id,
|
|
301
|
+
name: toolCall.function.name,
|
|
302
|
+
input: safeJsonParse(toolCall.function.arguments, {})
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
//#endregion
|
|
308
|
+
export { OpenAIModel };
|
|
309
|
+
//# sourceMappingURL=model.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.mjs","names":[],"sources":["../../../../../@warlock.js/ai-openai/src/model.ts"],"sourcesContent":["import {\n safeJsonParse,\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type OpenAI from \"openai\";\nimport type { OpenAIModelConfig, OpenAIResponseFormat } from \"./config.type\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport { mapFinishReason, toOpenAIMessages, toOpenAITools, wrapOpenAIError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.openai\";\n\n/**\n * Map an explicit `responseFormat` override to the default\n * `structuredOutput` capability. Loose wire modes (`\"json_object\"`,\n * `\"text\"`) don't enforce shape, so the agent needs to see the soft\n * schema hint in the system prompt — that only happens when the\n * capability is `false`. Default (no override) stays `true` to\n * preserve the prior assumption that OpenAI models support strict\n * structured output.\n */\nfunction inferStructuredOutput(responseFormat: OpenAIResponseFormat | undefined): boolean {\n if (responseFormat === \"json_object\" || responseFormat === \"text\") {\n return false;\n }\n\n return true;\n}\n\n/**\n * OpenAI-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and the official `openai` SDK. Agents,\n * workflows, and supervisors never talk to OpenAI directly — they hold a\n * `ModelContract`, and this class is what makes that contract concrete for\n * any OpenAI-compatible endpoint (OpenAI, Azure OpenAI, OpenRouter, local\n * gateways that speak the Chat Completions protocol).\n *\n * **Responsibility.**\n * - Owns: a long-lived `OpenAI` client + frozen `ModelConfig` (name,\n * temperature, maxTokens) used as defaults for every call.\n * - Owns: translating vendor-neutral `Message[]` and\n * `ToolContract[]` into OpenAI wire shapes on the way out, and\n * translating OpenAI's response (content, finish reason, tool calls,\n * usage) back into the neutral shapes on the way in.\n * - Does NOT own: dispatching tools, deciding whether to loop, tracking\n * conversation history, or retrying on failure — those are agent\n * concerns. The model is a stateless (per-call) protocol adapter.\n *\n * Because it holds a live client and shared defaults, it is modeled as a\n * class (see §4.2 of code-style.md — \"long-lived state across calls\").\n *\n * @example\n * import OpenAI from \"openai\";\n * const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n * const model = new OpenAIModel(client, { name: \"gpt-4o\", temperature: 0.3 });\n *\n * const myAgent = agent({\n * model,\n * systemPrompt: \"You are a helpful assistant.\",\n * tools: [searchTool],\n * });\n *\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class OpenAIModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly client: OpenAI;\n private readonly config: OpenAIModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(client: OpenAI, config: OpenAIModelConfig, provider: string = \"openai\") {\n this.client = client;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),\n vision: config.vision ?? inferVisionCapability(config.name),\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to the Chat\n * Completions endpoint, waits for the terminal response, and reshapes it\n * into a vendor-neutral `ModelResponse`. Per-call `options` override the\n * instance's `ModelConfig` defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n // Per-call request/response logs are hot-path in production agents\n // — keep them at `debug` so `info` stays reserved for lifecycle\n // events (agent starting/completed, etc.). Operators who need to\n // audit every LLM call can raise log-level at runtime.\n this.logger.debug(LOG_MODULE, \"request\", \"Starting call to chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let response: OpenAI.Chat.Completions.ChatCompletion;\n\n try {\n response = await this.client.chat.completions.create(\n {\n model: this.name,\n messages: toOpenAIMessages(messages),\n temperature: options?.temperature ?? this.config.temperature,\n max_tokens: options?.maxTokens ?? this.config.maxTokens,\n tools: toOpenAITools(options?.tools),\n ...this.buildResponseFormat(options?.responseSchema),\n },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const choice = response.choices[0];\n const finishReason = mapFinishReason(choice.finish_reason);\n const usage = this.extractUsage(response.usage);\n\n this.logger.debug(LOG_MODULE, \"response\", \"call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n return {\n content: choice.message.content ?? \"\",\n finishReason,\n usage,\n toolCalls: this.extractToolCalls(choice.message.tool_calls),\n };\n }\n\n /**\n * Incremental streaming completion. Yields neutral `ModelStreamChunk`s —\n * `delta` for text tokens, `tool-call` when the model requests a tool,\n * and a terminal `done` carrying the final finish reason + usage totals.\n * Callers consume it with `for await`.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting streaming call to chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let stream: Awaited<ReturnType<typeof this.client.chat.completions.create>>;\n\n try {\n stream = await this.client.chat.completions.create(\n {\n model: this.name,\n messages: toOpenAIMessages(messages),\n temperature: options?.temperature ?? this.config.temperature,\n max_tokens: options?.maxTokens ?? this.config.maxTokens,\n tools: toOpenAITools(options?.tools),\n stream: true,\n stream_options: { include_usage: true },\n ...this.buildResponseFormat(options?.responseSchema),\n },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n let rawFinishReason: string = \"stop\";\n const usage: Usage = { input: 0, output: 0, total: 0 };\n const toolCallAccum = new Map<number, { id: string; name: string; arguments: string }>();\n\n try {\n for await (const chunk of stream as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>) {\n const delta = chunk.choices[0]?.delta;\n const finish = chunk.choices[0]?.finish_reason;\n\n if (delta?.content) {\n yield { type: \"delta\", content: delta.content };\n }\n\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n const idx = toolCall.index ?? 0;\n if (!toolCallAccum.has(idx)) {\n toolCallAccum.set(idx, { id: \"\", name: \"\", arguments: \"\" });\n }\n const acc = toolCallAccum.get(idx)!;\n if (toolCall.id) acc.id = toolCall.id;\n if (toolCall.function?.name) acc.name = toolCall.function.name;\n if (toolCall.function?.arguments) acc.arguments += toolCall.function.arguments;\n }\n }\n\n if (finish) {\n rawFinishReason = finish;\n }\n\n if (chunk.usage) {\n usage.input = chunk.usage.prompt_tokens ?? 0;\n usage.output = chunk.usage.completion_tokens ?? 0;\n usage.total = chunk.usage.total_tokens ?? 0;\n const cached = chunk.usage.prompt_tokens_details?.cached_tokens;\n if (cached !== undefined && cached > 0) {\n usage.cachedTokens = cached;\n }\n }\n }\n\n for (const acc of toolCallAccum.values()) {\n // Skip accumulators that never received a function name — those\n // are partial fragments the model started but never identified\n // (e.g. arguments-only deltas with no originating `id`/`name`).\n // Yielding them produces nameless tool-calls the agent runtime\n // can't dispatch and would mis-attribute as a registered tool.\n if (!acc.name) continue;\n\n yield {\n type: \"tool-call\",\n id: acc.id,\n name: acc.name,\n input: safeJsonParse<Record<string, unknown>>(acc.arguments, {}),\n };\n }\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const finishReason = mapFinishReason(rawFinishReason);\n\n this.logger.debug(LOG_MODULE, \"response\", \"Streaming call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Translate the neutral `responseSchema` option into OpenAI's\n * `response_format` parameter.\n *\n * When `config.responseFormat` is set, it wins: `\"text\"` emits no\n * `response_format` at all, `\"json_object\"` always picks the loose\n * mode, and `\"json_schema\"` picks strict mode (with the same\n * `isStrictCompatible` safety check — a malformed schema still\n * degrades to `json_object` rather than 400). The override exists\n * because some targets (older OpenAI models, OpenRouter routes,\n * Ollama OpenAI-compat) reject strict `json_schema` outright.\n *\n * When the override is omitted, uses strict `json_schema` mode\n * (token-level enforcement) only when the schema is a proper\n * root-object JSON Schema (`{ type: \"object\", properties: ... }`).\n * For anything else — malformed extractor output, non-object\n * schemas, or future shapes we haven't tested — falls back to loose\n * `json_object` mode, which guarantees *some* valid JSON without\n * enforcing shape. The agent's soft instruction already embeds the\n * schema text in the system prompt when the model declares no\n * native structured-output capability, so shape validation still\n * runs client-side via the Standard Schema `validate()` call.\n *\n * Returns an empty spread when no schema was supplied, so the caller\n * can unconditionally `...buildResponseFormat(...)` into the request.\n */\n private buildResponseFormat(responseSchema: Record<string, unknown> | undefined): {\n response_format?: OpenAI.Chat.Completions.ChatCompletionCreateParams[\"response_format\"];\n } {\n if (!responseSchema) {\n return {};\n }\n\n const override = this.config.responseFormat;\n\n if (override === \"text\") {\n return {};\n }\n\n if (override === \"json_object\") {\n return { response_format: { type: \"json_object\" } };\n }\n\n // Either auto-select (no override) or explicit `\"json_schema\"`.\n // The strict-compat check still applies in the explicit case —\n // a malformed / non-object schema would 400 before sampling, so\n // we degrade to `json_object` rather than crash.\n if (this.isStrictCompatible(responseSchema)) {\n return {\n response_format: {\n type: \"json_schema\",\n json_schema: {\n name: \"response\",\n schema: responseSchema,\n strict: true,\n },\n },\n };\n }\n\n return { response_format: { type: \"json_object\" } };\n }\n\n /**\n * OpenAI strict `json_schema` mode requires the root to be a JSON\n * Schema object type (`{ type: \"object\", properties: ... }`). Anything\n * else (top-level arrays, primitives, unknown shapes) is rejected with\n * a 400 before a token is sampled. We check structurally here so the\n * first call doesn't crash on a malformed extraction — loose\n * `json_object` mode is a safe degradation.\n */\n private isStrictCompatible(schema: Record<string, unknown>): boolean {\n return (\n schema.type === \"object\" &&\n typeof schema.properties === \"object\" &&\n schema.properties !== null\n );\n }\n\n /**\n * Normalize OpenAI's `usage` block (which may be absent on some responses\n * or partials) into the neutral `Usage` shape. Missing usage collapses to\n * zeros rather than propagating `undefined`, so downstream aggregation\n * math stays safe.\n */\n private extractUsage(raw: OpenAI.Completions.CompletionUsage | undefined): Usage {\n if (!raw) {\n return { input: 0, output: 0, total: 0 };\n }\n\n const cachedTokens = raw.prompt_tokens_details?.cached_tokens;\n\n return {\n input: raw.prompt_tokens,\n output: raw.completion_tokens,\n total: raw.total_tokens,\n ...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),\n };\n }\n\n /**\n * Reshape OpenAI's `tool_calls` array into the neutral\n * `ModelToolCallRequest[]`. The raw `arguments` field is a JSON string\n * per OpenAI's protocol — we parse it defensively via `safeJsonParse` so\n * malformed or empty arguments yield an empty object instead of crashing\n * the trip. Returns `undefined` when no tools were requested so callers\n * can branch on presence.\n */\n private extractToolCalls(\n rawToolCalls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[] | undefined,\n ): ModelToolCallRequest[] | undefined {\n if (!rawToolCalls || rawToolCalls.length === 0) {\n return undefined;\n }\n\n return rawToolCalls.map((toolCall) => ({\n id: toolCall.id,\n name: (toolCall as any).function.name,\n input: safeJsonParse<Record<string, unknown>>((toolCall as any).function.arguments, {}),\n }));\n }\n}\n"],"mappings":"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"}
|
package/esm/sdk.d.mts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { OpenAIEmbedderConfig, OpenAIModelConfig, OpenAISDKConfig } from "./config.type.mjs";
|
|
2
|
+
import { EmbedderContract, ModelContract, SDKAdapterContract } from "@warlock.js/ai";
|
|
3
|
+
|
|
4
|
+
//#region ../../@warlock.js/ai-openai/src/sdk.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* OpenAI-backed implementation of `SDKAdapterContract`.
|
|
7
|
+
*
|
|
8
|
+
* **Role.** The package entry point for any OpenAI-compatible provider
|
|
9
|
+
* (OpenAI, Azure OpenAI, OpenRouter, local gateways speaking the Chat
|
|
10
|
+
* Completions protocol). A single `OpenAISDK` instance holds one live
|
|
11
|
+
* `OpenAI` client, shared by every `ModelContract` it produces via
|
|
12
|
+
* `model()`. Users construct one SDK per provider/account and reuse it
|
|
13
|
+
* across all agents, workflows, and supervisors that target that
|
|
14
|
+
* provider.
|
|
15
|
+
*
|
|
16
|
+
* **Responsibility.**
|
|
17
|
+
* - Owns: a long-lived `OpenAI` client (authentication, base URL) and
|
|
18
|
+
* its lifetime scope. Factory for `OpenAIModel` instances — each
|
|
19
|
+
* model call gets a reference to the same client.
|
|
20
|
+
* - Does NOT own: anything per-call (tool execution, message history,
|
|
21
|
+
* streaming loop) — those live in `OpenAIModel` and the agent runtime.
|
|
22
|
+
*
|
|
23
|
+
* Modeled as a class (see §4.2 of code-style.md — "long-lived state
|
|
24
|
+
* across many calls"): the `OpenAI` client is heavy to construct and
|
|
25
|
+
* designed to be reused; keeping it on `this` makes that reuse
|
|
26
|
+
* explicit and aligns with the PascalCase naming convention readers
|
|
27
|
+
* expect from a constructor.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });
|
|
31
|
+
* const model = openai.model({ name: "gpt-4o", temperature: 0.7 });
|
|
32
|
+
* const tokens = await openai.count("Hello world");
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Compose into an `ai.openai` namespace for ergonomic agent wiring
|
|
36
|
+
* const ai = { agent, tool, systemPrompt, persona, instruction, openai: new OpenAISDK({ apiKey }) };
|
|
37
|
+
* const myAgent = ai.agent({ model: ai.openai.model({ name: "gpt-4o-mini" }) });
|
|
38
|
+
*/
|
|
39
|
+
declare class OpenAISDK implements SDKAdapterContract {
|
|
40
|
+
private readonly client;
|
|
41
|
+
private readonly provider;
|
|
42
|
+
private readonly pricing?;
|
|
43
|
+
constructor(config: OpenAISDKConfig);
|
|
44
|
+
/**
|
|
45
|
+
* Build an `OpenAIModel` bound to this SDK's client. Each call returns
|
|
46
|
+
* a fresh model instance, but all instances share the underlying
|
|
47
|
+
* `OpenAI` client — connection pools, rate limits, and authentication
|
|
48
|
+
* state stay unified across every model produced here. The SDK's
|
|
49
|
+
* `provider` label is forwarded so every model self-identifies as
|
|
50
|
+
* coming from the same upstream.
|
|
51
|
+
*
|
|
52
|
+
* Pricing resolution: per-model `config.pricing` wins; otherwise the
|
|
53
|
+
* SDK-level registry entry keyed by `config.name`; otherwise
|
|
54
|
+
* `undefined` (no cost computed).
|
|
55
|
+
*/
|
|
56
|
+
model(config: OpenAIModelConfig): ModelContract;
|
|
57
|
+
/**
|
|
58
|
+
* Rough token-count estimate for a given text. Uses a
|
|
59
|
+
* character-heuristic (`approximateTokenCount`) from the core package
|
|
60
|
+
* — good enough for budgeting and quota guards, not for billing.
|
|
61
|
+
* Accepts an optional model id for future per-model tokenizer
|
|
62
|
+
* dispatch; currently ignored.
|
|
63
|
+
*/
|
|
64
|
+
count(text: string, _model?: string): Promise<number>;
|
|
65
|
+
/**
|
|
66
|
+
* Build an `OpenAIEmbedder` bound to this SDK's client. Each call
|
|
67
|
+
* returns a fresh embedder instance sharing the same underlying
|
|
68
|
+
* `OpenAI` client — connection pools and authentication stay unified
|
|
69
|
+
* across every embedder produced here.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* const embedder = openai.embedder({ name: "text-embedding-3-small" });
|
|
73
|
+
* const { vector } = await embedder.embed("Hello world");
|
|
74
|
+
*/
|
|
75
|
+
embedder(config: OpenAIEmbedderConfig): EmbedderContract;
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { OpenAISDK };
|
|
79
|
+
//# sourceMappingURL=sdk.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../@warlock.js/ai-openai/src/sdk.ts"],"mappings":";;;;;;AA8CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,SAAA,YAAqB,kBAAA;EAAA,iBACf,MAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;cAEE,MAAA,EAAQ,eAAA;;;;;;;;;;;;;EAqBpB,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,aAAA;;;;;;;;EAe5B,KAAA,CAAM,IAAA,UAAc,MAAA,YAAkB,OAAA;;;;;;;;;;;EAc5C,QAAA,CAAS,MAAA,EAAQ,oBAAA,GAAuB,gBAAA;AAAA"}
|
package/esm/sdk.mjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { OpenAIEmbedder } from "./embedder.mjs";
|
|
2
|
+
import { OpenAIModel } from "./model.mjs";
|
|
3
|
+
import OpenAI from "openai";
|
|
4
|
+
import { approximateTokenCount } from "@warlock.js/ai";
|
|
5
|
+
|
|
6
|
+
//#region ../../@warlock.js/ai-openai/src/sdk.ts
|
|
7
|
+
/**
|
|
8
|
+
* OpenAI-backed implementation of `SDKAdapterContract`.
|
|
9
|
+
*
|
|
10
|
+
* **Role.** The package entry point for any OpenAI-compatible provider
|
|
11
|
+
* (OpenAI, Azure OpenAI, OpenRouter, local gateways speaking the Chat
|
|
12
|
+
* Completions protocol). A single `OpenAISDK` instance holds one live
|
|
13
|
+
* `OpenAI` client, shared by every `ModelContract` it produces via
|
|
14
|
+
* `model()`. Users construct one SDK per provider/account and reuse it
|
|
15
|
+
* across all agents, workflows, and supervisors that target that
|
|
16
|
+
* provider.
|
|
17
|
+
*
|
|
18
|
+
* **Responsibility.**
|
|
19
|
+
* - Owns: a long-lived `OpenAI` client (authentication, base URL) and
|
|
20
|
+
* its lifetime scope. Factory for `OpenAIModel` instances — each
|
|
21
|
+
* model call gets a reference to the same client.
|
|
22
|
+
* - Does NOT own: anything per-call (tool execution, message history,
|
|
23
|
+
* streaming loop) — those live in `OpenAIModel` and the agent runtime.
|
|
24
|
+
*
|
|
25
|
+
* Modeled as a class (see §4.2 of code-style.md — "long-lived state
|
|
26
|
+
* across many calls"): the `OpenAI` client is heavy to construct and
|
|
27
|
+
* designed to be reused; keeping it on `this` makes that reuse
|
|
28
|
+
* explicit and aligns with the PascalCase naming convention readers
|
|
29
|
+
* expect from a constructor.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });
|
|
33
|
+
* const model = openai.model({ name: "gpt-4o", temperature: 0.7 });
|
|
34
|
+
* const tokens = await openai.count("Hello world");
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // Compose into an `ai.openai` namespace for ergonomic agent wiring
|
|
38
|
+
* const ai = { agent, tool, systemPrompt, persona, instruction, openai: new OpenAISDK({ apiKey }) };
|
|
39
|
+
* const myAgent = ai.agent({ model: ai.openai.model({ name: "gpt-4o-mini" }) });
|
|
40
|
+
*/
|
|
41
|
+
var OpenAISDK = class {
|
|
42
|
+
constructor(config) {
|
|
43
|
+
this.client = new OpenAI({
|
|
44
|
+
apiKey: config.apiKey,
|
|
45
|
+
baseURL: config.baseURL
|
|
46
|
+
});
|
|
47
|
+
this.provider = config.provider ?? "openai";
|
|
48
|
+
this.pricing = config.pricing;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build an `OpenAIModel` bound to this SDK's client. Each call returns
|
|
52
|
+
* a fresh model instance, but all instances share the underlying
|
|
53
|
+
* `OpenAI` client — connection pools, rate limits, and authentication
|
|
54
|
+
* state stay unified across every model produced here. The SDK's
|
|
55
|
+
* `provider` label is forwarded so every model self-identifies as
|
|
56
|
+
* coming from the same upstream.
|
|
57
|
+
*
|
|
58
|
+
* Pricing resolution: per-model `config.pricing` wins; otherwise the
|
|
59
|
+
* SDK-level registry entry keyed by `config.name`; otherwise
|
|
60
|
+
* `undefined` (no cost computed).
|
|
61
|
+
*/
|
|
62
|
+
model(config) {
|
|
63
|
+
const resolvedPricing = config.pricing ?? this.pricing?.[config.name];
|
|
64
|
+
const resolvedConfig = resolvedPricing === config.pricing ? config : {
|
|
65
|
+
...config,
|
|
66
|
+
pricing: resolvedPricing
|
|
67
|
+
};
|
|
68
|
+
return new OpenAIModel(this.client, resolvedConfig, this.provider);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Rough token-count estimate for a given text. Uses a
|
|
72
|
+
* character-heuristic (`approximateTokenCount`) from the core package
|
|
73
|
+
* — good enough for budgeting and quota guards, not for billing.
|
|
74
|
+
* Accepts an optional model id for future per-model tokenizer
|
|
75
|
+
* dispatch; currently ignored.
|
|
76
|
+
*/
|
|
77
|
+
async count(text, _model) {
|
|
78
|
+
return approximateTokenCount(text);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build an `OpenAIEmbedder` bound to this SDK's client. Each call
|
|
82
|
+
* returns a fresh embedder instance sharing the same underlying
|
|
83
|
+
* `OpenAI` client — connection pools and authentication stay unified
|
|
84
|
+
* across every embedder produced here.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* const embedder = openai.embedder({ name: "text-embedding-3-small" });
|
|
88
|
+
* const { vector } = await embedder.embed("Hello world");
|
|
89
|
+
*/
|
|
90
|
+
embedder(config) {
|
|
91
|
+
return new OpenAIEmbedder(this.client, config);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
//#endregion
|
|
96
|
+
export { OpenAISDK };
|
|
97
|
+
//# sourceMappingURL=sdk.mjs.map
|
package/esm/sdk.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk.mjs","names":[],"sources":["../../../../../@warlock.js/ai-openai/src/sdk.ts"],"sourcesContent":["import OpenAI from \"openai\";\nimport type {\n EmbedderContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type { OpenAIEmbedderConfig, OpenAIModelConfig, OpenAISDKConfig } from \"./config.type\";\nimport { OpenAIEmbedder } from \"./embedder\";\nimport { OpenAIModel } from \"./model\";\n\n/**\n * OpenAI-backed implementation of `SDKAdapterContract`.\n *\n * **Role.** The package entry point for any OpenAI-compatible provider\n * (OpenAI, Azure OpenAI, OpenRouter, local gateways speaking the Chat\n * Completions protocol). A single `OpenAISDK` instance holds one live\n * `OpenAI` client, shared by every `ModelContract` it produces via\n * `model()`. Users construct one SDK per provider/account and reuse it\n * across all agents, workflows, and supervisors that target that\n * provider.\n *\n * **Responsibility.**\n * - Owns: a long-lived `OpenAI` client (authentication, base URL) and\n * its lifetime scope. Factory for `OpenAIModel` instances — each\n * model call gets a reference to the same client.\n * - Does NOT own: anything per-call (tool execution, message history,\n * streaming loop) — those live in `OpenAIModel` and the agent runtime.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across many calls\"): the `OpenAI` client is heavy to construct and\n * designed to be reused; keeping it on `this` makes that reuse\n * explicit and aligns with the PascalCase naming convention readers\n * expect from a constructor.\n *\n * @example\n * const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });\n * const model = openai.model({ name: \"gpt-4o\", temperature: 0.7 });\n * const tokens = await openai.count(\"Hello world\");\n *\n * @example\n * // Compose into an `ai.openai` namespace for ergonomic agent wiring\n * const ai = { agent, tool, systemPrompt, persona, instruction, openai: new OpenAISDK({ apiKey }) };\n * const myAgent = ai.agent({ model: ai.openai.model({ name: \"gpt-4o-mini\" }) });\n */\nexport class OpenAISDK implements SDKAdapterContract {\n private readonly client: OpenAI;\n private readonly provider: string;\n private readonly pricing?: Record<string, ModelPricing>;\n\n public constructor(config: OpenAISDKConfig) {\n this.client = new OpenAI({\n apiKey: config.apiKey,\n baseURL: config.baseURL,\n });\n this.provider = config.provider ?? \"openai\";\n this.pricing = config.pricing;\n }\n\n /**\n * Build an `OpenAIModel` bound to this SDK's client. Each call returns\n * a fresh model instance, but all instances share the underlying\n * `OpenAI` client — connection pools, rate limits, and authentication\n * state stay unified across every model produced here. The SDK's\n * `provider` label is forwarded so every model self-identifies as\n * coming from the same upstream.\n *\n * Pricing resolution: per-model `config.pricing` wins; otherwise the\n * SDK-level registry entry keyed by `config.name`; otherwise\n * `undefined` (no cost computed).\n */\n public model(config: OpenAIModelConfig): ModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: OpenAIModelConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new OpenAIModel(this.client, resolvedConfig, this.provider);\n }\n\n /**\n * Rough token-count estimate for a given text. Uses a\n * character-heuristic (`approximateTokenCount`) from the core package\n * — good enough for budgeting and quota guards, not for billing.\n * Accepts an optional model id for future per-model tokenizer\n * dispatch; currently ignored.\n */\n public async count(text: string, _model?: string): Promise<number> {\n return approximateTokenCount(text);\n }\n\n /**\n * Build an `OpenAIEmbedder` bound to this SDK's client. Each call\n * returns a fresh embedder instance sharing the same underlying\n * `OpenAI` client — connection pools and authentication stay unified\n * across every embedder produced here.\n *\n * @example\n * const embedder = openai.embedder({ name: \"text-embedding-3-small\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n */\n public embedder(config: OpenAIEmbedderConfig): EmbedderContract {\n return new OpenAIEmbedder(this.client, config);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,IAAa,YAAb,MAAqD;CAKnD,AAAO,YAAY,QAAyB;EAC1C,KAAK,SAAS,IAAI,OAAO;GACvB,QAAQ,OAAO;GACf,SAAS,OAAO;EAClB,CAAC;EACD,KAAK,WAAW,OAAO,YAAY;EACnC,KAAK,UAAU,OAAO;CACxB;;;;;;;;;;;;;CAcA,AAAO,MAAM,QAA0C;EACrD,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,OAAO,IAAI,YAAY,KAAK,QAAQ,gBAAgB,KAAK,QAAQ;CACnE;;;;;;;;CASA,MAAa,MAAM,MAAc,QAAkC;EACjE,OAAO,sBAAsB,IAAI;CACnC;;;;;;;;;;;CAYA,AAAO,SAAS,QAAgD;EAC9D,OAAO,IAAI,eAAe,KAAK,QAAQ,MAAM;CAC/C;AACF"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region ../../@warlock.js/ai-openai/src/utils/map-finish-reason.ts
|
|
2
|
+
const finishReasonMap = {
|
|
3
|
+
stop: "stop",
|
|
4
|
+
tool_calls: "tool_calls",
|
|
5
|
+
length: "length"
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Map the raw OpenAI `finish_reason` string to the normalized FinishReason union.
|
|
9
|
+
* Unknown/unexpected values fall through to "error".
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* mapFinishReason("stop"); // "stop"
|
|
13
|
+
* mapFinishReason("tool_calls"); // "tool_calls"
|
|
14
|
+
* mapFinishReason(null); // "error"
|
|
15
|
+
*/
|
|
16
|
+
function mapFinishReason(raw) {
|
|
17
|
+
return finishReasonMap[raw ?? ""] ?? "error";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { mapFinishReason };
|
|
22
|
+
//# sourceMappingURL=map-finish-reason.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map-finish-reason.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-openai/src/utils/map-finish-reason.ts"],"sourcesContent":["import type { FinishReason } from \"@warlock.js/ai\";\n\nconst finishReasonMap: Record<string, FinishReason> = {\n stop: \"stop\",\n tool_calls: \"tool_calls\",\n length: \"length\",\n};\n\n/**\n * Map the raw OpenAI `finish_reason` string to the normalized FinishReason union.\n * Unknown/unexpected values fall through to \"error\".\n *\n * @example\n * mapFinishReason(\"stop\"); // \"stop\"\n * mapFinishReason(\"tool_calls\"); // \"tool_calls\"\n * mapFinishReason(null); // \"error\"\n */\nexport function mapFinishReason(raw: string | null | undefined): FinishReason {\n return finishReasonMap[raw ?? \"\"] ?? \"error\";\n}\n"],"mappings":";AAEA,MAAM,kBAAgD;CACpD,MAAM;CACN,YAAY;CACZ,QAAQ;AACV;;;;;;;;;;AAWA,SAAgB,gBAAgB,KAA8C;CAC5E,OAAO,gBAAgB,OAAO,OAAO;AACvC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//#region ../../@warlock.js/ai-openai/src/utils/to-openai-messages.ts
|
|
2
|
+
/**
|
|
3
|
+
* Convert vendor-neutral Message[] to OpenAI's chat message shape.
|
|
4
|
+
* Handles the `tool` role (requires `tool_call_id`) and assistant messages
|
|
5
|
+
* that carry `toolCalls` from a prior model response.
|
|
6
|
+
*
|
|
7
|
+
* Multipart `content` (a `ContentPart[]`) is mapped into OpenAI's user-message
|
|
8
|
+
* content-parts shape: text becomes `{ type: "text", text }`, images become
|
|
9
|
+
* `{ type: "image_url", image_url: { url } }` — with base64 sources rendered
|
|
10
|
+
* as `data:` URLs inline.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const openaiMessages = toOpenAIMessages([
|
|
14
|
+
* { role: "user", content: "Hi" },
|
|
15
|
+
* { role: "tool", toolCallId: "call_1", content: '{"ok":true}' },
|
|
16
|
+
* ]);
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* toOpenAIMessages([
|
|
20
|
+
* { role: "user", content: [
|
|
21
|
+
* { type: "text", text: "What is this?" },
|
|
22
|
+
* { type: "image", source: { url: "https://example.com/cat.jpg" } },
|
|
23
|
+
* ]},
|
|
24
|
+
* ]);
|
|
25
|
+
*/
|
|
26
|
+
function toOpenAIMessages(messages) {
|
|
27
|
+
return messages.map((m) => {
|
|
28
|
+
if (m.role === "tool") return {
|
|
29
|
+
role: "tool",
|
|
30
|
+
content: stringifyContent(m.content),
|
|
31
|
+
tool_call_id: m.toolCallId ?? ""
|
|
32
|
+
};
|
|
33
|
+
if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0) return {
|
|
34
|
+
role: "assistant",
|
|
35
|
+
content: stringifyContent(m.content),
|
|
36
|
+
tool_calls: m.toolCalls.map((tc) => ({
|
|
37
|
+
id: tc.id,
|
|
38
|
+
type: "function",
|
|
39
|
+
function: {
|
|
40
|
+
name: tc.name,
|
|
41
|
+
arguments: JSON.stringify(tc.input ?? {})
|
|
42
|
+
}
|
|
43
|
+
}))
|
|
44
|
+
};
|
|
45
|
+
if (m.role === "user" && Array.isArray(m.content)) return {
|
|
46
|
+
role: "user",
|
|
47
|
+
content: m.content.map(toOpenAIContentPart)
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
role: m.role,
|
|
51
|
+
content: stringifyContent(m.content)
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Multipart content is only meaningful on user messages — for any other
|
|
57
|
+
* role (system / assistant text / tool), collapse a `ContentPart[]` to
|
|
58
|
+
* its concatenated text so OpenAI's wire format stays valid. Plain
|
|
59
|
+
* strings pass through unchanged.
|
|
60
|
+
*/
|
|
61
|
+
function stringifyContent(content) {
|
|
62
|
+
if (typeof content === "string") return content;
|
|
63
|
+
return content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
64
|
+
}
|
|
65
|
+
function toOpenAIContentPart(part) {
|
|
66
|
+
if (part.type === "text") return {
|
|
67
|
+
type: "text",
|
|
68
|
+
text: part.text
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
type: "image_url",
|
|
72
|
+
image_url: { url: "url" in part.source ? part.source.url : `data:${part.source.mediaType};base64,${part.source.base64}` }
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { toOpenAIMessages };
|
|
78
|
+
//# sourceMappingURL=to-openai-messages.mjs.map
|