@makaio/adapter-openai-node 1.0.0-dev-1779051654000

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/dist/index.mjs ADDED
@@ -0,0 +1,1790 @@
1
+ import { AIAdapter, ProceduralConnectorTurn, ScopedToolApprovalSchema, UserMessageQueue, createAdapterNamespace, createToolApprovalHandler, formatContextBlocksAsText, isTextLikeMimeType, mergeScopedToolApproval, normalizeMimeType, resolveConformanceTestPreset, resolveTestConfig, serializeBlockToText, serializeTurnContext } from "@makaio/framework/adapters";
2
+ import OpenAI from "openai";
3
+ import { AgentCompleteEventSchema, AgentStartedEventSchema, BaseStreamAgent, BaseStreamConnector, BaseStreamSession, ErrorEventSchema, MessageCompleteEventSchema, ReasoningCompleteEventSchema, ReasoningDeltaEventSchema, ToolCallsEventSchema, ToolCompletedEventSchema, ToolStartedEventSchema, TurnStateChangedSchema, filterToolsWithSchema, handleToolCalls, loadToolsFromRegistry } from "@makaio/framework/adapters/stream-session";
4
+ import { createAdapterConfigFactory, resolveConnectorCredentials } from "@makaio/framework/adapters/config";
5
+ import { z } from "zod";
6
+ import path from "node:path";
7
+ import { AuthenticationError, ModelUnavailableError, QuotaExceededError, RateLimitError } from "@makaio/framework/core";
8
+
9
+ //#region src/types/index.ts
10
+ /**
11
+ * OpenAI Node adapter namespace identifier
12
+ */
13
+ const OPENAI_NODE_NAMESPACE = "adapter:openai-node";
14
+
15
+ //#endregion
16
+ //#region src/namespaces/schemas/chunk.ts
17
+ /**
18
+ * Schema for tool call deltas within streaming chunks.
19
+ * Matches OpenAI ChatCompletionChunk.Choice.Delta.ToolCall structure.
20
+ */
21
+ const ToolCallDeltaSchema = z.object({
22
+ index: z.number(),
23
+ id: z.string().optional(),
24
+ type: z.literal("function").optional(),
25
+ function: z.object({
26
+ name: z.string().optional(),
27
+ arguments: z.string().optional()
28
+ }).optional()
29
+ });
30
+ /**
31
+ * Schema for streaming delta within a choice.
32
+ * Matches OpenAI ChatCompletionChunk.Choice.Delta structure.
33
+ */
34
+ const DeltaSchema = z.object({
35
+ content: z.string().nullish(),
36
+ role: z.enum([
37
+ "assistant",
38
+ "user",
39
+ "system",
40
+ "tool",
41
+ "developer"
42
+ ]).optional(),
43
+ refusal: z.string().nullish(),
44
+ tool_calls: z.array(ToolCallDeltaSchema).optional()
45
+ });
46
+ /**
47
+ * Schema for a single choice in a streaming chunk.
48
+ * Matches OpenAI ChatCompletionChunk.Choice structure.
49
+ */
50
+ const ChoiceSchema = z.object({
51
+ delta: DeltaSchema,
52
+ index: z.number(),
53
+ finish_reason: z.enum([
54
+ "stop",
55
+ "length",
56
+ "tool_calls",
57
+ "content_filter",
58
+ "function_call"
59
+ ]).nullable(),
60
+ logprobs: z.unknown().nullish()
61
+ });
62
+ /**
63
+ * Schema for streaming chunk event.
64
+ * Based on OpenAI ChatCompletionChunk structure.
65
+ * @see https://platform.openai.com/docs/api-reference/chat/streaming
66
+ */
67
+ const ChunkEventSchema = z.object({
68
+ eventType: z.literal("chunk"),
69
+ /** Unique identifier for the chat completion */
70
+ id: z.string(),
71
+ /** Array of choices (typically one for streaming) */
72
+ choices: z.array(ChoiceSchema),
73
+ /** Unix timestamp when the chunk was created */
74
+ created: z.number(),
75
+ /** Model that generated the chunk */
76
+ model: z.string(),
77
+ /** Object type identifier */
78
+ object: z.literal("chat.completion.chunk"),
79
+ /** Service tier used for processing */
80
+ service_tier: z.enum([
81
+ "auto",
82
+ "default",
83
+ "flex",
84
+ "scale",
85
+ "priority"
86
+ ]).nullish(),
87
+ /** System fingerprint for determinism tracking */
88
+ system_fingerprint: z.string().optional()
89
+ });
90
+
91
+ //#endregion
92
+ //#region src/namespaces/schemas/usage.ts
93
+ /**
94
+ * Schema for completion tokens breakdown.
95
+ * Matches OpenAI CompletionUsage.CompletionTokensDetails structure.
96
+ */
97
+ const CompletionTokensDetailsSchema = z.object({
98
+ /** Tokens from predicted outputs that appeared in completion */
99
+ accepted_prediction_tokens: z.number().optional(),
100
+ /** Audio tokens generated by the model */
101
+ audio_tokens: z.number().optional(),
102
+ /** Tokens generated for reasoning */
103
+ reasoning_tokens: z.number().optional(),
104
+ /** Tokens from predicted outputs that did not appear */
105
+ rejected_prediction_tokens: z.number().optional()
106
+ });
107
+ /**
108
+ * Schema for prompt tokens breakdown.
109
+ * Matches OpenAI CompletionUsage.PromptTokensDetails structure.
110
+ */
111
+ const PromptTokensDetailsSchema = z.object({
112
+ /** Audio tokens in the prompt */
113
+ audio_tokens: z.number().optional(),
114
+ /** Tokens read from cache */
115
+ cached_tokens: z.number().optional()
116
+ });
117
+ /**
118
+ * Schema for token usage event.
119
+ * Based on OpenAI CompletionUsage structure with additional cache fields.
120
+ * @see https://platform.openai.com/docs/api-reference/chat/object#chat/object-usage
121
+ */
122
+ const UsageEventSchema = z.object({
123
+ eventType: z.literal("usage"),
124
+ /** Number of tokens in the prompt */
125
+ prompt_tokens: z.number(),
126
+ /** Number of tokens in the generated completion */
127
+ completion_tokens: z.number(),
128
+ /** Total tokens used (prompt + completion) */
129
+ total_tokens: z.number(),
130
+ /** Breakdown of prompt tokens */
131
+ prompt_tokens_details: PromptTokensDetailsSchema.optional(),
132
+ /** Breakdown of completion tokens */
133
+ completion_tokens_details: CompletionTokensDetailsSchema.optional()
134
+ });
135
+
136
+ //#endregion
137
+ //#region src/namespaces/schemas/message.ts
138
+ /**
139
+ * Schema for message complete event.
140
+ * Emitted when a full assistant message has been assembled from streaming chunks.
141
+ * Extends the base schema with OpenAI-specific `finish_reason` values.
142
+ */
143
+ const MessageCompleteEventSchema$1 = MessageCompleteEventSchema.extend({
144
+ /** Why generation stopped (OpenAI-specific values). */
145
+ finish_reason: z.enum([
146
+ "stop",
147
+ "length",
148
+ "tool_calls",
149
+ "content_filter",
150
+ "function_call"
151
+ ]).nullable() });
152
+
153
+ //#endregion
154
+ //#region src/namespaces/index.ts
155
+ /**
156
+ * Discriminated union of all SDK event types.
157
+ * Uses discriminatedUnion for type-safe event routing based on eventType.
158
+ */
159
+ const SdkEventMessageSchema = z.discriminatedUnion("eventType", [
160
+ ChunkEventSchema,
161
+ UsageEventSchema,
162
+ ToolCallsEventSchema,
163
+ MessageCompleteEventSchema$1,
164
+ ReasoningDeltaEventSchema,
165
+ ReasoningCompleteEventSchema,
166
+ AgentStartedEventSchema,
167
+ AgentCompleteEventSchema,
168
+ ErrorEventSchema,
169
+ ToolStartedEventSchema,
170
+ ToolCompletedEventSchema
171
+ ]);
172
+ /**
173
+ * Envelope schema for sdk.event catch-all subject.
174
+ * Wraps the discriminated union in an envelope for proper TypeScript narrowing at emit time.
175
+ * Pattern matches codex-mcp's \{ id, msg \} envelope.
176
+ */
177
+ const SdkEventSchema = z.object({
178
+ event: SdkEventMessageSchema,
179
+ adapterName: z.string().optional(),
180
+ agentId: z.string().optional(),
181
+ adapterId: z.string().optional(),
182
+ adapterSessionId: z.string().optional(),
183
+ sessionId: z.string().optional()
184
+ });
185
+ /**
186
+ * Schema for raw OpenAI streaming chunks.
187
+ * Passthrough schema for observability - captures raw API response.
188
+ * Used by stream-bridge to emit unprocessed chunks before normalization.
189
+ */
190
+ const RawChunkSchema = z.custom(() => true);
191
+ /**
192
+ * OpenAI Node Adapter Namespace with semantic subjects.
193
+ *
194
+ * Provides typed subjects for:
195
+ * - sdk.event: Catch-all for raw SDK events (for observability/debugging)
196
+ * - tool_approval: RPC for tool execution approval
197
+ * - Semantic subjects: Individual typed events for agent layer processing
198
+ */
199
+ const OpenAINodeConnectorNamespace = createAdapterNamespace(OPENAI_NODE_NAMESPACE, {
200
+ "sdk.raw": RawChunkSchema,
201
+ "sdk.event": SdkEventSchema,
202
+ tool_approval: ScopedToolApprovalSchema,
203
+ chunk: ChunkEventSchema,
204
+ usage: UsageEventSchema,
205
+ message_complete: MessageCompleteEventSchema$1,
206
+ reasoning_delta: ReasoningDeltaEventSchema,
207
+ reasoning_complete: ReasoningCompleteEventSchema,
208
+ tool_calls: ToolCallsEventSchema,
209
+ tool_started: ToolStartedEventSchema,
210
+ tool_completed: ToolCompletedEventSchema,
211
+ agent_started: AgentStartedEventSchema,
212
+ agent_complete: AgentCompleteEventSchema,
213
+ error: ErrorEventSchema,
214
+ "turn.state_changed": TurnStateChangedSchema,
215
+ "turn.turn_started": TurnStateChangedSchema,
216
+ "turn.step_started": TurnStateChangedSchema,
217
+ "turn.step_finished": TurnStateChangedSchema,
218
+ "turn.turn_finished": TurnStateChangedSchema
219
+ });
220
+ /**
221
+ * Typed subject literals for OpenAI Node adapter.
222
+ * Use these constants to subscribe to specific message types with full type safety.
223
+ * @example
224
+ * ```typescript
225
+ * connector.on(OpenAINodeSubjects.chunk, (ctx) => {
226
+ * // ctx.payload is typed as ChunkEvent
227
+ * console.debug(ctx.payload.choices[0].delta.content);
228
+ * });
229
+ * ```
230
+ */
231
+ const OpenAINodeConnectorSubjects = OpenAINodeConnectorNamespace.subjects;
232
+
233
+ //#endregion
234
+ //#region src/tool-handling.ts
235
+ const ADAPTER_LABEL = "OpenAINodeAgent";
236
+ /**
237
+ * Register tool approval handler on scoped connector.
238
+ *
239
+ * Wires OpenAINodeConnectorSubjects.tool_approval → AgentSubjects.toolApprove.
240
+ * Used by agent.ts wireToolApprovalRpc() for consistent approval flow.
241
+ *
242
+ * The handler receives `AgentToolApproveRequest` from the connector (already
243
+ * transformed via toGlobalToolApproval), enriches with context if needed,
244
+ * and forwards to global MakaioBus.
245
+ * @param connector - OpenAI Node connector (needs only `on` method)
246
+ * @param context - Adapter context for request enrichment (static or lazy callback)
247
+ * @returns Unsubscribe function
248
+ */
249
+ const registerToolApprovalHandler = createToolApprovalHandler(OpenAINodeConnectorSubjects.tool_approval, (payload, context) => mergeScopedToolApproval(payload, context, "openai-node"), (response) => response);
250
+ /**
251
+ * Convert ToolListItem[] to OpenAI ChatCompletionTool[] format.
252
+ *
253
+ * Filters to only tools with inputSchema (required for OpenAI function calling).
254
+ * Maps name, description, and parameters to OpenAI's expected structure.
255
+ * @param tools - Tools from ToolRegistry
256
+ * @returns OpenAI-formatted tools
257
+ */
258
+ function toOpenAIToolFormat(tools) {
259
+ return filterToolsWithSchema(tools).map((tool) => ({
260
+ type: "function",
261
+ function: {
262
+ name: tool.name,
263
+ description: tool.description,
264
+ parameters: tool.inputSchema
265
+ }
266
+ }));
267
+ }
268
+ /**
269
+ * Convenience: Load tools and convert to OpenAI format in one call.
270
+ * @param adapterId - Adapter instance ID
271
+ * @param adapterName - Adapter type name
272
+ * @returns OpenAI-formatted tools ready for chat completions
273
+ */
274
+ async function fetchToolsForOpenAI(adapterId, adapterName) {
275
+ return toOpenAIToolFormat(await loadToolsFromRegistry(adapterId, adapterName));
276
+ }
277
+ /**
278
+ * Process tool calls: request approval, execute, and add results to messages.
279
+ *
280
+ * Tool call arguments are already normalized by stream-bridge
281
+ * (GLM \{\} fix, DeepSeek XML extraction).
282
+ * @param toolCalls - Array of tool calls from the model response (normalized)
283
+ * @param callbacks - Injected callbacks for event emission, approval requests, and optional ledger recording
284
+ * @param contextOverrides - Execution context (cwd, env, sessionId, agentId, turnId)
285
+ * @returns Tool result messages to append to conversation history
286
+ */
287
+ async function handleToolCalls$1(toolCalls, callbacks, contextOverrides) {
288
+ return handleToolCalls(toolCalls, callbacks, contextOverrides, (toolCallId, content, _isError) => ({
289
+ role: "tool",
290
+ tool_call_id: toolCallId,
291
+ content
292
+ }), ADAPTER_LABEL);
293
+ }
294
+
295
+ //#endregion
296
+ //#region src/utils/blockToContentPart.ts
297
+ /** Minimal extension→MIME map used when attachments omit explicit MIME type metadata. */
298
+ const ATTACHMENT_EXTENSION_MIME = {
299
+ ".png": "image/png",
300
+ ".jpg": "image/jpeg",
301
+ ".jpeg": "image/jpeg",
302
+ ".gif": "image/gif",
303
+ ".webp": "image/webp",
304
+ ".pdf": "application/pdf",
305
+ ".txt": "text/plain",
306
+ ".md": "text/markdown",
307
+ ".json": "application/json",
308
+ ".xml": "application/xml",
309
+ ".yaml": "application/x-yaml",
310
+ ".yml": "application/x-yaml",
311
+ ".csv": "text/csv",
312
+ ".ts": "application/typescript",
313
+ ".tsx": "application/typescript",
314
+ ".js": "application/javascript",
315
+ ".jsx": "application/javascript",
316
+ ".mjs": "application/javascript",
317
+ ".cjs": "application/javascript"
318
+ };
319
+ /**
320
+ * Build a data URI string for inline base64 content.
321
+ * @param mimeType - The MIME type of the content.
322
+ * @param data - The base64-encoded data string.
323
+ * @returns A `data:{mimeType};base64,{data}` URI string.
324
+ */
325
+ function buildDataUri(mimeType, data) {
326
+ return `data:${mimeType};base64,${data}`;
327
+ }
328
+ /**
329
+ * Infer MIME type from attachment filename when source metadata is absent.
330
+ * @param fileName - Attachment file name.
331
+ * @returns Inferred MIME type, or undefined when extension is unknown.
332
+ */
333
+ function inferMimeTypeFromFileName(fileName) {
334
+ if (!fileName) return void 0;
335
+ const ext = path.extname(fileName).toLowerCase();
336
+ if (!ext) return void 0;
337
+ return ATTACHMENT_EXTENSION_MIME[ext];
338
+ }
339
+ /**
340
+ * Convert a {@link MessageBlock} to an OpenAI {@link ChatCompletionContentPart}.
341
+ *
342
+ * Mapping rules:
343
+ * - `text` → `{ type: 'text', text: block.content }`
344
+ * - `image` (base64) → `{ type: 'image_url', image_url: { url: 'data:{mimeType};base64,{data}' } }`
345
+ * - `image` (url) → `{ type: 'image_url', image_url: { url: block.source.url } }`
346
+ * - `document` (base64) → `{ type: 'file', file: { file_data: 'data:{mimeType};base64,{data}', filename: 'document' } }`
347
+ * - `attachment` with image MIME → `{ type: 'image_url', ... }`
348
+ * - `attachment` with PDF MIME → `{ type: 'file', file: { file_data: ..., filename } }`
349
+ * - `attachment` with text-like MIME (base64) → decoded UTF-8 as `{ type: 'text', text }`
350
+ * - All other blocks → `{ type: 'text', text: serializeBlockToText(block) }`
351
+ * @param block - The message block to convert.
352
+ * @returns A `ChatCompletionContentPart` suitable for the OpenAI messages API.
353
+ */
354
+ function blockToContentPart(block) {
355
+ switch (block.type) {
356
+ case "text": return {
357
+ type: "text",
358
+ text: block.content
359
+ };
360
+ case "image":
361
+ if (block.source.type === "base64") return {
362
+ type: "image_url",
363
+ image_url: { url: buildDataUri(block.source.mimeType ?? "image/png", block.source.data) }
364
+ };
365
+ return {
366
+ type: "image_url",
367
+ image_url: { url: block.source.url }
368
+ };
369
+ case "document":
370
+ if (block.source.type === "base64") return {
371
+ type: "file",
372
+ file: {
373
+ file_data: buildDataUri(block.source.mimeType ?? "application/pdf", block.source.data),
374
+ filename: "document"
375
+ }
376
+ };
377
+ return {
378
+ type: "text",
379
+ text: serializeBlockToText(block)
380
+ };
381
+ case "attachment": {
382
+ const { source, fileName } = block;
383
+ if (source.type === "base64") {
384
+ const normalizedMimeType = normalizeMimeType(source.mimeType ?? inferMimeTypeFromFileName(fileName));
385
+ if (normalizedMimeType.startsWith("image/")) return {
386
+ type: "image_url",
387
+ image_url: { url: buildDataUri(normalizedMimeType, source.data) }
388
+ };
389
+ if (normalizedMimeType === "application/pdf") return {
390
+ type: "file",
391
+ file: {
392
+ file_data: buildDataUri(normalizedMimeType, source.data),
393
+ filename: fileName
394
+ }
395
+ };
396
+ if (isTextLikeMimeType(normalizedMimeType)) {
397
+ let decodedText;
398
+ try {
399
+ decodedText = Buffer.from(source.data, "base64").toString("utf-8");
400
+ } catch {
401
+ decodedText = "";
402
+ }
403
+ return {
404
+ type: "text",
405
+ text: decodedText
406
+ };
407
+ }
408
+ return {
409
+ type: "text",
410
+ text: serializeBlockToText(block)
411
+ };
412
+ }
413
+ return {
414
+ type: "text",
415
+ text: serializeBlockToText(block)
416
+ };
417
+ }
418
+ default: return {
419
+ type: "text",
420
+ text: serializeBlockToText(block)
421
+ };
422
+ }
423
+ }
424
+ /**
425
+ * Convert an array of {@link MessageBlock}s to `ChatCompletionContentPart[]`,
426
+ * filtering out parts that produce empty text.
427
+ * @param blocks - The blocks to convert.
428
+ * @returns A non-empty array of content parts, or an empty array if all parts are empty.
429
+ */
430
+ function blocksToContentParts(blocks) {
431
+ return blocks.map(blockToContentPart).filter((part) => {
432
+ if (part.type === "text") return part.text.length > 0;
433
+ return true;
434
+ });
435
+ }
436
+
437
+ //#endregion
438
+ //#region src/turn.ts
439
+ /**
440
+ * Turn state machine for OpenAI SDK.
441
+ *
442
+ * Tracks state transitions for a single user message.
443
+ * Unlike Claude, OpenAI has no true pause/resume - we abort and restart.
444
+ *
445
+ * State flow:
446
+ * idle to turn_started to step_started to step_finished to turn_finished
447
+ *
448
+ * On immediate: abort current turn, merge content, start new turn.
449
+ */
450
+ var OpenAIConnectorTurn = class extends ProceduralConnectorTurn {
451
+ abortController;
452
+ constructor(bus, adapterId, adapterName, agentId, messageHandle) {
453
+ super({
454
+ bus,
455
+ adapterId,
456
+ adapterName,
457
+ agentId,
458
+ messageHandle,
459
+ turnSubjects: OpenAINodeConnectorSubjects.turn
460
+ }, "idle");
461
+ this.abortController = new AbortController();
462
+ }
463
+ /**
464
+ * Get the AbortSignal for this turn.
465
+ * Pass to OpenAI client for cancellation.
466
+ * @returns The abort signal for this turn
467
+ */
468
+ getAbortSignal() {
469
+ return this.abortController.signal;
470
+ }
471
+ /**
472
+ * Pause (abort) at next opportunity.
473
+ * OpenAI doesn't support true pause - we abort and caller restarts with
474
+ * merged content.
475
+ * @returns Pause result indicating turn state
476
+ */
477
+ async pause() {
478
+ const result = await super.pause();
479
+ if (!result.turnEnded) this.abortController.abort();
480
+ return result;
481
+ }
482
+ };
483
+
484
+ //#endregion
485
+ //#region src/stream-bridge.ts
486
+ /**
487
+ * Convert accumulators to complete tool calls.
488
+ * @param accumulators - Tool call accumulators to convert
489
+ * @returns Complete tool call objects
490
+ */
491
+ function buildToolCalls(accumulators) {
492
+ return accumulators.map((acc) => ({
493
+ id: acc.id,
494
+ type: "function",
495
+ function: {
496
+ name: acc.name,
497
+ arguments: acc.arguments
498
+ }
499
+ }));
500
+ }
501
+ /**
502
+ * Update tool call accumulator with delta data.
503
+ * @param accumulators - Array of tool call accumulators
504
+ * @param delta - Delta data from streaming chunk
505
+ */
506
+ function updateToolCallAccumulator(accumulators, delta) {
507
+ const index = delta.index;
508
+ if (!accumulators[index]) accumulators[index] = {
509
+ id: delta.id ?? "",
510
+ name: delta.function?.name ?? "",
511
+ arguments: ""
512
+ };
513
+ const accumulator = accumulators[index];
514
+ if (delta.id) accumulator.id = delta.id;
515
+ if (delta.function?.name) accumulator.name = delta.function.name;
516
+ if (delta.function?.arguments) {
517
+ const current = accumulator.arguments;
518
+ const incoming = delta.function.arguments;
519
+ if (current.endsWith("}") && incoming.startsWith("{") && incoming !== "{}") accumulator.arguments = incoming;
520
+ else accumulator.arguments += incoming;
521
+ }
522
+ }
523
+ /**
524
+ * Parse DeepSeek-style XML tool calls from content.
525
+ * Format: `<function_calls><invoke name="..."><parameter name="..." string="true">value</parameter></invoke></function_calls>`
526
+ * @param content - Raw content potentially containing XML tool calls
527
+ * @returns Parsed tool calls or empty array if parsing fails
528
+ */
529
+ function parseXmlToolCalls(content) {
530
+ const toolCalls = [];
531
+ const functionCallsMatch = content.match(/<function_calls>([\s\S]*?)<\/function_calls>/);
532
+ if (!functionCallsMatch) return [];
533
+ const functionCallsContent = functionCallsMatch[1];
534
+ const invokeRegex = /<invoke\s+name="([^"]+)">([\s\S]*?)<\/invoke>/g;
535
+ let invokeMatch;
536
+ let callIndex = 0;
537
+ while ((invokeMatch = invokeRegex.exec(functionCallsContent)) !== null) {
538
+ const functionName = invokeMatch[1];
539
+ const invokeContent = invokeMatch[2];
540
+ const params = {};
541
+ const paramRegex = /<parameter\s+name="([^"]+)"(?:\s+string="true")?>([^<]*)<\/parameter>/g;
542
+ let paramMatch;
543
+ while ((paramMatch = paramRegex.exec(invokeContent)) !== null) {
544
+ const paramName = paramMatch[1];
545
+ params[paramName] = paramMatch[2];
546
+ }
547
+ toolCalls.push({
548
+ id: `xml_call_${callIndex++}`,
549
+ type: "function",
550
+ function: {
551
+ name: functionName,
552
+ arguments: JSON.stringify(params)
553
+ }
554
+ });
555
+ }
556
+ return toolCalls;
557
+ }
558
+ /**
559
+ * Parse GLM-4.5-Air-style XML tool calls from content.
560
+ * Format: `<tool_call>function_name\n<arg_key>key</arg_key>\n<arg_value>value</arg_value>\n</tool_call>`
561
+ * @param content - Raw content potentially containing XML tool calls
562
+ * @returns Parsed tool calls or empty array if parsing fails
563
+ */
564
+ function parseGlmAirXml(content) {
565
+ const toolCalls = [];
566
+ const toolCallRegex = /<tool_call>([\s\S]*?)<\/tool_call>/g;
567
+ let toolCallMatch;
568
+ let callIndex = 0;
569
+ while ((toolCallMatch = toolCallRegex.exec(content)) !== null) {
570
+ const toolCallContent = toolCallMatch[1];
571
+ const functionName = toolCallContent.trim().split("\n")[0]?.trim();
572
+ if (!functionName) continue;
573
+ const params = {};
574
+ const keys = Array.from(toolCallContent.matchAll(/<arg_key>([^<]*)<\/arg_key>/g), (m) => m[1]);
575
+ const values = Array.from(toolCallContent.matchAll(/<arg_value>([^<]*)<\/arg_value>/g), (m) => m[1]);
576
+ for (let i = 0; i < Math.min(keys.length, values.length); i++) params[keys[i]] = values[i];
577
+ toolCalls.push({
578
+ id: `glm_air_call_${callIndex++}`,
579
+ type: "function",
580
+ function: {
581
+ name: functionName,
582
+ arguments: JSON.stringify(params)
583
+ }
584
+ });
585
+ }
586
+ return toolCalls;
587
+ }
588
+ /**
589
+ * Create initial stream processing state.
590
+ * @returns Fresh state object
591
+ */
592
+ function createStreamState() {
593
+ return {
594
+ content: "",
595
+ reasoning: "",
596
+ finishReason: null,
597
+ usage: null,
598
+ toolCallAccumulators: []
599
+ };
600
+ }
601
+ /**
602
+ * Extract usage from chunk if present.
603
+ * @param chunk - Chunk to extract usage from
604
+ * @returns Usage object or null
605
+ */
606
+ function extractUsage(chunk) {
607
+ if (!chunk.usage) return null;
608
+ return {
609
+ promptTokens: chunk.usage.prompt_tokens,
610
+ completionTokens: chunk.usage.completion_tokens
611
+ };
612
+ }
613
+ /**
614
+ * Apply model-agnostic normalizations to tool calls and content.
615
+ * Detects quirks by content patterns, not model names.
616
+ * @param content - Raw content from stream
617
+ * @param reasoning - Reasoning content from stream (e.g., from reasoning_delta events)
618
+ * @param toolCalls - Aggregated tool calls from stream
619
+ * @param finishReason - Finish reason from stream
620
+ * @returns Normalized result with cleaned content, reasoning, and tool calls
621
+ */
622
+ function normalizeResult(content, reasoning, toolCalls, finishReason) {
623
+ let normalizedContent = content;
624
+ let normalizedReasoning = reasoning;
625
+ let normalizedToolCalls = toolCalls;
626
+ let normalizedFinishReason = finishReason;
627
+ const thinkMatch = normalizedContent.match(/^<think(?:ing)?>([\s\S]*?)<\/think(?:ing)?>\s*/);
628
+ if (thinkMatch) {
629
+ const extractedReasoning = thinkMatch[1].trim();
630
+ normalizedContent = normalizedContent.slice(thinkMatch[0].length).trim();
631
+ normalizedReasoning = normalizedReasoning ? `${extractedReasoning}\n\n${normalizedReasoning}` : extractedReasoning;
632
+ }
633
+ normalizedToolCalls = normalizedToolCalls.map((tc) => {
634
+ if (/\}\s*\{\s*\}$/.test(tc.function.arguments)) return {
635
+ ...tc,
636
+ function: {
637
+ ...tc.function,
638
+ arguments: tc.function.arguments.replace(/\}\s*\{\s*\}$/, "}")
639
+ }
640
+ };
641
+ return tc;
642
+ });
643
+ if (normalizedContent.includes("<function_calls>") && normalizedFinishReason === "stop" && normalizedToolCalls.length === 0) {
644
+ const parsed = parseXmlToolCalls(normalizedContent);
645
+ if (parsed.length > 0) {
646
+ normalizedToolCalls = parsed;
647
+ normalizedFinishReason = "tool_calls";
648
+ normalizedContent = normalizedContent.replace(/<function_calls>[\s\S]*<\/function_calls>/, "").trim();
649
+ }
650
+ }
651
+ if (normalizedFinishReason === "tool_calls" && normalizedToolCalls.length > 0 && normalizedToolCalls.every((tc) => tc.function.arguments === "{}") && normalizedContent.includes("<tool_call>")) {
652
+ const parsed = parseGlmAirXml(normalizedContent);
653
+ if (parsed.length > 0) {
654
+ normalizedToolCalls = parsed;
655
+ normalizedContent = normalizedContent.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, "").trim();
656
+ }
657
+ }
658
+ return {
659
+ content: normalizedContent,
660
+ reasoning: normalizedReasoning,
661
+ toolCalls: normalizedToolCalls,
662
+ finishReason: normalizedFinishReason
663
+ };
664
+ }
665
+ /**
666
+ * Process a choice delta and accumulate state.
667
+ * @param choice - Stream choice containing delta
668
+ * @param chunk - Full chunk for usage extraction
669
+ * @param state - Mutable aggregation state
670
+ * @param emitEvent - Event emitter function
671
+ */
672
+ async function processChoiceDelta(choice, chunk, state, emitEvent) {
673
+ const delta = choice.delta;
674
+ if (choice.finish_reason !== null) state.finishReason = choice.finish_reason;
675
+ if (delta.content) state.content += delta.content;
676
+ const reasoningDelta = delta.reasoning || delta.reasoning_content;
677
+ if (reasoningDelta) {
678
+ state.reasoning += reasoningDelta;
679
+ await emitEvent({
680
+ eventType: "reasoning_delta",
681
+ content: reasoningDelta
682
+ });
683
+ }
684
+ if (delta.tool_calls) for (const toolCallDelta of delta.tool_calls) updateToolCallAccumulator(state.toolCallAccumulators, toolCallDelta);
685
+ if (chunk.usage && !state.usage) {
686
+ state.usage = extractUsage(chunk);
687
+ if (state.usage) await emitEvent({
688
+ eventType: "usage",
689
+ prompt_tokens: state.usage.promptTokens,
690
+ completion_tokens: state.usage.completionTokens,
691
+ total_tokens: state.usage.promptTokens + state.usage.completionTokens
692
+ });
693
+ }
694
+ }
695
+ /**
696
+ * Emit final events after stream completes.
697
+ * @param state - Final aggregation state
698
+ * @param emitEvent - Event emitter function
699
+ */
700
+ async function emitFinalEvents(state, emitEvent) {
701
+ const rawToolCalls = buildToolCalls(state.toolCallAccumulators);
702
+ const normalized = normalizeResult(state.content, state.reasoning, rawToolCalls, state.finishReason);
703
+ if (normalized.toolCalls.length > 0) await emitEvent({
704
+ eventType: "tool_calls",
705
+ toolCalls: normalized.toolCalls
706
+ });
707
+ await emitEvent({
708
+ eventType: "message_complete",
709
+ content: normalized.content || null,
710
+ reasoning: normalized.reasoning || void 0,
711
+ tool_calls: normalized.toolCalls.length > 0 ? normalized.toolCalls : void 0,
712
+ finish_reason: normalized.finishReason
713
+ });
714
+ if (normalized.reasoning) await emitEvent({
715
+ eventType: "reasoning_complete",
716
+ content: normalized.reasoning
717
+ });
718
+ }
719
+ /**
720
+ * Process an OpenAI streaming response.
721
+ *
722
+ * Emits events to bus as stream progresses:
723
+ * - sdk.raw: Each raw chunk (for observability)
724
+ * - sdk.event: Semantic events (chunk, reasoning_delta, usage, tool_calls, message_complete)
725
+ *
726
+ * Applies normalizations before emitting final events:
727
+ * - GLM: Strips trailing `\{\}` from tool call arguments
728
+ * - DeepSeek: Extracts XML tool calls from content
729
+ * @param stream - OpenAI streaming response
730
+ * @param config - Bus and metadata configuration
731
+ */
732
+ async function processStream(stream, config) {
733
+ const state = createStreamState();
734
+ const metadata = {
735
+ adapterName: config.adapterName,
736
+ agentId: config.agentId,
737
+ adapterId: config.adapterId,
738
+ ...config.adapterSessionId ? { adapterSessionId: config.adapterSessionId } : {}
739
+ };
740
+ const emitRaw = async (chunk) => {
741
+ await config.bus.emit(OpenAINodeConnectorSubjects.sdk.raw, {
742
+ ...chunk,
743
+ ...metadata
744
+ });
745
+ };
746
+ const emitEvent = async (event) => {
747
+ await config.bus.emit(OpenAINodeConnectorSubjects.sdk.event, {
748
+ event,
749
+ ...metadata
750
+ });
751
+ };
752
+ for await (const chunk of stream) {
753
+ config.logLowLevelEvent?.(chunk);
754
+ await emitRaw(chunk);
755
+ const choice = chunk.choices[0];
756
+ if (!choice) {
757
+ const usage = extractUsage(chunk);
758
+ if (usage && !state.usage) {
759
+ state.usage = usage;
760
+ await emitEvent({
761
+ eventType: "usage",
762
+ prompt_tokens: usage.promptTokens,
763
+ completion_tokens: usage.completionTokens,
764
+ total_tokens: usage.promptTokens + usage.completionTokens
765
+ });
766
+ }
767
+ continue;
768
+ }
769
+ await processChoiceDelta(choice, chunk, state, emitEvent);
770
+ }
771
+ await emitFinalEvents(state, emitEvent);
772
+ }
773
+
774
+ //#endregion
775
+ //#region src/utils/convertMessageHistory.ts
776
+ /**
777
+ * Convert curated message history to OpenAI ChatCompletionMessageParam[].
778
+ *
779
+ * Transforms the shared Message format (with blocks) into OpenAI's expected format:
780
+ * - Text/reasoning blocks become assistant message content
781
+ * - tool_call blocks become `tool_calls` on assistant messages
782
+ * - tool_output blocks become separate `role: 'tool'` messages
783
+ * - Media blocks (image, document, attachment) are skipped for assistant messages
784
+ * - User message blocks are converted to native content parts (image_url, file, text)
785
+ * - System message blocks are serialized to text (OpenAI system messages only accept strings)
786
+ * @param history - Curated messages from sessionContext.messageHistory
787
+ * @returns Array of OpenAI ChatCompletionMessageParam
788
+ */
789
+ function convertMessageHistory(history) {
790
+ const result = [];
791
+ for (const msg of history) {
792
+ const blocks = Array.isArray(msg.blocks) ? msg.blocks : [msg.blocks];
793
+ if (msg.role === "assistant") {
794
+ const textParts = [];
795
+ const toolCalls = [];
796
+ const toolOutputs = [];
797
+ for (const block of blocks) switch (block.type) {
798
+ case "text":
799
+ textParts.push(serializeBlockToText(block));
800
+ break;
801
+ case "reasoning":
802
+ textParts.push(serializeBlockToText(block));
803
+ break;
804
+ case "tool_call":
805
+ toolCalls.push({
806
+ id: block.toolCallId,
807
+ type: "function",
808
+ function: {
809
+ name: block.name,
810
+ arguments: JSON.stringify(block.args)
811
+ }
812
+ });
813
+ break;
814
+ case "tool_output":
815
+ toolOutputs.push({
816
+ toolCallId: block.toolCallId,
817
+ content: block.output,
818
+ isError: block.isError
819
+ });
820
+ break;
821
+ }
822
+ const validToolCallIds = new Set(toolCalls.map((call) => call.id));
823
+ const matchedToolOutputs = toolOutputs.filter((output) => validToolCallIds.has(output.toolCallId));
824
+ const orphanToolOutputs = toolOutputs.filter((output) => !validToolCallIds.has(output.toolCallId));
825
+ for (const orphanOutput of orphanToolOutputs) textParts.push(serializeBlockToText({
826
+ type: "tool_output",
827
+ toolCallId: orphanOutput.toolCallId,
828
+ output: orphanOutput.content,
829
+ isError: orphanOutput.isError
830
+ }));
831
+ const assistantContent = textParts.join("\n");
832
+ if (assistantContent.length > 0 || toolCalls.length > 0) {
833
+ const assistantMsg = {
834
+ role: "assistant",
835
+ content: assistantContent.length > 0 ? assistantContent : null
836
+ };
837
+ if (toolCalls.length > 0) assistantMsg.tool_calls = toolCalls;
838
+ result.push(assistantMsg);
839
+ }
840
+ for (const output of matchedToolOutputs) result.push({
841
+ role: "tool",
842
+ tool_call_id: output.toolCallId,
843
+ content: output.isError ? `[Tool Error]\n${output.content}` : output.content
844
+ });
845
+ } else if (msg.role === "system") {
846
+ const content = blocks.map(serializeBlockToText).join("\n");
847
+ if (content.length > 0) result.push({
848
+ role: "system",
849
+ content
850
+ });
851
+ } else {
852
+ const parts = blocksToContentParts(blocks);
853
+ if (parts.length > 0) {
854
+ const content = parts.every((p) => p.type === "text") ? parts.map((p) => p.text).join("\n") : parts;
855
+ result.push({
856
+ role: "user",
857
+ content
858
+ });
859
+ }
860
+ }
861
+ }
862
+ return result;
863
+ }
864
+
865
+ //#endregion
866
+ //#region src/utils/classifyOpenAIError.ts
867
+ /**
868
+ * Classify error from OpenAI SDK into appropriate MakaioError subclass.
869
+ * OpenAI SDK throws typed APIError with status codes and error types.
870
+ * @param error - Error from OpenAI SDK
871
+ * @returns MakaioError subclass instance or the original error
872
+ */
873
+ function classifyOpenAIError(error) {
874
+ if (!(error instanceof Error)) return new Error(String(error));
875
+ const errorWithProps = error;
876
+ const status = errorWithProps.status;
877
+ const message = error.message;
878
+ const messageLower = message.toLowerCase();
879
+ if (status === 401 || status === 403) return new AuthenticationError(message);
880
+ if (status === 404 && (messageLower.includes("model") || errorWithProps.code === "model_not_found")) return new ModelUnavailableError(message);
881
+ if (status === 429) {
882
+ if (messageLower.includes("quota") || messageLower.includes("insufficient_quota") || errorWithProps.code === "insufficient_quota") return new QuotaExceededError(message);
883
+ return new RateLimitError(message);
884
+ }
885
+ if (errorWithProps.type === "insufficient_quota" || messageLower.includes("quota exceeded")) return new QuotaExceededError(message);
886
+ if (messageLower.includes("rate limit") || errorWithProps.code === "rate_limit_exceeded") return new RateLimitError(message);
887
+ if (messageLower.includes("authentication") || messageLower.includes("unauthorized") || errorWithProps.code === "invalid_api_key") return new AuthenticationError(message);
888
+ return error;
889
+ }
890
+
891
+ //#endregion
892
+ //#region src/utils/buildChatCompletionRequest.ts
893
+ /**
894
+ * Map normalized reasoning level to OpenAI `reasoning_effort`.
895
+ * @param reasoningEffort - Makaio reasoning effort
896
+ * @returns OpenAI-compatible reasoning effort
897
+ */
898
+ function toOpenAIReasoningEffort(reasoningEffort) {
899
+ if (reasoningEffort === "extra-high") return "xhigh";
900
+ return reasoningEffort;
901
+ }
902
+ /**
903
+ * Build chat.completions payload for OpenAI-compatible providers.
904
+ * @param input - Payload inputs and reasoning capability metadata
905
+ * @returns Request object for `chat.completions.create`
906
+ */
907
+ function buildChatCompletionRequest(input) {
908
+ const reasoningEffort = input.supportsReasoningEffort && input.reasoningEffort && input.reasoningEffort !== "none" ? toOpenAIReasoningEffort(input.reasoningEffort) : void 0;
909
+ return {
910
+ model: input.model,
911
+ messages: input.messages,
912
+ tools: input.tools.length > 0 ? input.tools : void 0,
913
+ stream: true,
914
+ ...reasoningEffort ? { reasoning_effort: reasoningEffort } : {},
915
+ stream_options: { include_usage: true }
916
+ };
917
+ }
918
+
919
+ //#endregion
920
+ //#region src/session.ts
921
+ /**
922
+ * Session for OpenAI SDK lifecycle management.
923
+ *
924
+ * Manages OpenAI client calls across multiple user messages:
925
+ * - Creates Turn instances for each user message
926
+ * - Handles immediate mode via abort+restart (no true pause)
927
+ * - Processes message queue with merge support
928
+ *
929
+ * Key difference from Claude: OpenAI has no server-side session.
930
+ * Each turn rebuilds the messages[] array with full history.
931
+ */
932
+ var OpenAIConnectorSession = class extends BaseStreamSession {
933
+ messages = [];
934
+ /**
935
+ * Mutable tool list for this session.
936
+ * Rebuilt whenever either `nativeTools` or `mcpTools` changes.
937
+ */
938
+ currentTools;
939
+ /** Latest native registry tools for this live session. */
940
+ nativeTools;
941
+ /**
942
+ * Latest MCP direct-inject tools converted to OpenAI format.
943
+ * Stored separately so that `replaceNativeTools` can rebuild `currentTools`
944
+ * without losing the MCP tool set.
945
+ */
946
+ mcpTools = [];
947
+ /**
948
+ * Runtime reasoning effort level; updated by `updateReasoning()`.
949
+ * Initialized from `config.reasoningEffort` at session construction.
950
+ */
951
+ currentReasoningEffort;
952
+ /**
953
+ * Create an OpenAI connector session.
954
+ * @param config - OpenAI session configuration (bus identity, model/cwd, SDK client, and lifecycle hooks).
955
+ */
956
+ constructor(config) {
957
+ super(config);
958
+ this.nativeTools = config.openAITools;
959
+ this.currentTools = config.openAITools;
960
+ this.currentReasoningEffort = config.reasoningEffort;
961
+ }
962
+ /**
963
+ * Replace the native registry tool set used for subsequent turns.
964
+ *
965
+ * Rebuilds `currentTools` so that MCP tools registered via `updateTools`
966
+ * are preserved alongside the new native tools.
967
+ * @param tools - Fresh OpenAI-formatted native tools
968
+ */
969
+ replaceNativeTools(tools) {
970
+ this.nativeTools = tools;
971
+ this.currentTools = [...this.nativeTools, ...this.mcpTools];
972
+ }
973
+ /**
974
+ * Update the session's tool set for the next API call.
975
+ *
976
+ * Converts generic `ToolListItem[]` (MCP direct-inject tools) to
977
+ * OpenAI `ChatCompletionTool[]` format and merges them with the existing native tools.
978
+ * @param tools - New MCP direct-inject tools in generic format
979
+ */
980
+ updateTools(tools) {
981
+ this.mcpTools = toOpenAIToolFormat(tools);
982
+ this.currentTools = [...this.nativeTools, ...this.mcpTools];
983
+ }
984
+ /**
985
+ * Update the reasoning effort level used for subsequent API calls.
986
+ *
987
+ * Called by the connector's `changeReasoningInPlace()` to sync the session
988
+ * with the connector-level reasoning effort without requiring a full session
989
+ * swap. The updated value is picked up by `executeApiCall` on the next turn.
990
+ * @internal Direct calls bypass capability validation — only the mutation
991
+ * manager should call this (via the connector's `changeReasoningInPlace`).
992
+ * @param level - New reasoning effort level
993
+ */
994
+ updateReasoning(level) {
995
+ this.currentReasoningEffort = level;
996
+ }
997
+ /**
998
+ * Return the names of all tools that would be sent in the next API call.
999
+ *
1000
+ * Reflects the merged set of native and MCP tools after any `replaceNativeTools`
1001
+ * or `updateTools` calls.
1002
+ * @returns Ordered list of tool names matching the next API request
1003
+ */
1004
+ getEffectiveToolNames() {
1005
+ return this.currentTools.map((tool) => tool.type === "function" ? tool.function.name : tool.custom.name);
1006
+ }
1007
+ /**
1008
+ * Create an OpenAI connector turn for the given message handle.
1009
+ * @param handle - The message handle this turn will process
1010
+ * @returns A new `OpenAIConnectorTurn` instance
1011
+ */
1012
+ createTurn(handle) {
1013
+ return new OpenAIConnectorTurn(this.bus, this.config.adapterId, this.config.adapterName, this.config.agentId, handle);
1014
+ }
1015
+ /**
1016
+ * Validate that content is non-empty after trim.
1017
+ * Throws an Error with a descriptive message if content is empty.
1018
+ * @param content - Content string to validate
1019
+ * @param messageId - Message ID for error context
1020
+ * @param context - Additional context for the error message
1021
+ */
1022
+ throwIfEmptyContent(content, messageId, context) {
1023
+ if (content.trim().length === 0) throw new Error(`[${context}] buildMessages produced empty user content (messageId=${messageId})`);
1024
+ }
1025
+ /**
1026
+ * Build the messages array from handle history and optional merged content.
1027
+ *
1028
+ * Ensures the configured system prompt is always at index 0 without
1029
+ * duplicating existing history system entries.
1030
+ * @param handle - The message handle containing history
1031
+ * @param mergedContent - Optional content from superseded/merged messages
1032
+ */
1033
+ buildMessages(handle, mergedContent) {
1034
+ if (handle.messageHistory) this.messages = convertMessageHistory(handle.messageHistory);
1035
+ if (this.config.systemPrompt !== void 0) {
1036
+ const firstSystemIndex = this.messages.findIndex((message) => message.role === "system");
1037
+ if (firstSystemIndex >= 0) {
1038
+ const [systemMessage] = this.messages.splice(firstSystemIndex, 1);
1039
+ this.messages.unshift({
1040
+ ...systemMessage,
1041
+ content: this.config.systemPrompt
1042
+ });
1043
+ } else this.messages.unshift({
1044
+ role: "system",
1045
+ content: this.config.systemPrompt
1046
+ });
1047
+ }
1048
+ if (mergedContent && mergedContent.length > 0) for (const content of mergedContent) {
1049
+ this.messages.push({
1050
+ role: "user",
1051
+ content
1052
+ });
1053
+ this.messages.push({
1054
+ role: "assistant",
1055
+ content: "Acknowledged."
1056
+ });
1057
+ }
1058
+ const contextText = formatContextBlocksAsText(serializeTurnContext(handle.turnContext));
1059
+ if (handle.message.message !== void 0) {
1060
+ const content = contextText.length > 0 ? `${contextText}\n\n${handle.message.message}` : handle.message.message;
1061
+ this.throwIfEmptyContent(content, handle.messageId, "OpenAIConnectorSession");
1062
+ this.messages.push({
1063
+ role: "user",
1064
+ content
1065
+ });
1066
+ } else {
1067
+ const parts = blocksToContentParts(handle.message.blocks);
1068
+ if (contextText.length > 0) parts.unshift({
1069
+ type: "text",
1070
+ text: contextText
1071
+ });
1072
+ const textContent = parts.filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
1073
+ if (!parts.some((part) => part.type !== "text")) this.throwIfEmptyContent(textContent, handle.messageId, "OpenAIConnectorSession");
1074
+ this.messages.push({
1075
+ role: "user",
1076
+ content: parts
1077
+ });
1078
+ }
1079
+ }
1080
+ /**
1081
+ * Execute the OpenAI streaming API call.
1082
+ *
1083
+ * Calls `client.chat.completions.create` with the current messages array
1084
+ * and pipes the resulting stream through the stream-bridge event emitter.
1085
+ * @param turn - Captured turn instance for this run loop
1086
+ * @param abortSignal - Combined abort signal (turn abort + stream timeout)
1087
+ * @param adapterSessionId - Turn-scoped adapter session ID for event correlation
1088
+ */
1089
+ async executeApiCall(turn, abortSignal, adapterSessionId) {
1090
+ const supportsReasoningEffort = this.currentReasoningEffort !== void 0 && this.currentReasoningEffort !== "none";
1091
+ const stream = await this.config.client.chat.completions.create(buildChatCompletionRequest({
1092
+ model: this.currentModel,
1093
+ messages: this.messages,
1094
+ tools: this.currentTools,
1095
+ reasoningEffort: this.currentReasoningEffort,
1096
+ supportsReasoningEffort
1097
+ }), { signal: abortSignal });
1098
+ await turn.markStepStarted();
1099
+ try {
1100
+ await processStream(stream, {
1101
+ bus: this.config.bus,
1102
+ agentId: this.config.agentId,
1103
+ adapterId: this.config.adapterId,
1104
+ adapterName: this.config.adapterName,
1105
+ adapterSessionId,
1106
+ model: this.currentModel,
1107
+ logLowLevelEvent: this.config.logLowLevelEvent
1108
+ });
1109
+ } finally {
1110
+ await turn.markStepFinished();
1111
+ }
1112
+ }
1113
+ /**
1114
+ * Return the OpenAI Node event bus subject for `message_complete` waiting.
1115
+ * @returns The `sdk.event` subject for the OpenAI adapter namespace
1116
+ */
1117
+ getSdkEventSubject() {
1118
+ return OpenAINodeConnectorSubjects.sdk.event;
1119
+ }
1120
+ /**
1121
+ * Apply a `message_complete` event to the OpenAI messages array and recurse
1122
+ * for tool calls.
1123
+ *
1124
+ * Appends the assistant response as a `ChatCompletionMessageParam`. When
1125
+ * `finish_reason` is `'tool_calls'`, executes tools and continues the loop.
1126
+ * @param result - The parsed OpenAI `message_complete` event
1127
+ * @param currentHandle - The active message handle
1128
+ * @param toolCallIteration - Current tool recursion depth
1129
+ * @param turn - Captured turn instance for this run loop
1130
+ */
1131
+ async applyMessageComplete(result, currentHandle, toolCallIteration, turn) {
1132
+ if (this.shouldAbortTurnProcessing(turn, currentHandle)) return;
1133
+ const messageContent = result.content || null;
1134
+ const assistantMessage = {
1135
+ role: "assistant",
1136
+ content: messageContent
1137
+ };
1138
+ if (result.tool_calls && result.tool_calls.length > 0) assistantMessage.tool_calls = result.tool_calls;
1139
+ this.messages.push(assistantMessage);
1140
+ this.lastAssistantMessage = messageContent || "";
1141
+ if (result.finish_reason === "tool_calls" && result.tool_calls && result.tool_calls.length > 0) {
1142
+ if (toolCallIteration >= BaseStreamSession.MAX_TOOL_CALL_ITERATIONS) throw new Error(`Tool call iteration limit exceeded (${BaseStreamSession.MAX_TOOL_CALL_ITERATIONS}). Aborting to prevent context blowup.`);
1143
+ const messagesFromToolCalls = await handleToolCalls$1(result.tool_calls, {
1144
+ emitSdkEvent: this.config.emitSdkEvent,
1145
+ requestToolApproval: this.config.requestToolApproval,
1146
+ recordMcpCall: this.config.recordMcpCall
1147
+ }, {
1148
+ env: this.config.env,
1149
+ cwd: this.currentCwd,
1150
+ sessionId: this.config.sessionId,
1151
+ agentId: this.config.agentId,
1152
+ adapterId: this.config.adapterId,
1153
+ adapterName: this.config.adapterName,
1154
+ turnId: currentHandle.messageId,
1155
+ turnContext: currentHandle.turnContext,
1156
+ reasoning: result.reasoning,
1157
+ constraints: this.buildToolConstraints()
1158
+ });
1159
+ this.messages.push(...messagesFromToolCalls);
1160
+ await this.runTurnIteration(turn, currentHandle, toolCallIteration + 1);
1161
+ }
1162
+ }
1163
+ /**
1164
+ * Classify an OpenAI SDK error into the appropriate Makaio error type.
1165
+ * @param error - The raw error from the OpenAI SDK
1166
+ * @returns A classified `Error` instance
1167
+ */
1168
+ classifyError(error) {
1169
+ return classifyOpenAIError(error);
1170
+ }
1171
+ };
1172
+
1173
+ //#endregion
1174
+ //#region src/constants.ts
1175
+ /** Adapter name constant for consistent identification */
1176
+ const OpenAINodeAdapterName = "openai-node";
1177
+ /** Default model to use if not specified in config */
1178
+ const DEFAULT_MODEL = "deepseek-ai/DeepSeek-V3.1:thinking";
1179
+ /** Default timeout configuration for OpenAI adapter */
1180
+ const DEFAULT_TIMEOUTS = {
1181
+ initialization: 3e4,
1182
+ acknowledgement: 12e4,
1183
+ completion: 12e4,
1184
+ toolApproval: 5e3,
1185
+ eventWait: 3e4
1186
+ };
1187
+ /** Timeout for live OpenAI-compatible model discovery requests. */
1188
+ const MODEL_FETCH_TIMEOUT_MS = 15e3;
1189
+
1190
+ //#endregion
1191
+ //#region src/connector.ts
1192
+ /** Default adapter identifier for standalone OpenAINodeAgent instances (without adapter layer) */
1193
+ const defaultAdapterId = crypto.randomUUID();
1194
+ /** Default scoped bus for OpenAI Node adapter */
1195
+ const defaultBus = await OpenAINodeConnectorNamespace.scopedBus();
1196
+ /**
1197
+ * OpenAI Node Agent - Wraps the OpenAI SDK for agentic chat completions.
1198
+ *
1199
+ * Implements the agentic loop pattern:
1200
+ * 1. Send user message to OpenAI
1201
+ * 2. Process streaming response
1202
+ * 3. If tool_calls present, execute tools via Bus RPC and recurse
1203
+ * 4. When no tool_calls, mark turn complete
1204
+ *
1205
+ * Tools are fetched from ToolRegistry via MakaioBus.request(ToolSubjects.list)
1206
+ * on the first turn. Tool execution routes through MakaioBus.request(ToolSubjects.execute)
1207
+ * to the ToolRegistry, which validates input and executes the tool handler.
1208
+ *
1209
+ * Credential resolution happens in `fetchTools()` (the first async lifecycle hook)
1210
+ * via `resolveConnectorCredentials()`. Plaintext credentials are stored in instance
1211
+ * fields and never leave the connector.
1212
+ */
1213
+ var OpenAINodeConnector = class extends BaseStreamConnector {
1214
+ /** Resolved API key (populated in fetchTools before createSession is called). */
1215
+ resolvedApiKey = "";
1216
+ client;
1217
+ /** OpenAI-format tools for chat completions API (fetched from bus on first turn) */
1218
+ openAITools = [];
1219
+ /**
1220
+ * Emit an SDK event wrapped in envelope for type-safe discriminated union handling.
1221
+ * Pattern matches codex-mcp's \{ id, msg \} envelope.
1222
+ * @param event - The typed SDK event message to emit
1223
+ */
1224
+ async emitSdkEvent(event) {
1225
+ await this.emit(OpenAINodeConnectorSubjects.sdk.event, { event });
1226
+ }
1227
+ constructor(config) {
1228
+ super({
1229
+ ...config,
1230
+ bus: config?.bus ?? defaultBus,
1231
+ adapterId: config?.adapterId ?? defaultAdapterId,
1232
+ adapterName: config?.adapterName ?? "openai-node"
1233
+ });
1234
+ this.model = config?.model ?? "deepseek-ai/DeepSeek-V3.1:thinking";
1235
+ this.initAdapterSessionId();
1236
+ }
1237
+ /**
1238
+ * Resolve credentials and fetch available tools from the bus.
1239
+ *
1240
+ * Resolves `apiKey` from `providerContext.credentialRefs` via the encrypted
1241
+ * credential channel, then initializes the OpenAI SDK client. Called once
1242
+ * by `BaseStreamConnector.initializeSession()` before `createSession()`.
1243
+ *
1244
+ * Also converts fetched tools to OpenAI format and caches them in
1245
+ * `this.openAITools` for use in `createSession`.
1246
+ */
1247
+ async fetchTools() {
1248
+ const credentialRefs = this.config.providerContext?.credentialRefs ?? {};
1249
+ const credentials = await resolveConnectorCredentials(this.config.bus, credentialRefs);
1250
+ this.resolvedApiKey = credentials["apiKey"] ?? "";
1251
+ const baseUrl = this.config.providerConfig?.baseUrl;
1252
+ this.client = new OpenAI({
1253
+ ...this.resolvedApiKey ? { apiKey: this.resolvedApiKey } : {},
1254
+ baseURL: baseUrl
1255
+ });
1256
+ this.openAITools = await fetchToolsForOpenAI(this.adapterId, this.adapterName);
1257
+ }
1258
+ /**
1259
+ * Re-fetch native tools and re-resolve MCP direct-inject tools.
1260
+ *
1261
+ * Called at turn boundary when `hasPendingToolRefresh` is true.
1262
+ * Re-fetches OpenAI-format tools from the registry, re-prepares MCP
1263
+ * direct tools (and arms the ledger injection flag via `super.refreshTools()`),
1264
+ * then notifies the active session so the next API call uses the updated
1265
+ * merged tool list. The ledger `recordInjection` is deferred to
1266
+ * `onTurnStarted` so it uses the canonical turn number.
1267
+ */
1268
+ async refreshTools() {
1269
+ this.openAITools = await fetchToolsForOpenAI(this.adapterId, this.adapterName);
1270
+ await super.refreshTools();
1271
+ const session = this.getSession();
1272
+ session?.replaceNativeTools(this.openAITools);
1273
+ session?.updateTools?.(this.mcpDirectTools);
1274
+ }
1275
+ /**
1276
+ * Construct the OpenAI connector session with all required config.
1277
+ *
1278
+ * Called by `BaseStreamConnector.initializeSession()` after `fetchTools()`
1279
+ * has resolved credentials and initialized `this.client`.
1280
+ * @returns A new `OpenAIConnectorSession` instance
1281
+ */
1282
+ createSession() {
1283
+ if (!this.client) throw new Error("[OpenAINodeConnector] createSession() called before fetchTools() — client not initialized");
1284
+ const systemPrompt = this.resolveSystemPrompt();
1285
+ return new OpenAIConnectorSession({
1286
+ bus: this.config.bus,
1287
+ adapterId: this.config.adapterId ?? "",
1288
+ adapterName: this.config.adapterName ?? "",
1289
+ agentId: this.agentId,
1290
+ sessionId: this.config.sessionId,
1291
+ cwd: this.cwd,
1292
+ model: this.model ?? "",
1293
+ reasoningEffort: this.config.reasoningEffort,
1294
+ streamStartTimeoutMs: this.getTimeoutMs("eventWait"),
1295
+ env: this.config.env ?? {},
1296
+ client: this.client,
1297
+ openAITools: this.openAITools,
1298
+ systemPrompt,
1299
+ allowedDirectories: this.config.allowedDirectories,
1300
+ supportedReasoningLevels: this.config.supportedReasoningLevels,
1301
+ emitSdkEvent: this.emitSdkEvent.bind(this),
1302
+ handleError: this.handleError.bind(this),
1303
+ requestToolApproval: (payload) => this.requestToolApprovalWithHandling(OpenAINodeConnectorSubjects.tool_approval, payload),
1304
+ logLowLevelEvent: this.logLowLevelEvent,
1305
+ onTurnStart: (handle) => {
1306
+ this.pendingMessageHandle = handle;
1307
+ },
1308
+ onTurnComplete: (_handle, result) => {
1309
+ this.lastResult = result;
1310
+ this.pendingMessageHandle = void 0;
1311
+ },
1312
+ recordMcpCall: this.config.toolLedger ? (toolFullName) => {
1313
+ this.config.toolLedger?.recordCall(toolFullName, this.currentTurnNumber);
1314
+ } : void 0
1315
+ });
1316
+ }
1317
+ /**
1318
+ * Get OpenAI namespace turn subjects.
1319
+ * @returns Turn subject definitions
1320
+ */
1321
+ getTurnSubjects() {
1322
+ return OpenAINodeConnectorSubjects.turn;
1323
+ }
1324
+ };
1325
+
1326
+ //#endregion
1327
+ //#region src/agent.ts
1328
+ /**
1329
+ * OpenAI Agent - Middle layer between AIAdapter and OpenAINodeConnector.
1330
+ *
1331
+ * Responsibilities:
1332
+ * 1. Wire connector's scoped bus events to global agent.* subjects
1333
+ * 2. Auto-enrich payloads with AgentContext via emitGlobal()
1334
+ *
1335
+ * Event Flow:
1336
+ * - OpenAINodeConnector emits to semantic subjects (chunk, usage, etc.)
1337
+ * - OpenAIAgent subscribes to semantic subjects and emits to global bus (agent.*)
1338
+ * - Downstream consumers subscribe to normalized agent.* subjects
1339
+ *
1340
+ * Extends `BaseStreamAgent` which provides all shared wiring logic. This class only
1341
+ * implements the adapter-specific abstract hooks.
1342
+ */
1343
+ var OpenAIAgent = class extends BaseStreamAgent {
1344
+ /**
1345
+ * Tier 1: Route sdk.event catch-all to typed semantic subjects.
1346
+ *
1347
+ * Uses `createConnectorEventMapping` for type-safe discriminated union routing.
1348
+ * Pattern matches codex-mcp: envelope `{ id, event }` with nestedMessageProp 'event'.
1349
+ */
1350
+ wireSdkEvents() {
1351
+ this.createConnectorEventMapping(OpenAINodeConnectorSubjects.sdk.event, "eventType", {
1352
+ chunk: OpenAINodeConnectorSubjects.chunk,
1353
+ usage: OpenAINodeConnectorSubjects.usage,
1354
+ tool_calls: OpenAINodeConnectorSubjects.tool_calls,
1355
+ message_complete: OpenAINodeConnectorSubjects.message_complete,
1356
+ reasoning_delta: OpenAINodeConnectorSubjects.reasoning_delta,
1357
+ reasoning_complete: OpenAINodeConnectorSubjects.reasoning_complete,
1358
+ agent_started: OpenAINodeConnectorSubjects.agent_started,
1359
+ agent_complete: OpenAINodeConnectorSubjects.agent_complete,
1360
+ error: OpenAINodeConnectorSubjects.error,
1361
+ tool_started: OpenAINodeConnectorSubjects.tool_started,
1362
+ tool_completed: OpenAINodeConnectorSubjects.tool_completed
1363
+ }, "event");
1364
+ }
1365
+ /**
1366
+ * Return the OpenAI Node adapter's connector subject spec.
1367
+ *
1368
+ * Maps adapter subject constants to the `StreamAdapterSubjectSpec` interface used
1369
+ * by `BaseStreamAgent` to subscribe to semantic events.
1370
+ * @returns OpenAI-specific subject spec
1371
+ */
1372
+ getConnectorSubjects() {
1373
+ return {
1374
+ chunk: OpenAINodeConnectorSubjects.chunk,
1375
+ usage: OpenAINodeConnectorSubjects.usage,
1376
+ toolCalls: OpenAINodeConnectorSubjects.tool_calls,
1377
+ reasoningComplete: OpenAINodeConnectorSubjects.reasoning_complete,
1378
+ messageComplete: OpenAINodeConnectorSubjects.message_complete,
1379
+ reasoningDelta: OpenAINodeConnectorSubjects.reasoning_delta,
1380
+ toolStarted: OpenAINodeConnectorSubjects.tool_started,
1381
+ toolCompleted: OpenAINodeConnectorSubjects.tool_completed,
1382
+ agentStarted: OpenAINodeConnectorSubjects.agent_started,
1383
+ agentComplete: OpenAINodeConnectorSubjects.agent_complete,
1384
+ error: OpenAINodeConnectorSubjects.error
1385
+ };
1386
+ }
1387
+ /**
1388
+ * Extract plain text from an OpenAI chunk event payload.
1389
+ *
1390
+ * OpenAI chunk events use the ChatCompletionChunk structure where text content
1391
+ * is at `choices[0].delta.content`.
1392
+ * @param payload - OpenAI chunk event payload
1393
+ * @returns The text content, or empty string when no content is present
1394
+ */
1395
+ extractChunkText(payload) {
1396
+ const { choices } = payload;
1397
+ return choices?.[0]?.delta?.content ?? "";
1398
+ }
1399
+ /**
1400
+ * Map an OpenAI usage event payload to the shared `NormalizedCallUsage` format.
1401
+ *
1402
+ * Reads OpenAI-specific nested fields: `prompt_tokens_details.cached_tokens`
1403
+ * and `completion_tokens_details.reasoning_tokens`.
1404
+ * @param payload - OpenAI usage event payload
1405
+ * @returns Normalized usage metrics
1406
+ */
1407
+ extractUsagePayload(payload) {
1408
+ const usage = payload;
1409
+ return {
1410
+ provider: "openai",
1411
+ inputTokens: usage.prompt_tokens,
1412
+ inputCachedTokens: usage.prompt_tokens_details?.cached_tokens ?? 0,
1413
+ outputTokens: usage.completion_tokens,
1414
+ reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? 0,
1415
+ totalTokens: usage.total_tokens,
1416
+ costUnits: usage.total_tokens,
1417
+ costUnitType: "tokens"
1418
+ };
1419
+ }
1420
+ /**
1421
+ * Emit context window update after OpenAI usage tracking.
1422
+ *
1423
+ * Uses `prompt_tokens_details.cached_tokens` for the cached token count and
1424
+ * defaults to 128,000 tokens (standard GPT-4o context window).
1425
+ * @param payload - OpenAI usage event payload
1426
+ */
1427
+ async emitUsageContextWindowUpdate(payload) {
1428
+ const usage = payload;
1429
+ await this.emitContextWindowUpdate({
1430
+ currentTokens: usage.prompt_tokens + usage.completion_tokens,
1431
+ maxTokens: this.getContextWindowSize() ?? 128e3,
1432
+ cachedTokens: usage.prompt_tokens_details?.cached_tokens
1433
+ });
1434
+ }
1435
+ /**
1436
+ * Reserve a block index for a tool call using the local internal counter.
1437
+ *
1438
+ * OpenAI does not provide provider-native block indices on tool calls, so we
1439
+ * allocate the current counter value and increment to reserve the slot.
1440
+ * @param _tc - OpenAI tool call entry (blockIndex not used)
1441
+ * @returns Current block index (incremented after return by base class via `afterToolCallStepEmitted`)
1442
+ */
1443
+ reserveToolCallBlockIndex(_tc) {
1444
+ return this.getBlockIndex();
1445
+ }
1446
+ /**
1447
+ * Increment the block index after emitting a tool call step.
1448
+ *
1449
+ * OpenAI uses the local counter, so we must explicitly reserve the slot by
1450
+ * incrementing after the step has been emitted.
1451
+ * @param _blockIndex - The block index just emitted (unused; counter manages state)
1452
+ */
1453
+ afterToolCallStepEmitted(_blockIndex) {
1454
+ this.incrementBlockIndex();
1455
+ }
1456
+ /**
1457
+ * Return `-1` as the fallback block index when no map entry exists for a toolCallId.
1458
+ *
1459
+ * OpenAI uses a sentinel value of `-1` to indicate an unknown index, which is
1460
+ * distinguishable from any valid (non-negative) provider block index.
1461
+ * @returns `-1` sentinel value
1462
+ */
1463
+ getFallbackToolCompletedBlockIndex() {
1464
+ return -1;
1465
+ }
1466
+ /**
1467
+ * No-op: OpenAI does not need post-step index reconciliation.
1468
+ *
1469
+ * The internal block index was already incremented inside `afterToolCallStepEmitted`,
1470
+ * so no reconciliation is needed after tool_completed.
1471
+ * @param _resolvedBlockIndex - Unused
1472
+ */
1473
+ afterToolCompletedStepEmitted(_resolvedBlockIndex) {}
1474
+ /**
1475
+ * Build the reasoning `SessionMessageBlock` from an OpenAI reasoning_complete payload.
1476
+ *
1477
+ * OpenAI reasoning events do not carry a `signature` field, so the block
1478
+ * contains only `{ type, content }` without metadata.
1479
+ * @param payload - OpenAI reasoning complete event payload
1480
+ * @returns Plain reasoning block without signature metadata
1481
+ */
1482
+ buildReasoningBlock(payload) {
1483
+ const { content } = payload;
1484
+ return {
1485
+ type: "reasoning",
1486
+ content
1487
+ };
1488
+ }
1489
+ /**
1490
+ * Wire tool approval RPC from connector's scoped bus to global AgentSubjects.toolApprove.
1491
+ *
1492
+ * Uses centralized tool-handling helper for consistent approval flow.
1493
+ * Uses lazy callback to resolve adapterSessionId at request time (not constructor time).
1494
+ * @param connector - The OpenAINodeConnector to wire RPC from
1495
+ */
1496
+ wireToolApprovalRpc(connector) {
1497
+ this.addConnectorWiringCleanup(registerToolApprovalHandler(connector, async () => {
1498
+ if (this.sessionId == null) throw new Error("Agent sessionId is required for tool approval");
1499
+ return {
1500
+ adapterId: this.adapterId,
1501
+ adapterName: this.adapterName,
1502
+ agentId: this.agentId,
1503
+ adapterSessionId: await this.getAdapterSessionId(),
1504
+ sessionId: this.sessionId
1505
+ };
1506
+ }));
1507
+ }
1508
+ };
1509
+
1510
+ //#endregion
1511
+ //#region src/schemas.ts
1512
+ /**
1513
+ * Zod schema for OpenAI Node provider-specific configuration.
1514
+ *
1515
+ * Used for:
1516
+ * 1. Type-safe config resolution
1517
+ * 2. Serialization to JSON Schema for web-ui form generation
1518
+ * 3. Runtime validation
1519
+ */
1520
+ const OpenAINodeProviderConfigSchema = z.object({
1521
+ /**
1522
+ * Base URL for OpenAI-compatible APIs.
1523
+ * Defaults to OpenAI's API. Use for alternatives like:
1524
+ * - NanoGPT: 'https://nano-gpt.com/api/v1'
1525
+ * - Azure OpenAI: 'https://\{resource\}.openai.azure.com/openai/deployments/\{deployment\}'
1526
+ * - Local LLMs: 'http://localhost:1234/v1'
1527
+ */
1528
+ baseUrl: z.string().optional().meta({
1529
+ title: "Base URL",
1530
+ description: "API endpoint for OpenAI-compatible providers"
1531
+ }) });
1532
+ /**
1533
+ * Zod schema for OpenAI Node credential input.
1534
+ *
1535
+ * Used for:
1536
+ * 1. Write-only credential capture in settings UI
1537
+ * 2. Secure storage in credential service
1538
+ */
1539
+ const OpenAINodeCredentialSchema = z.object({
1540
+ /**
1541
+ * API key for OpenAI.
1542
+ */
1543
+ apiKey: z.string().optional().meta({
1544
+ title: "API Key",
1545
+ description: "Stored securely (not saved in config)",
1546
+ format: "password"
1547
+ }) });
1548
+
1549
+ //#endregion
1550
+ //#region src/config.ts
1551
+ const OpenAINodeConfig = createAdapterConfigFactory(() => ({
1552
+ adapterName: OpenAINodeAdapterName,
1553
+ adapterDefaults: {
1554
+ reasoningEffort: "low",
1555
+ providerConfig: {}
1556
+ },
1557
+ schema: OpenAINodeProviderConfigSchema,
1558
+ adapterDefinition: { defaultTimeouts: DEFAULT_TIMEOUTS },
1559
+ protocol: "openai"
1560
+ }));
1561
+
1562
+ //#endregion
1563
+ //#region src/model-normalization.ts
1564
+ /**
1565
+ * Normalize OpenAI model response to AIModel format.
1566
+ *
1567
+ * Handles variance in field names across OpenAI-compatible providers:
1568
+ * - NanoGPT: context_length, name
1569
+ * - Z.AI: (no context_length)
1570
+ * - Moonshot: context_length
1571
+ * - Kimi: context_length, display_name
1572
+ * @param raw - Raw model data from provider API
1573
+ * @param labId - Lab identifier to assign (omit for multi-lab providers)
1574
+ * @returns Normalized model descriptor
1575
+ */
1576
+ function normalizeOpenAIModel(raw, labId) {
1577
+ return {
1578
+ name: raw.id ?? raw.name ?? "unknown",
1579
+ friendlyName: raw.name ?? raw.display_name,
1580
+ contextWindowSize: raw.context_length ?? 0,
1581
+ ...labId ? { labId } : {}
1582
+ };
1583
+ }
1584
+ /**
1585
+ * Normalize array of raw model data from OpenAI /v1/models endpoint.
1586
+ * @param rawModels - Array of raw model data
1587
+ * @param labId - Lab identifier to assign (omit for multi-lab providers)
1588
+ * @returns Array of normalized model objects
1589
+ */
1590
+ function normalizeOpenAIModels(rawModels, labId) {
1591
+ return rawModels.map((raw) => normalizeOpenAIModel(raw, labId));
1592
+ }
1593
+
1594
+ //#endregion
1595
+ //#region src/adapter.ts
1596
+ /**
1597
+ * OpenAI Adapter - Domain-level adapter using the three-layer architecture.
1598
+ *
1599
+ * Architecture:
1600
+ * ```
1601
+ * OpenAIAdapter extends AIAdapter
1602
+ * -> creates via agentFactory()
1603
+ * OpenAIAgent extends AIAgent
1604
+ * -> creates via connectorFactory()
1605
+ * OpenAINodeConnector extends AIAgentConnector
1606
+ * ```
1607
+ *
1608
+ * Responsibilities:
1609
+ * - Handle adapter.startAgent RPC (inherited from AIAdapter)
1610
+ * - Create OpenAIAgent instances with proper configuration
1611
+ * - Emit adapter.initialized and adapter.session.created events
1612
+ * - Manage agent lifecycle (tracking, disposal)
1613
+ * @example
1614
+ * ```typescript
1615
+ * // Using the class directly
1616
+ * const adapter = new OpenAIAdapter();
1617
+ * await adapter.init();
1618
+ *
1619
+ * // Using the convenience factory
1620
+ * const adapter = await createOpenAINodeAdapter();
1621
+ * ```
1622
+ */
1623
+ var OpenAIAdapter = class extends AIAdapter {
1624
+ constructor(config) {
1625
+ super({
1626
+ name: OpenAINodeAdapterName,
1627
+ capabilities: [
1628
+ "tools",
1629
+ "streaming",
1630
+ "systemPrompt:override",
1631
+ "systemPrompt:append"
1632
+ ],
1633
+ ...config,
1634
+ namespace: OpenAINodeConnectorNamespace,
1635
+ agentFactory: (agentConfig) => {
1636
+ return new OpenAIAgent(agentConfig);
1637
+ },
1638
+ configFactory: OpenAINodeConfig.getConfig,
1639
+ connectorFactory: (fullConfig) => new OpenAINodeConnector(fullConfig),
1640
+ definitionProviders: config?.definitionProviders
1641
+ });
1642
+ }
1643
+ /**
1644
+ * Fetch available models from OpenAI-compatible /v1/models endpoint.
1645
+ *
1646
+ * Normalizes responses from various providers (OpenAI, NanoGPT, Z.AI, Kimi, etc.)
1647
+ * to a consistent AIModel[] format.
1648
+ * @param baseUrl - Optional base URL for the provider (defaults to OpenAI)
1649
+ * @param credentials - Optional credentials object with apiKey
1650
+ * @returns Array of normalized model objects
1651
+ * @throws Error if the API request fails
1652
+ */
1653
+ async fetchModels(baseUrl, credentials) {
1654
+ const apiUrl = baseUrl ? `${baseUrl.replace(/\/$/, "")}/models` : "https://api.openai.com/v1/models";
1655
+ const apiKey = credentials?.apiKey;
1656
+ if (!apiKey) throw new Error("API key required for fetching models. Provide via provider configuration, credential storage, or the OPENAI_API_KEY environment variable.");
1657
+ const controller = new AbortController();
1658
+ const timeout = setTimeout(() => controller.abort(), MODEL_FETCH_TIMEOUT_MS);
1659
+ try {
1660
+ const response = await fetch(apiUrl, {
1661
+ headers: {
1662
+ Authorization: `Bearer ${apiKey}`,
1663
+ "Content-Type": "application/json"
1664
+ },
1665
+ signal: controller.signal
1666
+ });
1667
+ if (!response.ok) throw new Error(`Failed to fetch models from ${apiUrl}: ${response.status} ${response.statusText}`);
1668
+ const data = await response.json();
1669
+ if (!data.data || !Array.isArray(data.data)) throw new Error("Invalid response format from /v1/models endpoint - expected { data: [...] }");
1670
+ return normalizeOpenAIModels(data.data);
1671
+ } catch (error) {
1672
+ if (error instanceof Error && error.name === "AbortError") throw new Error(`Failed to fetch models from ${apiUrl}: timed out after ${MODEL_FETCH_TIMEOUT_MS}ms`, { cause: error });
1673
+ throw error;
1674
+ } finally {
1675
+ clearTimeout(timeout);
1676
+ }
1677
+ }
1678
+ };
1679
+ /**
1680
+ * Factory function to create and initialize an OpenAI adapter.
1681
+ *
1682
+ * Convenience wrapper that creates the adapter and calls init() for you.
1683
+ * @param config - Optional adapter configuration
1684
+ * @returns Initialized OpenAIAdapter instance
1685
+ * @example
1686
+ * ```typescript
1687
+ * const adapter = await createOpenAINodeAdapter();
1688
+ *
1689
+ * // Adapter is ready to handle requests via bus
1690
+ * // e.g., MakaioBus.request(AdapterSubjects.startAgent, { adapterId: adapter.adapterId, ... })
1691
+ * ```
1692
+ */
1693
+ async function createOpenAINodeAdapter(config) {
1694
+ const adapter = new OpenAIAdapter(config);
1695
+ await adapter.init();
1696
+ return adapter;
1697
+ }
1698
+
1699
+ //#endregion
1700
+ //#region src/provider.ts
1701
+ /**
1702
+ * Provider IDs and preset configuration for the OpenAI Node adapter.
1703
+ *
1704
+ * The OpenAI adapter supports any OpenAI-compatible API.
1705
+ * Provider compatibility is declared by stable definition ID — the adapter
1706
+ * subsystem resolves each ID to a full ProviderDefinitionInput from the
1707
+ * provider registry at boot time.
1708
+ *
1709
+ * Add new provider IDs here when introducing an OpenAI-compatible provider.
1710
+ */
1711
+ const providerIds = [
1712
+ "openai",
1713
+ "nanogpt",
1714
+ "openrouter",
1715
+ "z-ai",
1716
+ "alibaba",
1717
+ "opencode-go"
1718
+ ];
1719
+ /**
1720
+ * Provider id used for conformance tests.
1721
+ *
1722
+ * Set to `opencode-go` (OpenCode Go gateway) to avoid expensive OpenAI API
1723
+ * calls during test runs while still exercising the OpenAI-compatible wire protocol.
1724
+ */
1725
+ const testPresetId = "opencode-go";
1726
+
1727
+ //#endregion
1728
+ //#region src/index.ts
1729
+ /**
1730
+ * OpenAI Node Adapter
1731
+ *
1732
+ * Provides a three-layer adapter implementation for the OpenAI SDK:
1733
+ * - OpenAIAdapter: Domain-level adapter, handles adapter.* subjects
1734
+ * - OpenAIAgent: Agent wrapper, handles agent.* subjects
1735
+ * - OpenAINodeAgent: SDK connector, handles provider communication
1736
+ * @example
1737
+ * ```typescript
1738
+ * import { OpenAIAdapter, createOpenAINodeAdapter } from '@makaio/adapter-openai-node';
1739
+ *
1740
+ * // Using the class-based adapter
1741
+ * const adapter = new OpenAIAdapter();
1742
+ * await adapter.init();
1743
+ *
1744
+ * // Or using the convenience factory
1745
+ * const adapter = await createOpenAINodeAdapter();
1746
+ *
1747
+ * // Adapter is ready to handle adapter.startAgent RPC via bus
1748
+ * // Start agents via MakaioBus.request(AdapterSubjects.startAgent, { adapterId, ... })
1749
+ * ```
1750
+ * @packageDocumentation
1751
+ */
1752
+ /**
1753
+ * Creates test configuration for conformance test suite.
1754
+ * Sets up scoped bus and tool approval proxy.
1755
+ * @param options - Provider definitions supplied by the conformance harness
1756
+ * @returns Configuration for running conformance tests against this adapter
1757
+ */
1758
+ const createTestConfig = async (options) => {
1759
+ const { scopedBus } = OpenAINodeConnectorNamespace;
1760
+ const bus = await scopedBus();
1761
+ const testPreset = resolveConformanceTestPreset({
1762
+ adapterName: OpenAINodeAdapterName,
1763
+ defaultProviderId: testPresetId,
1764
+ providerIds,
1765
+ providerDefinitions: options?.providerDefinitions,
1766
+ reasoningEffort: "low"
1767
+ });
1768
+ return {
1769
+ createConnector: async (options) => new OpenAINodeConnector(await OpenAINodeConfig.getConfig(resolveTestConfig(options, bus, testPreset.provider, testPreset.providers))),
1770
+ bus,
1771
+ registerToolApprovalHandler,
1772
+ capabilities: {
1773
+ supportsReplace: true,
1774
+ supportsInterrupt: true,
1775
+ supportsUsageMetrics: true
1776
+ },
1777
+ options: {
1778
+ defaultTimeout: 9e4,
1779
+ concurrency: 8,
1780
+ primaryModel: testPreset.primaryModel,
1781
+ secondaryModel: testPreset.secondaryModel
1782
+ },
1783
+ createAdapter: async (options) => createOpenAINodeAdapter({ adapterId: options?.adapterId }),
1784
+ adapterName: OpenAINodeAdapterName,
1785
+ testProviderContext: testPreset.providerContext
1786
+ };
1787
+ };
1788
+
1789
+ //#endregion
1790
+ export { OPENAI_NODE_NAMESPACE, OpenAIAdapter, OpenAIAgent, OpenAIConnectorSession, OpenAIConnectorTurn, OpenAINodeAdapterName, OpenAINodeConnector, OpenAINodeConnectorNamespace, OpenAINodeConnectorSubjects, UserMessageQueue, createOpenAINodeAdapter, createTestConfig };