@flink-app/openai-adapter 2.0.0-alpha.100

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.
@@ -0,0 +1,667 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.OpenAIAdapter = void 0;
7
+ const flink_1 = require("@flink-app/flink");
8
+ const openai_1 = __importDefault(require("openai"));
9
+ /**
10
+ * OpenAI adapter for Flink AI framework using the Responses API
11
+ *
12
+ * Built on OpenAI's Responses API (evolution of Chat Completions) which provides:
13
+ * - Step-aware reasoning: Model can return multiple tool calls in a single response
14
+ * - Explicit tool invocation: Tool calls and results are first-class typed items
15
+ * - Better performance: 3% improvement on SWE-bench, 5% on TAUBench
16
+ *
17
+ * ## Architecture
18
+ *
19
+ * IMPORTANT: The Responses API does NOT run your agent loop.
20
+ *
21
+ * What it provides:
22
+ * - Multiple tool calls in one response (but you execute them)
23
+ * - Structured step types (message, function_call, function_call_output)
24
+ * - Streaming event taxonomy
25
+ *
26
+ * What you still handle:
27
+ * - Tool execution (Flink's AgentRunner does this)
28
+ * - Multi-turn loops (call API → execute tools → call API again)
29
+ * - Deciding when to stop
30
+ * - Managing conversation state
31
+ *
32
+ * This is intentional - it gives you full control over the agent loop.
33
+ *
34
+ * ## Streaming-Only Design
35
+ *
36
+ * This adapter uses streaming as the primary method because:
37
+ * - Better user experience (time-to-first-token <500ms)
38
+ * - Prevents HTTP timeouts for long responses
39
+ * - FlinkAgent's lazy generator pattern supports both streaming and non-streaming UX
40
+ *
41
+ * For non-streaming consumption, simply await the final result:
42
+ * ```typescript
43
+ * const response = agent.execute({ message: "Hello" });
44
+ * const result = await response.result; // Consumes stream but returns final result
45
+ * ```
46
+ *
47
+ * ## System Instructions - Two Approaches
48
+ *
49
+ * You can provide system instructions in two ways:
50
+ *
51
+ * 1. **Top-level instructions** (via Flink's instructions parameter):
52
+ * - Automatically prepended as system message to conversation
53
+ * - Simpler approach, recommended for most cases
54
+ * - Following Vercel AI SDK pattern
55
+ *
56
+ * 2. **System role messages** (via messages array):
57
+ * - Add `{ role: "system", content: "..." }` to messages
58
+ * - Gives more control over message structure and placement
59
+ * - Useful when you need system context at specific points in conversation
60
+ *
61
+ * Both approaches can be used together - system messages in input array + top-level instructions.
62
+ *
63
+ * ## Features
64
+ * - ✅ Step-based tool calling with multiple calls per response
65
+ * - ✅ Streaming support with event-based updates
66
+ * - ✅ Structured outputs with JSON schema (100% reliability)
67
+ * - ✅ Support for all OpenAI models (GPT-4.1, GPT-5, o-series)
68
+ * - ✅ Flexible system instructions (top-level or in messages)
69
+ *
70
+ * ## Basic Usage
71
+ * ```typescript
72
+ * import { OpenAIAdapter } from "@flink-app/openai-adapter";
73
+ * import { FlinkApp } from "@flink-app/flink";
74
+ *
75
+ * const app = new FlinkApp({
76
+ * ai: {
77
+ * llms: {
78
+ * default: new OpenAIAdapter({
79
+ * apiKey: process.env.OPENAI_API_KEY!,
80
+ * model: "gpt-5"
81
+ * }),
82
+ * },
83
+ * },
84
+ * });
85
+ * ```
86
+ *
87
+ * ## Structured Outputs
88
+ * ```typescript
89
+ * const adapter = new OpenAIAdapter({
90
+ * apiKey: process.env.OPENAI_API_KEY!,
91
+ * model: "gpt-5",
92
+ * structuredOutput: {
93
+ * type: "json_schema",
94
+ * name: "user_profile",
95
+ * schema: {
96
+ * type: "object",
97
+ * properties: {
98
+ * name: { type: "string" },
99
+ * age: { type: "number" }
100
+ * },
101
+ * required: ["name", "age"],
102
+ * additionalProperties: false
103
+ * },
104
+ * strict: true
105
+ * }
106
+ * });
107
+ * ```
108
+ */
109
+ class OpenAIAdapter {
110
+ constructor(optionsOrApiKey, model) {
111
+ this.log = flink_1.FlinkLogFactory.createLogger("flink.ai.openai");
112
+ if (typeof optionsOrApiKey === "string") {
113
+ // Legacy constructor: new OpenAIAdapter(apiKey, model)
114
+ this.client = new openai_1.default({ apiKey: optionsOrApiKey });
115
+ this.model = model;
116
+ this.debug = false; // Default: no debug logging
117
+ }
118
+ else {
119
+ // New constructor: new OpenAIAdapter({ apiKey, model, structuredOutput, debug })
120
+ this.client = new openai_1.default({ apiKey: optionsOrApiKey.apiKey });
121
+ this.model = optionsOrApiKey.model;
122
+ this.structuredOutput = optionsOrApiKey.structuredOutput;
123
+ this.debug = optionsOrApiKey.debug ?? false; // Default: no debug logging
124
+ }
125
+ // Enable debug logging for this component if debug flag is set
126
+ if (this.debug) {
127
+ this.log.setLevel("debug");
128
+ this.log.info("OpenAI adapter initialized with debug logging enabled");
129
+ }
130
+ }
131
+ async *stream(params) {
132
+ try {
133
+ // Convert instructions to system message + Flink messages to OpenAI Responses API format
134
+ // Following Vercel AI SDK pattern: instructions are prepended as a system message
135
+ const input = this.convertMessagesToResponsesInput(params.instructions, params.messages);
136
+ // Convert tools to Responses API format
137
+ const tools = params.tools.length > 0 ? this.convertToolsToResponsesFormat(params.tools) : undefined;
138
+ this.log.trace(JSON.stringify(tools));
139
+ // Build text format config for structured outputs
140
+ const textFormat = this.structuredOutput
141
+ ? {
142
+ format: {
143
+ type: "json_schema",
144
+ name: this.structuredOutput.name,
145
+ description: this.structuredOutput.description,
146
+ schema: this.structuredOutput.schema,
147
+ strict: this.structuredOutput.strict ?? true,
148
+ },
149
+ }
150
+ : undefined;
151
+ // Call Responses API with streaming
152
+ // Note: Each stream is one turn; Flink's AgentRunner handles the multi-turn loop
153
+ // IMPORTANT: Do NOT include top-level instructions field - we prepend as system message in input array
154
+ const streamParams = {
155
+ model: this.model,
156
+ input: input,
157
+ tools: tools,
158
+ max_output_tokens: params.maxTokens,
159
+ text: textFormat,
160
+ stream: true,
161
+ // Only include temperature for models that support it
162
+ // Reasoning models (o1, o3, gpt-5) don't support temperature
163
+ ...(this.isReasoningModel() ? {} : { temperature: params.temperature }),
164
+ };
165
+ this.log.debug("Starting OpenAI stream request", {
166
+ model: this.model,
167
+ messageCount: input.length,
168
+ toolCount: tools?.length || 0,
169
+ maxTokens: params.maxTokens,
170
+ hasStructuredOutput: !!this.structuredOutput,
171
+ });
172
+ if (this.debug) {
173
+ this.log.debug("Full request params:", streamParams);
174
+ }
175
+ const stream = await this.client.responses.stream(streamParams);
176
+ this.log.debug("Stream connection established");
177
+ let totalInputTokens = 0;
178
+ let totalOutputTokens = 0;
179
+ let cachedInputTokens = 0;
180
+ let finalStatus = "completed";
181
+ // Track function calls in progress (streaming may send partial data)
182
+ const functionCallsInProgress = new Map();
183
+ for await (const event of stream) {
184
+ // Handle different event types from Responses streaming
185
+ if (event.type === "response.output_item.added") {
186
+ const addedEvent = event;
187
+ const item = addedEvent.item;
188
+ if (item?.type === "function_call") {
189
+ functionCallsInProgress.set(item.call_id, {
190
+ name: item.name || "",
191
+ arguments: "",
192
+ });
193
+ }
194
+ }
195
+ else if (event.type === "response.output_text.delta") {
196
+ const textEvent = event;
197
+ yield { type: "text", delta: textEvent.delta || "" };
198
+ }
199
+ else if (event.type === "response.function_call_arguments.delta") {
200
+ const argsEvent = event;
201
+ const callId = argsEvent.item_id;
202
+ const existing = functionCallsInProgress.get(callId);
203
+ if (existing) {
204
+ existing.arguments += argsEvent.delta || "";
205
+ }
206
+ }
207
+ else if (event.type === "response.output_item.done") {
208
+ const doneEvent = event;
209
+ const item = doneEvent.item;
210
+ if (item?.type === "function_call") {
211
+ // Read arguments from the done event item, not from accumulated deltas
212
+ // The item.arguments property contains the complete arguments string
213
+ const funcItem = item;
214
+ // Log the tool call decision from LLM
215
+ this.log.debug("LLM decided to call tool", {
216
+ name: funcItem.name,
217
+ call_id: funcItem.call_id,
218
+ arguments: funcItem.arguments,
219
+ });
220
+ // Handle empty string arguments (LLM sometimes sends "" instead of "{}")
221
+ const argsStr = funcItem.arguments?.trim() || "{}";
222
+ yield {
223
+ type: "tool_call",
224
+ toolCall: {
225
+ id: funcItem.call_id,
226
+ name: funcItem.name,
227
+ input: JSON.parse(argsStr),
228
+ },
229
+ };
230
+ }
231
+ }
232
+ else if (event.type === "response.completed") {
233
+ const completedEvent = event;
234
+ const response = completedEvent.response;
235
+ if (response?.usage) {
236
+ totalInputTokens = response.usage.input_tokens || 0;
237
+ totalOutputTokens = response.usage.output_tokens || 0;
238
+ // Capture cached tokens from prompt_tokens_details
239
+ // @ts-ignore - OpenAI SDK types may not include this yet
240
+ cachedInputTokens = response.usage.input_tokens_details?.cached_tokens || 0;
241
+ }
242
+ finalStatus = response?.status || "completed";
243
+ }
244
+ }
245
+ // Log token usage
246
+ this.log.info("OpenAI request completed", {
247
+ inputTokens: totalInputTokens,
248
+ outputTokens: totalOutputTokens,
249
+ cachedInputTokens: cachedInputTokens,
250
+ totalTokens: totalInputTokens + totalOutputTokens,
251
+ status: finalStatus,
252
+ model: this.model,
253
+ });
254
+ // Yield usage
255
+ yield {
256
+ type: "usage",
257
+ usage: {
258
+ inputTokens: totalInputTokens,
259
+ outputTokens: totalOutputTokens,
260
+ ...(cachedInputTokens > 0 && { cachedInputTokens }),
261
+ },
262
+ };
263
+ // Yield done
264
+ yield {
265
+ type: "done",
266
+ stopReason: this.normalizeStopReason(finalStatus),
267
+ };
268
+ }
269
+ catch (error) {
270
+ // Log the error
271
+ this.log.error("OpenAI API request failed", {
272
+ status: error.status,
273
+ message: error.message,
274
+ model: this.model,
275
+ error: error,
276
+ });
277
+ // Enhanced error handling for OpenAI API errors
278
+ if (error.status === 400) {
279
+ throw new Error(`OpenAI API error (400 Bad Request): ${error.message || "Invalid request"}. ` +
280
+ `Common causes: invalid model name, malformed messages, or invalid tool schemas. ` +
281
+ `Model: ${this.model}`);
282
+ }
283
+ throw error;
284
+ }
285
+ }
286
+ /**
287
+ * Convert Flink messages to OpenAI Responses API input format
288
+ *
289
+ * Always sends full conversation history for reliability.
290
+ *
291
+ * The Responses API accepts:
292
+ * 1. System messages: { role: "system", content: "..." }
293
+ * 2. User messages: { role: "user", content: "..." }
294
+ * 3. Assistant messages: { role: "assistant", content: "..." }
295
+ * 4. Function calls: { type: "function_call", ... } ← From previous responses
296
+ * 5. Function call outputs: { type: "function_call_output", ... } ← Tool results
297
+ *
298
+ * Multi-turn pattern:
299
+ * - User: "What's the weather?"
300
+ * - Assistant: "Let me check" + function_call
301
+ * - function_call_output: "22°C"
302
+ * - Assistant: "It's 22°C and sunny"
303
+ * - User: "What about tomorrow?"
304
+ *
305
+ * All message types are preserved for full conversation context.
306
+ */
307
+ /**
308
+ * Build the OpenAI user content from text/image content blocks.
309
+ * Returns a plain string when there are no images (most common case),
310
+ * or an array of input_text / input_image items for multimodal messages.
311
+ * Returns null if there are no user-facing blocks.
312
+ */
313
+ buildOpenAIUserContent(blocks) {
314
+ const textBlocks = blocks.filter((b) => b.type === "text");
315
+ const imageBlocks = blocks.filter((b) => b.type === "image");
316
+ if (imageBlocks.length === 0) {
317
+ // Text-only: keep as plain string for backwards compat
318
+ const firstText = textBlocks[0];
319
+ return firstText && firstText.type === "text" ? firstText.text : null;
320
+ }
321
+ // Multimodal: build content array
322
+ const content = [];
323
+ for (const block of textBlocks) {
324
+ if (block.type === "text")
325
+ content.push({ type: "input_text", text: block.text });
326
+ }
327
+ for (const block of imageBlocks) {
328
+ if (block.type === "image")
329
+ content.push({ type: "input_image", image_url: block.url });
330
+ }
331
+ return content.length > 0 ? content : null;
332
+ }
333
+ convertMessagesToResponsesInput(instructions, messages) {
334
+ const result = [];
335
+ // 1. Prepend instructions as system message (Vercel AI SDK pattern)
336
+ if (instructions) {
337
+ result.push({
338
+ role: "system",
339
+ content: instructions,
340
+ });
341
+ }
342
+ // Convert all messages to full conversation history
343
+ for (const msg of messages) {
344
+ // 2. Handle additional system messages (for dynamic context)
345
+ if (msg.role === "system") {
346
+ result.push({
347
+ role: "system",
348
+ content: msg.content,
349
+ });
350
+ continue;
351
+ }
352
+ // 3. Handle user messages
353
+ if (msg.role === "user") {
354
+ if (typeof msg.content === "string") {
355
+ // Simple text message
356
+ result.push({
357
+ role: "user",
358
+ content: msg.content,
359
+ });
360
+ }
361
+ else {
362
+ // Handle content blocks (tool results + text + images)
363
+ const content = msg.content;
364
+ const toolResultBlocks = content.filter((block) => block.type === "tool_result");
365
+ const userBlocks = content.filter((block) => block.type !== "tool_result");
366
+ // Add function call outputs (tool results) — these are top-level items
367
+ for (const block of toolResultBlocks) {
368
+ if (block.type === "tool_result") {
369
+ result.push({
370
+ type: "function_call_output",
371
+ call_id: block.tool_use_id,
372
+ output: block.content,
373
+ });
374
+ }
375
+ }
376
+ // Add user content (text + images) if present
377
+ const userContent = this.buildOpenAIUserContent(userBlocks);
378
+ if (userContent) {
379
+ result.push({ role: "user", content: userContent });
380
+ }
381
+ }
382
+ continue;
383
+ }
384
+ // 4. Handle assistant messages
385
+ if (msg.role === "assistant") {
386
+ if (typeof msg.content === "string") {
387
+ // Pass through text-only assistant messages
388
+ result.push({
389
+ role: "assistant",
390
+ content: msg.content,
391
+ });
392
+ continue;
393
+ }
394
+ // Handle assistant messages with content blocks (tool calls)
395
+ const content = msg.content;
396
+ const textBlocks = content.filter((block) => block.type === "text");
397
+ const toolUseBlocks = content.filter((block) => block.type === "tool_use");
398
+ // Add assistant message if there's text content
399
+ if (textBlocks.length > 0) {
400
+ const firstTextBlock = textBlocks[0];
401
+ if (firstTextBlock.type === "text") {
402
+ result.push({
403
+ role: "assistant",
404
+ content: firstTextBlock.text,
405
+ });
406
+ }
407
+ }
408
+ // Add function calls (these MUST be included for context)
409
+ for (const block of toolUseBlocks) {
410
+ if (block.type === "tool_use") {
411
+ result.push({
412
+ type: "function_call",
413
+ call_id: block.id,
414
+ name: block.name,
415
+ arguments: JSON.stringify(block.input),
416
+ });
417
+ }
418
+ }
419
+ continue;
420
+ }
421
+ }
422
+ return result;
423
+ }
424
+ /**
425
+ * Convert Flink tools to Responses API format
426
+ *
427
+ * Responses API uses internally-tagged format:
428
+ * { type: "function", name: "...", description: "...", parameters: {...} }
429
+ *
430
+ * vs Chat Completions externally-tagged format:
431
+ * { type: "function", function: { name: "...", description: "...", parameters: {...} } }
432
+ */
433
+ convertToolsToResponsesFormat(tools) {
434
+ return tools.map((tool) => ({
435
+ type: "function",
436
+ name: tool.name,
437
+ description: tool.description,
438
+ parameters: this.sanitizeSchemaForStrictMode(tool.inputSchema),
439
+ strict: true,
440
+ }));
441
+ }
442
+ /**
443
+ * Sanitize JSON Schema for OpenAI's strict mode
444
+ *
445
+ * OpenAI's strict mode only supports a subset of JSON Schema.
446
+ * This function removes unsupported keywords that would cause the model
447
+ * to silently fail and return empty {} arguments.
448
+ *
449
+ * Unsupported keywords (stripped):
450
+ * - $schema (meta-schema reference)
451
+ * - minLength, maxLength, pattern (string constraints)
452
+ * - minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf (number constraints)
453
+ * - minItems, maxItems, uniqueItems (array constraints)
454
+ * - minProperties, maxProperties (object constraints)
455
+ * - format (string format hints like "email", "uri")
456
+ *
457
+ * Enforced requirements:
458
+ * - additionalProperties: false on all objects (required by strict mode)
459
+ * - required array must include all properties without default values
460
+ *
461
+ * Note: Removed constraints should be documented in the description field
462
+ * where the model can read them as natural language instructions.
463
+ *
464
+ * Reference: https://platform.openai.com/docs/guides/structured-outputs
465
+ */
466
+ sanitizeSchemaForStrictMode(schema) {
467
+ // Deep clone to avoid mutating the original schema
468
+ const sanitized = JSON.parse(JSON.stringify(schema));
469
+ // OpenAI requires the root schema to be of type "object"
470
+ if (sanitized.type !== "object") {
471
+ throw new Error(`OpenAI tool input schema must have root type "object", got "${sanitized.type ?? "unknown"}". ` + `Wrap your input type in an object interface.`);
472
+ }
473
+ const unsupportedKeywords = [
474
+ "$schema",
475
+ "minLength",
476
+ "maxLength",
477
+ "pattern",
478
+ "minimum",
479
+ "maximum",
480
+ "exclusiveMinimum",
481
+ "exclusiveMaximum",
482
+ "multipleOf",
483
+ "minItems",
484
+ "maxItems",
485
+ "uniqueItems",
486
+ "minProperties",
487
+ "maxProperties",
488
+ "format",
489
+ ];
490
+ const sanitizeNode = (node) => {
491
+ if (!node || typeof node !== "object")
492
+ return;
493
+ // OpenAI does not allow any sibling keywords alongside $ref — strip them all
494
+ if (node.$ref) {
495
+ const ref = node.$ref;
496
+ for (const key of Object.keys(node)) {
497
+ if (key !== "$ref")
498
+ delete node[key];
499
+ }
500
+ node.$ref = ref;
501
+ return; // nothing else to process on a $ref node
502
+ }
503
+ // Remove unsupported keywords from this node
504
+ for (const keyword of unsupportedKeywords) {
505
+ delete node[keyword];
506
+ }
507
+ // Convert const to enum (OpenAI strict mode prefers enum; const alone may lack type)
508
+ // e.g. { "const": "normal" } → { "type": "string", "enum": ["normal"] }
509
+ if (node.hasOwnProperty("const")) {
510
+ const val = node.const;
511
+ if (!node.type) {
512
+ if (val === null)
513
+ node.type = "null";
514
+ else if (typeof val === "string")
515
+ node.type = "string";
516
+ else if (typeof val === "number")
517
+ node.type = "number";
518
+ else if (typeof val === "boolean")
519
+ node.type = "boolean";
520
+ }
521
+ node.enum = [val];
522
+ delete node.const;
523
+ }
524
+ // Enforce additionalProperties: false on objects (required by strict mode)
525
+ // Also ensure properties exists (OpenAI requires it even for empty objects)
526
+ if (node.type === "object" && !node.properties) {
527
+ node.properties = {};
528
+ }
529
+ if (node.type === "object" && node.properties) {
530
+ node.additionalProperties = false;
531
+ // Fix the required array to only include properties without defaults
532
+ // OpenAI requires all properties to either be in 'required' or have a 'default'
533
+ if (node.required && Array.isArray(node.required)) {
534
+ const propertyKeys = Object.keys(node.properties);
535
+ const requiredFields = propertyKeys.filter((key) => {
536
+ const prop = node.properties[key];
537
+ return !prop.hasOwnProperty("default");
538
+ });
539
+ node.required = requiredFields;
540
+ }
541
+ else if (node.properties) {
542
+ // If no required array exists, create one with all properties without defaults
543
+ const propertyKeys = Object.keys(node.properties);
544
+ const requiredFields = propertyKeys.filter((key) => {
545
+ const prop = node.properties[key];
546
+ return !prop.hasOwnProperty("default");
547
+ });
548
+ if (requiredFields.length > 0) {
549
+ node.required = requiredFields;
550
+ }
551
+ }
552
+ }
553
+ // Recursively sanitize nested structures
554
+ if (node.properties) {
555
+ for (const key in node.properties) {
556
+ sanitizeNode(node.properties[key]);
557
+ }
558
+ }
559
+ if (node.items) {
560
+ if (Array.isArray(node.items)) {
561
+ node.items.forEach(sanitizeNode);
562
+ }
563
+ else {
564
+ sanitizeNode(node.items);
565
+ }
566
+ }
567
+ if (node.allOf)
568
+ node.allOf.forEach(sanitizeNode);
569
+ if (node.anyOf) {
570
+ node.anyOf.forEach(sanitizeNode);
571
+ // Collapse anyOf of simple typed enums into a cleaner form.
572
+ // e.g. anyOf[{type:"string",enum:["a"]},{type:"string",enum:["b"]},{type:"number",enum:[1]}]
573
+ // → anyOf[{type:"string",enum:["a","b"]},{type:"number",enum:[1]}]
574
+ // And if only one type remains, unwrap the anyOf entirely:
575
+ // → {type:"string",enum:["a","b"]}
576
+ const isSimpleEnum = (e) => e &&
577
+ typeof e === "object" &&
578
+ typeof e.type === "string" &&
579
+ Array.isArray(e.enum) &&
580
+ !e.$ref &&
581
+ !e.anyOf &&
582
+ !e.allOf &&
583
+ !e.oneOf &&
584
+ !e.properties &&
585
+ !e.items;
586
+ if (node.anyOf.every(isSimpleEnum)) {
587
+ const byType = {};
588
+ for (const entry of node.anyOf) {
589
+ if (!byType[entry.type])
590
+ byType[entry.type] = [];
591
+ byType[entry.type].push(...entry.enum);
592
+ }
593
+ const types = Object.keys(byType);
594
+ if (types.length === 1) {
595
+ node.type = types[0];
596
+ node.enum = byType[types[0]];
597
+ delete node.anyOf;
598
+ }
599
+ else {
600
+ node.anyOf = types.map((t) => ({ type: t, enum: byType[t] }));
601
+ }
602
+ }
603
+ }
604
+ if (node.oneOf)
605
+ node.oneOf.forEach(sanitizeNode);
606
+ if (node.not)
607
+ sanitizeNode(node.not);
608
+ // Sanitize schemas in $defs (added by ToolExecutor to make schemas self-contained)
609
+ if (node.$defs && typeof node.$defs === "object") {
610
+ for (const def of Object.values(node.$defs)) {
611
+ sanitizeNode(def);
612
+ }
613
+ }
614
+ };
615
+ sanitizeNode(sanitized);
616
+ return sanitized;
617
+ }
618
+ /**
619
+ * Check if the model is a reasoning model that doesn't support temperature
620
+ *
621
+ * Reasoning models use a different internal process and don't allow temperature control:
622
+ * - o1, o1-mini, o1-preview
623
+ * - o3, o3-mini
624
+ * - gpt-5 (uses reasoning approach)
625
+ */
626
+ isReasoningModel() {
627
+ const modelLower = this.model.toLowerCase();
628
+ return modelLower.startsWith("o1") || modelLower.startsWith("o3") || modelLower.startsWith("gpt-5");
629
+ }
630
+ /**
631
+ * Normalize stop reason from Responses API to Flink format
632
+ *
633
+ * LIMITATION: This mapping is lossy. Responses API provides richer status:
634
+ * - "completed" → "end_turn"
635
+ * - "incomplete" → "max_tokens"
636
+ * - "failed" → "max_tokens" (loses error signal)
637
+ * - "cancelled" → "end_turn" (loses cancellation signal)
638
+ *
639
+ * Future improvement: Add LLMResponse.metadata field to preserve:
640
+ * - Raw status from provider
641
+ * - Safety stop reasons
642
+ * - Tool execution errors
643
+ * - Model-specific error codes
644
+ *
645
+ * For now, we map to Flink's limited set and lose granularity.
646
+ */
647
+ normalizeStopReason(status) {
648
+ // Map Responses API status to Flink's stopReason enum
649
+ switch (status) {
650
+ case "incomplete":
651
+ return "max_tokens"; // Hit token limit
652
+ case "failed":
653
+ // TODO: This loses important signal (safety, tool_error, model_error)
654
+ // Consider logging or adding metadata field
655
+ return "max_tokens"; // Best approximation for now
656
+ case "cancelled":
657
+ return "end_turn"; // User/system cancelled
658
+ case "completed":
659
+ default:
660
+ // For Responses API, tool calls are part of the response items
661
+ // so we typically get "completed" even when tools were called
662
+ return "end_turn";
663
+ }
664
+ }
665
+ }
666
+ exports.OpenAIAdapter = OpenAIAdapter;
667
+ //# sourceMappingURL=OpenAIAdapter.js.map