@robota-sdk/agent-provider 3.0.0-beta.64
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/LICENSE +21 -0
- package/dist/browser/index.d.ts +1104 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +7 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/loggers/index.cjs +1 -0
- package/dist/loggers/index.d.ts +151 -0
- package/dist/loggers/index.d.ts.map +1 -0
- package/dist/loggers/index.js +2 -0
- package/dist/loggers/index.js.map +1 -0
- package/dist/node/anthropic/index.cjs +1 -0
- package/dist/node/anthropic/index.d.ts +158 -0
- package/dist/node/anthropic/index.d.ts.map +1 -0
- package/dist/node/anthropic/index.js +1 -0
- package/dist/node/anthropic--1vgLC-e.js +5 -0
- package/dist/node/anthropic--1vgLC-e.js.map +1 -0
- package/dist/node/anthropic-BFQ6DSCP.cjs +4 -0
- package/dist/node/bytedance/index.cjs +1 -0
- package/dist/node/bytedance/index.d.ts +74 -0
- package/dist/node/bytedance/index.d.ts.map +1 -0
- package/dist/node/bytedance/index.js +1 -0
- package/dist/node/bytedance-C_0sF_pJ.js +2 -0
- package/dist/node/bytedance-C_0sF_pJ.js.map +1 -0
- package/dist/node/bytedance-DVPxqEiC.cjs +1 -0
- package/dist/node/chunk-Bmb41Sf3.cjs +1 -0
- package/dist/node/deepseek/index.cjs +1 -0
- package/dist/node/deepseek/index.d.ts +2 -0
- package/dist/node/deepseek/index.js +1 -0
- package/dist/node/deepseek-_8Ixx7rA.js +2 -0
- package/dist/node/deepseek-_8Ixx7rA.js.map +1 -0
- package/dist/node/deepseek-oA2Y6bD0.cjs +1 -0
- package/dist/node/gemini/index.cjs +1 -0
- package/dist/node/gemini/index.d.ts +173 -0
- package/dist/node/gemini/index.d.ts.map +1 -0
- package/dist/node/gemini/index.js +1 -0
- package/dist/node/gemini-Bh2U87MY.js +4 -0
- package/dist/node/gemini-Bh2U87MY.js.map +1 -0
- package/dist/node/gemini-DSaNCxZj.cjs +3 -0
- package/dist/node/gemma/index.cjs +1 -0
- package/dist/node/gemma/index.d.ts +2 -0
- package/dist/node/gemma/index.js +1 -0
- package/dist/node/gemma-Dp_AfCUR.js +2 -0
- package/dist/node/gemma-Dp_AfCUR.js.map +1 -0
- package/dist/node/gemma-G-Pf_PnX.cjs +1 -0
- package/dist/node/google/index.cjs +1 -0
- package/dist/node/google/index.d.ts +14 -0
- package/dist/node/google/index.d.ts.map +1 -0
- package/dist/node/google/index.js +2 -0
- package/dist/node/google/index.js.map +1 -0
- package/dist/node/index-B6PnlDMd.d.ts +82 -0
- package/dist/node/index-B6PnlDMd.d.ts.map +1 -0
- package/dist/node/index-B7UvPJcI.d.ts +315 -0
- package/dist/node/index-B7UvPJcI.d.ts.map +1 -0
- package/dist/node/index-BLPOTNb5.d.ts +98 -0
- package/dist/node/index-BLPOTNb5.d.ts.map +1 -0
- package/dist/node/index-BqixM_XD.d.ts +231 -0
- package/dist/node/index-BqixM_XD.d.ts.map +1 -0
- package/dist/node/index-C3beaqKO.d.ts +231 -0
- package/dist/node/index-C3beaqKO.d.ts.map +1 -0
- package/dist/node/index-Cp2XRh9G.d.ts +82 -0
- package/dist/node/index-Cp2XRh9G.d.ts.map +1 -0
- package/dist/node/index-DSv5xruI.d.ts +98 -0
- package/dist/node/index-DSv5xruI.d.ts.map +1 -0
- package/dist/node/index-w0bV1uaP.d.ts +315 -0
- package/dist/node/index-w0bV1uaP.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.ts +8 -0
- package/dist/node/index.js +1 -0
- package/dist/node/openai/index.cjs +1 -0
- package/dist/node/openai/index.d.ts +2 -0
- package/dist/node/openai/index.js +1 -0
- package/dist/node/openai-CRQjg4xF.js +2 -0
- package/dist/node/openai-CRQjg4xF.js.map +1 -0
- package/dist/node/openai-compatible-BYfyY5lb.cjs +1 -0
- package/dist/node/openai-compatible-Dm4Sof9e.js +2 -0
- package/dist/node/openai-compatible-Dm4Sof9e.js.map +1 -0
- package/dist/node/openai-xWC6pY7r.cjs +1 -0
- package/dist/node/qwen/index.cjs +1 -0
- package/dist/node/qwen/index.d.ts +2 -0
- package/dist/node/qwen/index.js +1 -0
- package/dist/node/qwen-ChUZobTL.js +2 -0
- package/dist/node/qwen-ChUZobTL.js.map +1 -0
- package/dist/node/qwen-CjT71vSM.cjs +1 -0
- package/package.json +157 -0
- package/src/anthropic/__tests__/abort-streaming.test.ts +199 -0
- package/src/anthropic/__tests__/model-catalog-refresh.test.ts +92 -0
- package/src/anthropic/__tests__/provider-definition.test.ts +55 -0
- package/src/anthropic/__tests__/provider.test.ts +1357 -0
- package/src/anthropic/__tests__/response-parser.test.ts +326 -0
- package/src/anthropic/index.ts +22 -0
- package/src/anthropic/message-converter.ts +181 -0
- package/src/anthropic/model-catalog-refresh.ts +128 -0
- package/src/anthropic/parsers/response-parser.ts +184 -0
- package/src/anthropic/provider-definition.ts +93 -0
- package/src/anthropic/provider.ts +290 -0
- package/src/anthropic/streaming-handler.ts +204 -0
- package/src/anthropic/types/api-types.ts +158 -0
- package/src/anthropic/types.ts +79 -0
- package/src/bytedance/http-client.test.ts +288 -0
- package/src/bytedance/http-client.ts +163 -0
- package/src/bytedance/index.ts +2 -0
- package/src/bytedance/provider.spec.ts +320 -0
- package/src/bytedance/provider.ts +171 -0
- package/src/bytedance/status-mapper.test.ts +299 -0
- package/src/bytedance/status-mapper.ts +141 -0
- package/src/bytedance/types.ts +68 -0
- package/src/deepseek/defaults.ts +4 -0
- package/src/deepseek/index.ts +22 -0
- package/src/deepseek/model-catalog-refresh.test.ts +57 -0
- package/src/deepseek/model-catalog-refresh.ts +105 -0
- package/src/deepseek/model-catalog.ts +55 -0
- package/src/deepseek/provider-definition.test.ts +109 -0
- package/src/deepseek/provider-definition.ts +132 -0
- package/src/deepseek/provider.test.ts +324 -0
- package/src/deepseek/provider.ts +298 -0
- package/src/deepseek/types.ts +37 -0
- package/src/gemini/execution-helpers.ts +233 -0
- package/src/gemini/genai-transport.test.ts +208 -0
- package/src/gemini/image-operations.test.ts +448 -0
- package/src/gemini/image-operations.ts +261 -0
- package/src/gemini/index.ts +11 -0
- package/src/gemini/message-converter.test.ts +616 -0
- package/src/gemini/message-converter.ts +140 -0
- package/src/gemini/model-catalog-refresh.test.ts +107 -0
- package/src/gemini/model-catalog-refresh.ts +92 -0
- package/src/gemini/provider-definition.test.ts +70 -0
- package/src/gemini/provider-definition.ts +78 -0
- package/src/gemini/provider-extended.test.ts +898 -0
- package/src/gemini/provider.spec.ts +216 -0
- package/src/gemini/provider.ts +279 -0
- package/src/gemini/request-converter.ts +226 -0
- package/src/gemini/tool-schema-converter.ts +78 -0
- package/src/gemini/types/api-types.ts +235 -0
- package/src/gemini/types.ts +121 -0
- package/src/gemma/index.ts +5 -0
- package/src/gemma/message-factory.ts +38 -0
- package/src/gemma/provider-definition.test.ts +43 -0
- package/src/gemma/provider-definition.ts +84 -0
- package/src/gemma/provider-projection.ts +49 -0
- package/src/gemma/provider.test.ts +628 -0
- package/src/gemma/provider.ts +308 -0
- package/src/gemma/pseudo-command-envelope.ts +58 -0
- package/src/gemma/pseudo-tool-call-projector.ts +243 -0
- package/src/gemma/pseudo-tool-call-tag-parser.ts +153 -0
- package/src/gemma/pseudo-tool-call-types.ts +31 -0
- package/src/gemma/reasoning-projector.test.ts +52 -0
- package/src/gemma/reasoning-projector.ts +144 -0
- package/src/gemma/streaming-projection.ts +79 -0
- package/src/gemma/tool-call-argument-parser.ts +126 -0
- package/src/gemma/tool-call-projector.test.ts +227 -0
- package/src/gemma/tool-call-projector.ts +264 -0
- package/src/gemma/types.ts +27 -0
- package/src/google/index.ts +11 -0
- package/src/google/provider-compat.test.ts +19 -0
- package/src/google/provider-definition.ts +6 -0
- package/src/google/provider.ts +10 -0
- package/src/google/types.ts +5 -0
- package/src/index.ts +9 -0
- package/src/openai/adapter.test.ts +494 -0
- package/src/openai/adapter.ts +145 -0
- package/src/openai/chat-completions-chat.ts +189 -0
- package/src/openai/executor-integration.test.ts +206 -0
- package/src/openai/index.ts +21 -0
- package/src/openai/interfaces/payload-logger.ts +48 -0
- package/src/openai/loggers/console-payload-logger.test.ts +173 -0
- package/src/openai/loggers/console-payload-logger.ts +94 -0
- package/src/openai/loggers/console.ts +9 -0
- package/src/openai/loggers/file-payload-logger.test.ts +238 -0
- package/src/openai/loggers/file-payload-logger.ts +112 -0
- package/src/openai/loggers/file.ts +9 -0
- package/src/openai/loggers/index.ts +12 -0
- package/src/openai/loggers/sanitize-openai-log-data.test.ts +89 -0
- package/src/openai/loggers/sanitize-openai-log-data.ts +14 -0
- package/src/openai/message-converter.ts +22 -0
- package/src/openai/model-catalog-refresh.test.ts +92 -0
- package/src/openai/model-catalog-refresh.ts +115 -0
- package/src/openai/openai-request-format.ts +92 -0
- package/src/openai/parsers/response-parser.test.ts +407 -0
- package/src/openai/parsers/response-parser.ts +47 -0
- package/src/openai/provider-definition.test.ts +75 -0
- package/src/openai/provider-definition.ts +132 -0
- package/src/openai/provider.test.ts +1402 -0
- package/src/openai/provider.ts +237 -0
- package/src/openai/responses-chat.ts +258 -0
- package/src/openai/responses-converter.ts +112 -0
- package/src/openai/responses-parser.ts +285 -0
- package/src/openai/responses-stream-utils.ts +45 -0
- package/src/openai/responses-types.ts +195 -0
- package/src/openai/streaming/stream-assembler.ts +3 -0
- package/src/openai/streaming/stream-handler.test.ts +367 -0
- package/src/openai/streaming/stream-handler.ts +119 -0
- package/src/openai/types/api-types.ts +112 -0
- package/src/openai/types.ts +194 -0
- package/src/qwen/defaults.ts +26 -0
- package/src/qwen/index.ts +5 -0
- package/src/qwen/model-catalog-refresh.test.ts +91 -0
- package/src/qwen/model-catalog-refresh.ts +97 -0
- package/src/qwen/provider-capabilities.ts +34 -0
- package/src/qwen/provider-definition.test.ts +139 -0
- package/src/qwen/provider-definition.ts +173 -0
- package/src/qwen/provider-streaming-assembly.ts +40 -0
- package/src/qwen/provider.test.ts +640 -0
- package/src/qwen/provider.ts +293 -0
- package/src/qwen/responses-chat.ts +194 -0
- package/src/qwen/responses-converter.ts +104 -0
- package/src/qwen/responses-parser.ts +299 -0
- package/src/qwen/responses-stream-utils.ts +38 -0
- package/src/qwen/types.ts +228 -0
- package/src/shared/openai-compatible/endpoint-probe.test.ts +52 -0
- package/src/shared/openai-compatible/endpoint-probe.ts +43 -0
- package/src/shared/openai-compatible/index.ts +6 -0
- package/src/shared/openai-compatible/message-converter.test.ts +111 -0
- package/src/shared/openai-compatible/message-converter.ts +84 -0
- package/src/shared/openai-compatible/native-payload-observer.test.ts +43 -0
- package/src/shared/openai-compatible/native-payload-observer.ts +26 -0
- package/src/shared/openai-compatible/response-parser.test.ts +172 -0
- package/src/shared/openai-compatible/response-parser.ts +180 -0
- package/src/shared/openai-compatible/stream-assembler.test.ts +266 -0
- package/src/shared/openai-compatible/stream-assembler.ts +248 -0
- package/src/shared/openai-compatible/types.ts +59 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import type { IOpenAIProviderOptions, TOpenAIApiSurface } from './types';
|
|
3
|
+
import { AbstractAIProvider } from '@robota-sdk/agent-core';
|
|
4
|
+
import type {
|
|
5
|
+
TUniversalMessage,
|
|
6
|
+
IChatOptions,
|
|
7
|
+
IAssistantMessage,
|
|
8
|
+
IProviderCapabilities,
|
|
9
|
+
TTextDeltaCallback,
|
|
10
|
+
} from '@robota-sdk/agent-core';
|
|
11
|
+
import type { IPayloadLogger } from './interfaces/payload-logger';
|
|
12
|
+
import { OpenAIResponseParser } from './parsers/response-parser';
|
|
13
|
+
import { SilentLogger } from '@robota-sdk/agent-core';
|
|
14
|
+
import {
|
|
15
|
+
chatStreamWithOpenAIChatCompletions,
|
|
16
|
+
chatWithOpenAIChatCompletions,
|
|
17
|
+
} from './chat-completions-chat';
|
|
18
|
+
import { chatStreamWithOpenAIResponsesApi, chatWithOpenAIResponsesApi } from './responses-chat';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* OpenAI provider implementation for Robota
|
|
22
|
+
*
|
|
23
|
+
* Provides integration with OpenAI models through the Robota provider contract.
|
|
24
|
+
* Uses OpenAI SDK native types internally for optimal performance and feature support.
|
|
25
|
+
*
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
export class OpenAIProvider extends AbstractAIProvider {
|
|
29
|
+
override readonly name = 'openai';
|
|
30
|
+
override readonly version = '1.0.0';
|
|
31
|
+
|
|
32
|
+
private readonly client?: OpenAI;
|
|
33
|
+
private readonly options: IOpenAIProviderOptions;
|
|
34
|
+
private readonly apiSurface: TOpenAIApiSurface;
|
|
35
|
+
private readonly payloadLogger: IPayloadLogger | undefined;
|
|
36
|
+
private readonly responseParser: OpenAIResponseParser;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optional callback for text deltas during streaming.
|
|
40
|
+
* Set by the consumer (e.g., Session) to receive real-time text chunks.
|
|
41
|
+
* When set, chat() uses streaming internally while still returning
|
|
42
|
+
* the complete assembled message.
|
|
43
|
+
*/
|
|
44
|
+
onTextDelta?: TTextDeltaCallback;
|
|
45
|
+
|
|
46
|
+
constructor(options: IOpenAIProviderOptions) {
|
|
47
|
+
super(options.logger || SilentLogger);
|
|
48
|
+
this.options = options;
|
|
49
|
+
this.apiSurface = resolveApiSurface(options);
|
|
50
|
+
validateOpenAIProviderNativeWebTools(this.apiSurface, options.nativeWebTools);
|
|
51
|
+
|
|
52
|
+
if (options.executor) {
|
|
53
|
+
this.executor = options.executor;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!this.executor) {
|
|
57
|
+
if (options.client) {
|
|
58
|
+
this.client = options.client;
|
|
59
|
+
} else if (options.apiKey) {
|
|
60
|
+
this.client = new OpenAI({
|
|
61
|
+
apiKey: options.apiKey,
|
|
62
|
+
...(options.organization && { organization: options.organization }),
|
|
63
|
+
...(options.timeout && { timeout: options.timeout }),
|
|
64
|
+
...(options.baseURL && { baseURL: options.baseURL }),
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
throw new Error('Either OpenAI client, apiKey, or executor is required');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.responseParser = new OpenAIResponseParser(this.logger);
|
|
72
|
+
this.payloadLogger = options.payloadLogger;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override async chat(
|
|
76
|
+
messages: TUniversalMessage[],
|
|
77
|
+
options?: IChatOptions,
|
|
78
|
+
): Promise<TUniversalMessage> {
|
|
79
|
+
this.validateMessages(messages);
|
|
80
|
+
this.validateNativeWebTools(options?.nativeWebTools);
|
|
81
|
+
|
|
82
|
+
if (this.executor) {
|
|
83
|
+
try {
|
|
84
|
+
return await this.executeViaExecutorOrDirect(messages, options);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
this.logger.error(
|
|
87
|
+
'OpenAI Provider executor chat error:',
|
|
88
|
+
error instanceof Error ? error.message : String(error),
|
|
89
|
+
);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (this.apiSurface === 'responses') {
|
|
95
|
+
return chatWithOpenAIResponsesApi({
|
|
96
|
+
client: this.client,
|
|
97
|
+
messages,
|
|
98
|
+
chatOptions: options,
|
|
99
|
+
providerOptions: this.options,
|
|
100
|
+
onTextDelta: this.onTextDelta,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return chatWithOpenAIChatCompletions({
|
|
105
|
+
client: this.client,
|
|
106
|
+
messages,
|
|
107
|
+
chatOptions: options,
|
|
108
|
+
providerOptions: this.options,
|
|
109
|
+
payloadLogger: this.payloadLogger,
|
|
110
|
+
responseParser: this.responseParser,
|
|
111
|
+
onTextDelta: this.onTextDelta,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
override async *chatStream(
|
|
116
|
+
messages: TUniversalMessage[],
|
|
117
|
+
options?: IChatOptions,
|
|
118
|
+
): AsyncIterable<TUniversalMessage> {
|
|
119
|
+
this.validateNativeWebTools(options?.nativeWebTools);
|
|
120
|
+
|
|
121
|
+
if (this.executor) {
|
|
122
|
+
try {
|
|
123
|
+
yield* this.executeStreamViaExecutorOrDirect(messages, options);
|
|
124
|
+
return;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
this.logger.error(
|
|
127
|
+
'OpenAI Provider executor stream error:',
|
|
128
|
+
error instanceof Error ? error.message : String(error),
|
|
129
|
+
);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this.apiSurface === 'responses') {
|
|
135
|
+
yield* chatStreamWithOpenAIResponsesApi({
|
|
136
|
+
client: this.client,
|
|
137
|
+
messages,
|
|
138
|
+
chatOptions: options,
|
|
139
|
+
providerOptions: this.options,
|
|
140
|
+
onTextDelta: this.onTextDelta,
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
yield* chatStreamWithOpenAIChatCompletions({
|
|
146
|
+
client: this.client,
|
|
147
|
+
messages,
|
|
148
|
+
chatOptions: options,
|
|
149
|
+
providerOptions: this.options,
|
|
150
|
+
payloadLogger: this.payloadLogger,
|
|
151
|
+
responseParser: this.responseParser,
|
|
152
|
+
onTextDelta: this.onTextDelta,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
override supportsTools(): boolean {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
override getCapabilities(): IProviderCapabilities {
|
|
161
|
+
const source =
|
|
162
|
+
this.apiSurface === 'chat-completions'
|
|
163
|
+
? 'openai-compatible-chat-completions'
|
|
164
|
+
: 'openai-responses';
|
|
165
|
+
return {
|
|
166
|
+
functionCalling: { supported: true },
|
|
167
|
+
nativeWebTools: {
|
|
168
|
+
webSearch: {
|
|
169
|
+
supported: false,
|
|
170
|
+
enabled: false,
|
|
171
|
+
source,
|
|
172
|
+
reason: getOpenAIUnsupportedNativeWebReason(this.apiSurface, 'search'),
|
|
173
|
+
},
|
|
174
|
+
webFetch: {
|
|
175
|
+
supported: false,
|
|
176
|
+
enabled: false,
|
|
177
|
+
source,
|
|
178
|
+
reason: getOpenAIUnsupportedNativeWebReason(this.apiSurface, 'fetch'),
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
override validateConfig(): boolean {
|
|
185
|
+
return !!this.client && !!this.options;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
override async dispose(): Promise<void> {
|
|
189
|
+
// OpenAI client doesn't need explicit cleanup
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
protected override validateMessages(messages: TUniversalMessage[]): void {
|
|
193
|
+
super.validateMessages(messages);
|
|
194
|
+
|
|
195
|
+
for (const message of messages) {
|
|
196
|
+
if (message.role === 'assistant') {
|
|
197
|
+
const assistantMsg = message as IAssistantMessage;
|
|
198
|
+
if (
|
|
199
|
+
assistantMsg.toolCalls &&
|
|
200
|
+
assistantMsg.toolCalls.length > 0 &&
|
|
201
|
+
assistantMsg.content === ''
|
|
202
|
+
) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function resolveApiSurface(options: IOpenAIProviderOptions): TOpenAIApiSurface {
|
|
211
|
+
if (options.apiSurface !== undefined) {
|
|
212
|
+
return options.apiSurface;
|
|
213
|
+
}
|
|
214
|
+
return options.baseURL ? 'chat-completions' : 'responses';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getOpenAIUnsupportedNativeWebReason(
|
|
218
|
+
apiSurface: TOpenAIApiSurface,
|
|
219
|
+
toolKind: 'search' | 'fetch',
|
|
220
|
+
): string {
|
|
221
|
+
if (apiSurface === 'chat-completions') {
|
|
222
|
+
return `OpenAI-compatible Chat Completions endpoints support declared function tools, not provider-native web ${toolKind}.`;
|
|
223
|
+
}
|
|
224
|
+
return `OpenAI Responses native web ${toolKind} is not wired in this Robota provider version.`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function validateOpenAIProviderNativeWebTools(
|
|
228
|
+
apiSurface: TOpenAIApiSurface,
|
|
229
|
+
nativeWebTools: IOpenAIProviderOptions['nativeWebTools'],
|
|
230
|
+
): void {
|
|
231
|
+
if (nativeWebTools?.webSearch !== true && nativeWebTools?.webFetch !== true) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
throw new Error(
|
|
235
|
+
`Provider openai native web search/fetch is not supported for apiSurface ${apiSurface} in this Robota provider version.`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type OpenAI from 'openai';
|
|
3
|
+
import type { IChatOptions, TTextDeltaCallback, TUniversalMessage } from '@robota-sdk/agent-core';
|
|
4
|
+
import { observeProviderNativeRawPayloadStream } from '../shared/openai-compatible/index.js';
|
|
5
|
+
import type { IOpenAIError } from './types/api-types';
|
|
6
|
+
import type { IOpenAIProviderOptions } from './types';
|
|
7
|
+
import { buildOpenAIResponsesTextConfig } from './openai-request-format';
|
|
8
|
+
import {
|
|
9
|
+
convertToOpenAIResponsesInput,
|
|
10
|
+
convertToOpenAIResponsesTools,
|
|
11
|
+
} from './responses-converter';
|
|
12
|
+
import { assembleOpenAIResponsesStream, parseOpenAIResponsesResponse } from './responses-parser';
|
|
13
|
+
import type {
|
|
14
|
+
IOpenAIResponsesRequestNonStreaming,
|
|
15
|
+
IOpenAIResponsesRequestStreaming,
|
|
16
|
+
TOpenAIResponsesStreamEvent,
|
|
17
|
+
} from './responses-types';
|
|
18
|
+
|
|
19
|
+
export interface IOpenAIResponsesChatOptions {
|
|
20
|
+
client?: OpenAI;
|
|
21
|
+
messages: TUniversalMessage[];
|
|
22
|
+
chatOptions?: IChatOptions;
|
|
23
|
+
providerOptions: IOpenAIProviderOptions;
|
|
24
|
+
onTextDelta?: TTextDeltaCallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface IResponsesStreamMessageQueue {
|
|
28
|
+
deltas: TUniversalMessage[];
|
|
29
|
+
finalMessage?: TUniversalMessage;
|
|
30
|
+
error?: Error;
|
|
31
|
+
wake?: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function chatWithOpenAIResponsesApi(
|
|
35
|
+
input: IOpenAIResponsesChatOptions,
|
|
36
|
+
): Promise<TUniversalMessage> {
|
|
37
|
+
const textDeltaCb = input.chatOptions?.onTextDelta ?? input.onTextDelta;
|
|
38
|
+
if (textDeltaCb) {
|
|
39
|
+
return chatWithOpenAIResponsesStreamingAssembly({
|
|
40
|
+
...input,
|
|
41
|
+
chatOptions: {
|
|
42
|
+
...input.chatOptions,
|
|
43
|
+
onTextDelta: textDeltaCb,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!input.client) {
|
|
49
|
+
throw new Error('OpenAI Responses client not available.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const requestParams = buildResponsesRequestParams(input);
|
|
54
|
+
input.chatOptions?.onProviderNativeRawPayload?.({
|
|
55
|
+
provider: 'openai',
|
|
56
|
+
apiSurface: 'responses',
|
|
57
|
+
payloadKind: 'request',
|
|
58
|
+
payload: requestParams,
|
|
59
|
+
});
|
|
60
|
+
const response = await input.client.responses.create(
|
|
61
|
+
requestParams as OpenAI.Responses.ResponseCreateParamsNonStreaming,
|
|
62
|
+
input.chatOptions?.signal ? { signal: input.chatOptions.signal } : undefined,
|
|
63
|
+
);
|
|
64
|
+
input.chatOptions?.onProviderNativeRawPayload?.({
|
|
65
|
+
provider: 'openai',
|
|
66
|
+
apiSurface: 'responses',
|
|
67
|
+
payloadKind: 'response',
|
|
68
|
+
payload: response,
|
|
69
|
+
});
|
|
70
|
+
return parseOpenAIResponsesResponse(response);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const openaiError = error as IOpenAIError;
|
|
73
|
+
const errorMessage = openaiError.message || 'OpenAI Responses API request failed';
|
|
74
|
+
throw new Error(`OpenAI responses failed: ${errorMessage}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function* chatStreamWithOpenAIResponsesApi(
|
|
79
|
+
input: IOpenAIResponsesChatOptions,
|
|
80
|
+
): AsyncIterable<TUniversalMessage> {
|
|
81
|
+
const queue: IResponsesStreamMessageQueue = { deltas: [] };
|
|
82
|
+
const textDeltaCb = input.chatOptions?.onTextDelta ?? input.onTextDelta;
|
|
83
|
+
const assembly = chatWithOpenAIResponsesStreamingAssembly({
|
|
84
|
+
...input,
|
|
85
|
+
chatOptions: {
|
|
86
|
+
...input.chatOptions,
|
|
87
|
+
onTextDelta: (delta) => {
|
|
88
|
+
textDeltaCb?.(delta);
|
|
89
|
+
enqueueStreamDelta(queue, createStreamDeltaMessage(delta));
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
.then((result) => finishStreamQueue(queue, result))
|
|
94
|
+
.catch((error) => failStreamQueue(queue, toError(error)));
|
|
95
|
+
|
|
96
|
+
yield* drainResponsesStreamQueue(queue);
|
|
97
|
+
await assembly;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function chatWithOpenAIResponsesStreamingAssembly(
|
|
101
|
+
input: IOpenAIResponsesChatOptions,
|
|
102
|
+
): Promise<TUniversalMessage> {
|
|
103
|
+
if (!input.client) {
|
|
104
|
+
throw new Error('OpenAI Responses client not available.');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const requestParams = buildResponsesStreamingRequestParams(input);
|
|
109
|
+
input.chatOptions?.onProviderNativeRawPayload?.({
|
|
110
|
+
provider: 'openai',
|
|
111
|
+
apiSurface: 'responses',
|
|
112
|
+
payloadKind: 'request',
|
|
113
|
+
payload: requestParams,
|
|
114
|
+
});
|
|
115
|
+
const stream = await input.client.responses.create(
|
|
116
|
+
requestParams as OpenAI.Responses.ResponseCreateParamsStreaming,
|
|
117
|
+
input.chatOptions?.signal ? { signal: input.chatOptions.signal } : undefined,
|
|
118
|
+
);
|
|
119
|
+
return assembleOpenAIResponsesStream({
|
|
120
|
+
stream: observeProviderNativeRawPayloadStream(
|
|
121
|
+
stream as AsyncIterable<TOpenAIResponsesStreamEvent>,
|
|
122
|
+
{
|
|
123
|
+
provider: 'openai',
|
|
124
|
+
apiSurface: 'responses',
|
|
125
|
+
onProviderNativeRawPayload: input.chatOptions?.onProviderNativeRawPayload,
|
|
126
|
+
},
|
|
127
|
+
),
|
|
128
|
+
onTextDelta: input.chatOptions?.onTextDelta,
|
|
129
|
+
signal: input.chatOptions?.signal,
|
|
130
|
+
});
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const openaiError = error as IOpenAIError;
|
|
133
|
+
const errorMessage = openaiError.message || 'OpenAI Responses streaming request failed';
|
|
134
|
+
throw new Error(`OpenAI responses stream failed: ${errorMessage}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildResponsesRequestParams(
|
|
139
|
+
input: IOpenAIResponsesChatOptions,
|
|
140
|
+
): IOpenAIResponsesRequestNonStreaming {
|
|
141
|
+
const model = input.chatOptions?.model ?? input.providerOptions.defaultModel;
|
|
142
|
+
if (!model) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
'Model is required in chat options. Please specify a model in defaultModel configuration.',
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const tools = convertToOpenAIResponsesTools(
|
|
149
|
+
input.chatOptions?.tools,
|
|
150
|
+
input.providerOptions.strictTools,
|
|
151
|
+
);
|
|
152
|
+
const textConfig = buildOpenAIResponsesTextConfig(input.providerOptions);
|
|
153
|
+
return {
|
|
154
|
+
model,
|
|
155
|
+
input: convertToOpenAIResponsesInput(input.messages),
|
|
156
|
+
...(tools !== undefined && { tools, tool_choice: 'auto' }),
|
|
157
|
+
...(input.chatOptions?.temperature !== undefined && {
|
|
158
|
+
temperature: input.chatOptions.temperature,
|
|
159
|
+
}),
|
|
160
|
+
...(input.chatOptions?.maxTokens !== undefined && {
|
|
161
|
+
max_output_tokens: input.chatOptions.maxTokens,
|
|
162
|
+
}),
|
|
163
|
+
...(textConfig !== undefined && { text: textConfig }),
|
|
164
|
+
...(input.providerOptions.reasoning !== undefined && {
|
|
165
|
+
reasoning: input.providerOptions.reasoning,
|
|
166
|
+
}),
|
|
167
|
+
...(input.providerOptions.includeEncryptedReasoning === true && {
|
|
168
|
+
include: ['reasoning.encrypted_content'],
|
|
169
|
+
}),
|
|
170
|
+
...(input.providerOptions.store !== undefined && { store: input.providerOptions.store }),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildResponsesStreamingRequestParams(
|
|
175
|
+
input: IOpenAIResponsesChatOptions,
|
|
176
|
+
): IOpenAIResponsesRequestStreaming {
|
|
177
|
+
return {
|
|
178
|
+
...buildResponsesRequestParams(input),
|
|
179
|
+
stream: true,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function createStreamDeltaMessage(delta: string): TUniversalMessage {
|
|
184
|
+
return {
|
|
185
|
+
id: randomUUID(),
|
|
186
|
+
role: 'assistant',
|
|
187
|
+
content: delta,
|
|
188
|
+
state: 'complete',
|
|
189
|
+
timestamp: new Date(),
|
|
190
|
+
metadata: {
|
|
191
|
+
providerApiSurface: 'responses',
|
|
192
|
+
isStreamChunk: true,
|
|
193
|
+
isComplete: false,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function* drainResponsesStreamQueue(
|
|
199
|
+
queue: IResponsesStreamMessageQueue,
|
|
200
|
+
): AsyncIterable<TUniversalMessage> {
|
|
201
|
+
while (true) {
|
|
202
|
+
const next = queue.deltas.shift();
|
|
203
|
+
if (next !== undefined) {
|
|
204
|
+
yield next;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (queue.error !== undefined) {
|
|
208
|
+
throw queue.error;
|
|
209
|
+
}
|
|
210
|
+
if (queue.finalMessage !== undefined) {
|
|
211
|
+
yield createFinalStreamMessage(queue.finalMessage);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
await waitForStreamQueue(queue);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function enqueueStreamDelta(queue: IResponsesStreamMessageQueue, message: TUniversalMessage): void {
|
|
219
|
+
queue.deltas.push(message);
|
|
220
|
+
wakeStreamQueue(queue);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function finishStreamQueue(queue: IResponsesStreamMessageQueue, result: TUniversalMessage): void {
|
|
224
|
+
queue.finalMessage = result;
|
|
225
|
+
wakeStreamQueue(queue);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function failStreamQueue(queue: IResponsesStreamMessageQueue, error: Error): void {
|
|
229
|
+
queue.error = error;
|
|
230
|
+
wakeStreamQueue(queue);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function waitForStreamQueue(queue: IResponsesStreamMessageQueue): Promise<void> {
|
|
234
|
+
return new Promise((resolve) => {
|
|
235
|
+
queue.wake = resolve;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function wakeStreamQueue(queue: IResponsesStreamMessageQueue): void {
|
|
240
|
+
queue.wake?.();
|
|
241
|
+
queue.wake = undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function createFinalStreamMessage(result: TUniversalMessage): TUniversalMessage {
|
|
245
|
+
return {
|
|
246
|
+
...result,
|
|
247
|
+
content: '',
|
|
248
|
+
metadata: {
|
|
249
|
+
...result.metadata,
|
|
250
|
+
isStreamChunk: true,
|
|
251
|
+
isComplete: true,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function toError(error: Error | string): Error {
|
|
257
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
258
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IAssistantMessage,
|
|
3
|
+
IToolSchema,
|
|
4
|
+
TUniversalMessage,
|
|
5
|
+
TUniversalMessagePart,
|
|
6
|
+
} from '@robota-sdk/agent-core';
|
|
7
|
+
import type {
|
|
8
|
+
IOpenAIResponsesFunctionTool,
|
|
9
|
+
IOpenAIResponsesMessageInput,
|
|
10
|
+
TOpenAIResponsesInputContent,
|
|
11
|
+
TOpenAIResponsesInputItem,
|
|
12
|
+
} from './responses-types';
|
|
13
|
+
|
|
14
|
+
export function convertToOpenAIResponsesInput(
|
|
15
|
+
messages: TUniversalMessage[],
|
|
16
|
+
): TOpenAIResponsesInputItem[] {
|
|
17
|
+
return messages.flatMap((message) => convertMessage(message));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function convertToOpenAIResponsesTools(
|
|
21
|
+
tools: IToolSchema[] | undefined,
|
|
22
|
+
strictTools: boolean | undefined,
|
|
23
|
+
): IOpenAIResponsesFunctionTool[] | undefined {
|
|
24
|
+
const converted =
|
|
25
|
+
tools?.map((tool) => ({
|
|
26
|
+
type: 'function' as const,
|
|
27
|
+
name: tool.name,
|
|
28
|
+
description: tool.description,
|
|
29
|
+
parameters: tool.parameters,
|
|
30
|
+
strict: strictTools ?? false,
|
|
31
|
+
})) ?? [];
|
|
32
|
+
return converted.length > 0 ? converted : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function convertMessage(message: TUniversalMessage): TOpenAIResponsesInputItem[] {
|
|
36
|
+
if (message.role === 'user') {
|
|
37
|
+
return [createMessageInput('user', getUserContent(message.content, message.parts))];
|
|
38
|
+
}
|
|
39
|
+
if (message.role === 'system') {
|
|
40
|
+
return [createMessageInput('system', message.content)];
|
|
41
|
+
}
|
|
42
|
+
if (message.role === 'tool') {
|
|
43
|
+
if (!message.toolCallId || message.toolCallId.trim().length === 0) {
|
|
44
|
+
throw new Error(`Tool message missing toolCallId: ${JSON.stringify(message)}`);
|
|
45
|
+
}
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
type: 'function_call_output',
|
|
49
|
+
call_id: message.toolCallId,
|
|
50
|
+
output: message.content || '',
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
return convertAssistantMessage(message);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function convertAssistantMessage(message: IAssistantMessage): TOpenAIResponsesInputItem[] {
|
|
58
|
+
const items: TOpenAIResponsesInputItem[] = [];
|
|
59
|
+
if (message.content && message.content.length > 0) {
|
|
60
|
+
items.push(createMessageInput('assistant', message.content));
|
|
61
|
+
}
|
|
62
|
+
for (const toolCall of message.toolCalls ?? []) {
|
|
63
|
+
items.push({
|
|
64
|
+
type: 'function_call',
|
|
65
|
+
call_id: toolCall.id,
|
|
66
|
+
name: toolCall.function.name,
|
|
67
|
+
arguments: toolCall.function.arguments,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (items.length === 0) {
|
|
71
|
+
items.push(createMessageInput('assistant', ''));
|
|
72
|
+
}
|
|
73
|
+
return items;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getUserContent(
|
|
77
|
+
content: string,
|
|
78
|
+
parts: TUniversalMessagePart[] | undefined,
|
|
79
|
+
): string | TOpenAIResponsesInputContent[] {
|
|
80
|
+
if (!parts || parts.length === 0) {
|
|
81
|
+
return content;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const converted = parts.map((part) => convertPart(part));
|
|
85
|
+
if (content.length > 0 && !parts.some((part) => part.type === 'text')) {
|
|
86
|
+
return [{ type: 'input_text', text: content }, ...converted];
|
|
87
|
+
}
|
|
88
|
+
return converted;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function convertPart(part: TUniversalMessagePart): TOpenAIResponsesInputContent {
|
|
92
|
+
if (part.type === 'text') {
|
|
93
|
+
return { type: 'input_text', text: part.text };
|
|
94
|
+
}
|
|
95
|
+
if (part.type === 'image_uri') {
|
|
96
|
+
return { type: 'input_image', image_url: part.uri };
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
type: 'input_image',
|
|
100
|
+
image_url: `data:${part.mimeType};base64,${part.data}`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createMessageInput(
|
|
105
|
+
role: IOpenAIResponsesMessageInput['role'],
|
|
106
|
+
content: string | TOpenAIResponsesInputContent[],
|
|
107
|
+
): IOpenAIResponsesMessageInput {
|
|
108
|
+
return {
|
|
109
|
+
role,
|
|
110
|
+
content,
|
|
111
|
+
};
|
|
112
|
+
}
|