@tanstack/openai-base 0.10.0 → 0.10.2

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.
Files changed (29) hide show
  1. package/README.md +2 -2
  2. package/dist/esm/adapters/chat-completions-text.d.ts +18 -7
  3. package/dist/esm/adapters/chat-completions-text.js +86 -89
  4. package/dist/esm/adapters/chat-completions-text.js.map +1 -1
  5. package/dist/esm/adapters/chat-completions-tool-converter.test.d.ts +1 -0
  6. package/dist/esm/adapters/responses-text.d.ts +9 -1
  7. package/dist/esm/adapters/responses-text.js +41 -6
  8. package/dist/esm/adapters/responses-text.js.map +1 -1
  9. package/dist/esm/adapters/responses-tool-converter.test.d.ts +1 -0
  10. package/dist/esm/index.d.ts +1 -1
  11. package/dist/esm/index.js +2 -2
  12. package/dist/esm/tools/function-tool.test.d.ts +1 -0
  13. package/dist/esm/utils/schema-converter.d.ts +14 -0
  14. package/dist/esm/utils/schema-converter.js +106 -18
  15. package/dist/esm/utils/schema-converter.js.map +1 -1
  16. package/dist/esm/utils/tool-input-normalizer.d.ts +12 -0
  17. package/dist/esm/utils/tool-input-normalizer.js +36 -0
  18. package/dist/esm/utils/tool-input-normalizer.js.map +1 -0
  19. package/dist/esm/utils/tool-input-normalizer.test.d.ts +1 -0
  20. package/package.json +3 -3
  21. package/src/adapters/chat-completions-text.ts +128 -120
  22. package/src/adapters/chat-completions-tool-converter.test.ts +64 -0
  23. package/src/adapters/responses-text.ts +81 -8
  24. package/src/adapters/responses-tool-converter.test.ts +69 -0
  25. package/src/index.ts +4 -1
  26. package/src/tools/function-tool.test.ts +62 -0
  27. package/src/utils/schema-converter.ts +165 -20
  28. package/src/utils/tool-input-normalizer.test.ts +146 -0
  29. package/src/utils/tool-input-normalizer.ts +55 -0
package/README.md CHANGED
@@ -71,7 +71,7 @@ Per-provider quirks are handled via protected hooks:
71
71
  differences (extra fields, omitted fields, alternative encodings).
72
72
  - `extractReasoning` — surface a provider's reasoning channel into the
73
73
  shared `REASONING_*` AG-UI lifecycle.
74
- - `transformStructuredOutput`, `makeStructuredOutputCompatible` —
74
+ - `transformStructuredOutput`, `makeStructuredOutputCompatibleWithMap` —
75
75
  adjust structured-output handling for provider quirks (e.g. Groq's
76
76
  schema-shape requirements).
77
77
  - `processStreamChunks` — wrap the shared chunk processor for last-mile
@@ -117,5 +117,5 @@ self-hosted gateway, …), import the abstract adapters from this package
117
117
  and subclass them. The existing providers are worked examples —
118
118
  `@tanstack/ai-grok` is the simplest (xAI's API is a near-direct OpenAI
119
119
  Chat Completions clone), `@tanstack/ai-groq` shows the
120
- `processStreamChunks` and `makeStructuredOutputCompatible` override
120
+ `processStreamChunks` and `makeStructuredOutputCompatibleWithMap` override
121
121
  pattern.
@@ -1,7 +1,14 @@
1
1
  import { BaseTextAdapter, StructuredOutputOptions, StructuredOutputResult } from '@tanstack/ai/adapters';
2
+ import { StructuredOutputCompatibility } from '../utils/schema-converter.js';
2
3
  import { default as OpenAI } from 'openai';
3
4
  import { ChatCompletionChunk, ChatCompletionContentPart, ChatCompletionCreateParamsStreaming, ChatCompletionMessageParam } from 'openai/resources/chat/completions/completions';
4
5
  import { ContentPart, DefaultMessageMetadataByModality, Modality, ModelMessage, StreamChunk, TextOptions } from '@tanstack/ai';
6
+ type ChatStreamState = {
7
+ runId: string;
8
+ threadId: string;
9
+ messageId: string;
10
+ hasEmittedRunStarted: boolean;
11
+ };
5
12
  /**
6
13
  * Shared implementation of the OpenAI Chat Completions API. Holds the
7
14
  * stream-accumulator + AG-UI lifecycle logic and calls the OpenAI SDK
@@ -14,6 +21,7 @@ export declare abstract class OpenAIBaseChatCompletionsTextAdapter<TModel extend
14
21
  protected client: OpenAI;
15
22
  constructor(model: TModel, name: string, client: OpenAI);
16
23
  chatStream(options: TextOptions<TProviderOptions>): AsyncIterable<StreamChunk>;
24
+ private handleChatStreamError;
17
25
  /**
18
26
  * Extracts a rejected tool call from a provider error. Returned calls are
19
27
  * emitted as non-executable `output-error` results so the model can repair them.
@@ -54,9 +62,16 @@ export declare abstract class OpenAIBaseChatCompletionsTextAdapter<TModel extend
54
62
  * `@openrouter/sdk`'s `RequestAbortedError`) override to extend the check.
55
63
  */
