@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.
- package/README.md +2 -2
- package/dist/esm/adapters/chat-completions-text.d.ts +18 -7
- package/dist/esm/adapters/chat-completions-text.js +86 -89
- package/dist/esm/adapters/chat-completions-text.js.map +1 -1
- package/dist/esm/adapters/chat-completions-tool-converter.test.d.ts +1 -0
- package/dist/esm/adapters/responses-text.d.ts +9 -1
- package/dist/esm/adapters/responses-text.js +41 -6
- package/dist/esm/adapters/responses-text.js.map +1 -1
- package/dist/esm/adapters/responses-tool-converter.test.d.ts +1 -0
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/tools/function-tool.test.d.ts +1 -0
- package/dist/esm/utils/schema-converter.d.ts +14 -0
- package/dist/esm/utils/schema-converter.js +106 -18
- package/dist/esm/utils/schema-converter.js.map +1 -1
- package/dist/esm/utils/tool-input-normalizer.d.ts +12 -0
- package/dist/esm/utils/tool-input-normalizer.js +36 -0
- package/dist/esm/utils/tool-input-normalizer.js.map +1 -0
- package/dist/esm/utils/tool-input-normalizer.test.d.ts +1 -0
- package/package.json +3 -3
- package/src/adapters/chat-completions-text.ts +128 -120
- package/src/adapters/chat-completions-tool-converter.test.ts +64 -0
- package/src/adapters/responses-text.ts +81 -8
- package/src/adapters/responses-tool-converter.test.ts +69 -0
- package/src/index.ts +4 -1
- package/src/tools/function-tool.test.ts +62 -0
- package/src/utils/schema-converter.ts +165 -20
- package/src/utils/tool-input-normalizer.test.ts +146 -0
- 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`, `
|
|
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 `
|
|
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
|
|
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 {
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
55
|
+
type: EventType.RUN_STARTED,
|
|
56
|
+
runId: aguiState.runId,
|
|
57
|
+
threadId: aguiState.threadId,
|
|
101
58
|
model: options.model,
|
|
102
59
|
timestamp: Date.now(),
|
|
103
|
-
|
|
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
|
|
453
|
+
* Override `makeStructuredOutputCompatibleWithMap` when you need the inverse map
|
|
454
|
+
* to match the wire schema.
|
|
442
455
|
*/
|
|
443
456
|
makeStructuredOutputCompatible(schema, originalRequired) {
|
|
444
|
-
return
|
|
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
|
-
|
|
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
|
/**
|