56
64
  protected isAbortError(error: unknown): boolean;
65
+ /**
66
+ * Strict conversion plus the inverse null-widening map for this request.
67
+ * Override this when schema conversion changes, so tool-input undo matches
68
+ * the wire schema.
69
+ */
70
+ protected makeStructuredOutputCompatibleWithMap(schema: Record<string, any>, originalRequired?: Array<string>): StructuredOutputCompatibility;
57
71
  /**
58
72
  * Applies provider-specific transformations for structured output compatibility.
59
- * Override this in subclasses to handle provider-specific quirks.
73
+ * Override `makeStructuredOutputCompatibleWithMap` when you need the inverse map
74
+ * to match the wire schema.
60
75
  */
61
76
  protected makeStructuredOutputCompatible(schema: Record<string, any>, originalRequired?: Array<string>): Record<string, any>;
62
77
  /**
@@ -85,12 +100,7 @@ export declare abstract class OpenAIBaseChatCompletionsTextAdapter<TModel extend
85
100
  * Processes streamed chunks from the Chat Completions API and yields AG-UI events.
86
101
  * Override this in subclasses to handle provider-specific stream behavior.
87
102
  */
88
- protected processStreamChunks(stream: AsyncIterable<ChatCompletionChunk>, options: TextOptions, aguiState: {
89
- runId: string;
90
- threadId: string;
91
- messageId: string;
92
- hasEmittedRunStarted: boolean;
93
- }): AsyncIterable<StreamChunk>;
103
+ protected processStreamChunks(stream: AsyncIterable<ChatCompletionChunk>, options: TextOptions, aguiState: ChatStreamState): AsyncIterable<StreamChunk>;
94
104
  /**
95
105
  * Maps common TextOptions to Chat Completions API request format.
96
106
  * Override this in subclasses to add provider-specific options.
@@ -124,3 +134,4 @@ export declare abstract class OpenAIBaseChatCompletionsTextAdapter<TModel extend
124
134
  */
125
135
  protected extractTextContent(content: string | null | Array<ContentPart>): string;
126
136
  }
137
+ export {};
@@ -1,6 +1,7 @@
1
- import { makeStructuredOutputCompatible } from "../utils/schema-converter.js";
1
+ import { makeStructuredOutputCompatibleWithMap } from "../utils/schema-converter.js";
2
2
  import { buildChatCompletionsUsage } from "../usage.js";
3
3
  import { extractRequestOptions } from "../utils/request-options.js";
4
+ import { createToolInputNormalizer } from "../utils/tool-input-normalizer.js";
4
5
  import { convertToolsToChatCompletionsFormat } from "./chat-completions-tool-converter.js";
5
6
  import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
6
7
  import { toRunErrorPayload, toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
@@ -42,77 +43,80 @@ var OpenAIBaseChatCompletionsTextAdapter = class extends BaseTextAdapter {
42
43
  }, extractRequestOptions(options.request));
43
44
  yield* this.processStreamChunks(stream, options, aguiState);
44
45
  } catch (error) {
45
- const errorPayload = toRunErrorPayload(error, `${this.name}.chatStream failed`);
46
- const rawEvent = toRunErrorRawEvent(error);
47
- if (!aguiState.hasEmittedRunStarted) {
48
- aguiState.hasEmittedRunStarted = true;
49
- yield {
50
- type: EventType.RUN_STARTED,
51
- runId: aguiState.runId,
52
- threadId: aguiState.threadId,
53
- model: options.model,
54
- timestamp: Date.now(),
55
- parentRunId: options.parentRunId
56
- };
57
- }
58
- const rejectedToolCall = this.extractRejectedToolCall(rawEvent, errorPayload.message);
59
- if (rejectedToolCall) {
60
- const toolCallId = generateId(this.name);
61
- yield {
62
- type: EventType.TOOL_CALL_START,
63
- toolCallId,
64
- toolCallName: rejectedToolCall.toolName,
65
- toolName: rejectedToolCall.toolName,
66
- parentMessageId: aguiState.messageId,
67
- model: options.model,
68
- timestamp: Date.now()
69
- };
70
- yield {
71
- type: EventType.TOOL_CALL_ARGS,
72
- toolCallId,
73
- delta: rejectedToolCall.arguments,
74
- args: rejectedToolCall.arguments,
75
- model: options.model,
76
- timestamp: Date.now()
77
- };
78
- yield {
79
- type: EventType.TOOL_CALL_END,
80
- toolCallId,
81
- toolCallName: rejectedToolCall.toolName,
82
- toolName: rejectedToolCall.toolName,
83
- ...rejectedToolCall.input !== void 0 && { input: rejectedToolCall.input },
84
- result: JSON.stringify({ error: rejectedToolCall.error }),
85
- state: "output-error",
86
- model: options.model,
87
- timestamp: Date.now()
88
- };
89
- yield {
90
- type: EventType.RUN_FINISHED,
91
- runId: aguiState.runId,
92
- threadId: aguiState.threadId,
93
- model: options.model,
94
- timestamp: Date.now(),
95
- finishReason: "tool_calls"
96
- };
97
- return;
98
- }
46
+ yield* this.handleChatStreamError(error, options, aguiState, "chatStream");
47
+ }
48
+ }
49
+ async *handleChatStreamError(error, options, aguiState, source) {
50
+ const errorPayload = toRunErrorPayload(error, `${this.name}.${source} failed`);
51
+ const rawEvent = toRunErrorRawEvent(error);
52
+ if (!aguiState.hasEmittedRunStarted) {
53
+ aguiState.hasEmittedRunStarted = true;
99
54
  yield {
100
- type: EventType.RUN_ERROR,
55
+ type: EventType.RUN_STARTED,
56
+ runId: aguiState.runId,
57
+ threadId: aguiState.threadId,
101
58
  model: options.model,
102
59
  timestamp: Date.now(),
103
- message: errorPayload.message,
104
- code: errorPayload.code,
105
- ...rawEvent !== void 0 && { rawEvent },
106
- error: {
107
- message: errorPayload.message,
108
- code: errorPayload.code
109
- }
60
+ parentRunId: options.parentRunId
110
61
  };
111
- options.logger.errors(`${this.name}.chatStream fatal`, {
112
- error: errorPayload,
113
- source: `${this.name}.chatStream`
114
- });
115
62
  }
63
+ const rejectedToolCall = this.extractRejectedToolCall(rawEvent, errorPayload.message);
64
+ if (rejectedToolCall) {
65
+ const toolCallId = generateId(this.name);
66
+ yield {
67
+ type: EventType.TOOL_CALL_START,
68
+ toolCallId,
69
+ toolCallName: rejectedToolCall.toolName,
70
+ toolName: rejectedToolCall.toolName,
71
+ parentMessageId: aguiState.messageId,
72
+ model: options.model,
73
+ timestamp: Date.now()
74
+ };
75
+ yield {
76
+ type: EventType.TOOL_CALL_ARGS,
77
+ toolCallId,
78
+ delta: rejectedToolCall.arguments,
79
+ args: rejectedToolCall.arguments,
80
+ model: options.model,
81
+ timestamp: Date.now()
82
+ };
83
+ yield {
84
+ type: EventType.TOOL_CALL_END,
85
+ toolCallId,
86
+ toolCallName: rejectedToolCall.toolName,
87
+ toolName: rejectedToolCall.toolName,
88
+ ...rejectedToolCall.input !== void 0 && { input: rejectedToolCall.input },
89
+ result: JSON.stringify({ error: rejectedToolCall.error }),
90
+ state: "output-error",
91
+ model: options.model,
92
+ timestamp: Date.now()
93
+ };
94
+ yield {
95
+ type: EventType.RUN_FINISHED,
96
+ runId: aguiState.runId,
97
+ threadId: aguiState.threadId,
98
+ model: options.model,
99
+ timestamp: Date.now(),
100
+ finishReason: "tool_calls"
101
+ };
102
+ return;
103
+ }
104
+ options.logger.errors(`${this.name}.${source} fatal`, {
105
+ error: errorPayload,
106
+ source: `${this.name}.${source}`
107
+ });
108
+ yield {
109
+ type: EventType.RUN_ERROR,
110
+ model: options.model,
111
+ timestamp: Date.now(),
112
+ message: errorPayload.message,
113
+ ...errorPayload.code !== void 0 && { code: errorPayload.code },
114
+ ...rawEvent !== void 0 && { rawEvent },
115
+ error: {
116
+ message: errorPayload.message,
117
+ ...errorPayload.code !== void 0 && { code: errorPayload.code }
118
+ }
119
+ };
116
120
  }
117
121
  /**
118
122
  * Extracts a rejected tool call from a provider error. Returned calls are
@@ -437,11 +441,20 @@ var OpenAIBaseChatCompletionsTextAdapter = class extends BaseTextAdapter {
437
441
  return e.name === "APIUserAbortError" || e.name === "AbortError" || e.code === "ERR_CANCELED";
438
442
  }
439
443
  /**
444
+ * Strict conversion plus the inverse null-widening map for this request.
445
+ * Override this when schema conversion changes, so tool-input undo matches
446
+ * the wire schema.
447
+ */
448
+ makeStructuredOutputCompatibleWithMap(schema, originalRequired) {
449
+ return makeStructuredOutputCompatibleWithMap(schema, originalRequired);
450
+ }
451
+ /**
440
452
  * Applies provider-specific transformations for structured output compatibility.
441
- * Override this in subclasses to handle provider-specific quirks.
453
+ * Override `makeStructuredOutputCompatibleWithMap` when you need the inverse map
454
+ * to match the wire schema.
442
455
  */
443
456
  makeStructuredOutputCompatible(schema, originalRequired) {
444
- return makeStructuredOutputCompatible(schema, originalRequired);
457
+ return this.makeStructuredOutputCompatibleWithMap(schema, originalRequired).schema;
445
458
  }
446
459
  /**
447
460
  * Extract reasoning content from a stream chunk. Default returns
@@ -470,6 +483,7 @@ var OpenAIBaseChatCompletionsTextAdapter = class extends BaseTextAdapter {
470
483
  * Override this in subclasses to handle provider-specific stream behavior.
471
484
  */
472
485
  async *processStreamChunks(stream, options, aguiState) {
486
+ const normalizeToolInput = createToolInputNormalizer(options.tools, (schema, required) => this.makeStructuredOutputCompatibleWithMap(schema, required));
473
487
  let accumulatedContent = "";
474
488
  let hasEmittedTextMessageStart = false;
475
489
  let lastModel;
@@ -629,7 +643,7 @@ var OpenAIBaseChatCompletionsTextAdapter = class extends BaseTextAdapter {
629
643
  let parsedInput = {};
630
644
  if (toolCall.arguments) try {
631
645
  const parsed = JSON.parse(toolCall.arguments);
632
- parsedInput = parsed && typeof parsed === "object" ? parsed : {};
646
+ parsedInput = normalizeToolInput(toolCall.name, parsed && typeof parsed === "object" ? parsed : {});
633
647
  } catch (parseError) {
634
648
  options.logger.errors(`${this.name}.processStreamChunks tool-args JSON parse failed`, {
635
649
  error: toRunErrorPayload(parseError, `tool ${toolCall.name} (${toolCall.id}) returned malformed JSON arguments`),
@@ -672,7 +686,7 @@ var OpenAIBaseChatCompletionsTextAdapter = class extends BaseTextAdapter {
672
686
  let parsedInput = {};
673
687
  if (toolCall.arguments) try {
674
688
  const parsed = JSON.parse(toolCall.arguments);
675
- parsedInput = parsed && typeof parsed === "object" ? parsed : {};
689
+ parsedInput = normalizeToolInput(toolCall.name, parsed && typeof parsed === "object" ? parsed : {});
676
690
  } catch (parseError) {
677
691
  options.logger.errors(`${this.name}.processStreamChunks tool-args JSON parse failed (drain)`, {
678
692
  error: toRunErrorPayload(parseError, `tool ${toolCall.name} (${toolCall.id}) returned malformed JSON arguments`),
@@ -737,24 +751,7 @@ var OpenAIBaseChatCompletionsTextAdapter = class extends BaseTextAdapter {
737
751
  };
738
752
  }
739
753
  } catch (error) {
740
- const errorPayload = toRunErrorPayload(error, `${this.name}.processStreamChunks failed`);
741
- const rawEvent = toRunErrorRawEvent(error);
742
- options.logger.errors(`${this.name}.processStreamChunks fatal`, {
743
- error: errorPayload,
744
- source: `${this.name}.processStreamChunks`
745
- });
746
- yield {
747
- type: EventType.RUN_ERROR,
748
- model: options.model,
749
- timestamp: Date.now(),
750
- message: errorPayload.message,
751
- ...errorPayload.code !== void 0 && { code: errorPayload.code },
752
- ...rawEvent !== void 0 && { rawEvent },
753
- error: {
754
- message: errorPayload.message,
755
- ...errorPayload.code !== void 0 && { code: errorPayload.code }
756
- }
757
- };
754
+ yield* this.handleChatStreamError(error, options, aguiState, "processStreamChunks");
758
755
  }
759
756
  }
760
757
  /**