@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,189 @@
|
|
|
1
|
+
import type OpenAI from 'openai';
|
|
2
|
+
import type { IChatOptions, TTextDeltaCallback, TUniversalMessage } from '@robota-sdk/agent-core';
|
|
3
|
+
import { observeProviderNativeRawPayloadStream } from '../shared/openai-compatible/index.js';
|
|
4
|
+
import type { IPayloadLogger } from './interfaces/payload-logger';
|
|
5
|
+
import type { IOpenAIError, IOpenAILogData } from './types/api-types';
|
|
6
|
+
import type { IOpenAIProviderOptions } from './types';
|
|
7
|
+
import { buildOpenAIChatResponseFormat } from './openai-request-format';
|
|
8
|
+
import { convertToOpenAIMessages, convertToOpenAITools } from './message-converter';
|
|
9
|
+
import { OpenAIResponseParser } from './parsers/response-parser';
|
|
10
|
+
import { assembleOpenAIStream } from './streaming/stream-assembler';
|
|
11
|
+
|
|
12
|
+
export interface IOpenAIChatCompletionsOptions {
|
|
13
|
+
client?: OpenAI;
|
|
14
|
+
messages: TUniversalMessage[];
|
|
15
|
+
chatOptions?: IChatOptions;
|
|
16
|
+
providerOptions: IOpenAIProviderOptions;
|
|
17
|
+
payloadLogger?: IPayloadLogger;
|
|
18
|
+
responseParser: OpenAIResponseParser;
|
|
19
|
+
onTextDelta?: TTextDeltaCallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function chatWithOpenAIChatCompletions(
|
|
23
|
+
input: IOpenAIChatCompletionsOptions,
|
|
24
|
+
): Promise<TUniversalMessage> {
|
|
25
|
+
const client = requireClient(input.client);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const requestParams = buildChatRequestParams(input);
|
|
29
|
+
const textDeltaCb = input.chatOptions?.onTextDelta ?? input.onTextDelta;
|
|
30
|
+
if (textDeltaCb) {
|
|
31
|
+
return await chatWithStreamingAssembly(client, input, {
|
|
32
|
+
...requestParams,
|
|
33
|
+
stream: true,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await logPayload(input, requestParams, 'chat');
|
|
38
|
+
input.chatOptions?.onProviderNativeRawPayload?.({
|
|
39
|
+
provider: 'openai',
|
|
40
|
+
apiSurface: 'chat-completions',
|
|
41
|
+
payloadKind: 'request',
|
|
42
|
+
payload: requestParams,
|
|
43
|
+
});
|
|
44
|
+
const response = await client.chat.completions.create(requestParams);
|
|
45
|
+
input.chatOptions?.onProviderNativeRawPayload?.({
|
|
46
|
+
provider: 'openai',
|
|
47
|
+
apiSurface: 'chat-completions',
|
|
48
|
+
payloadKind: 'response',
|
|
49
|
+
payload: response,
|
|
50
|
+
});
|
|
51
|
+
return input.responseParser.parseResponse(response);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const openaiError = error as IOpenAIError;
|
|
54
|
+
const errorMessage = openaiError.message || 'OpenAI API request failed';
|
|
55
|
+
throw new Error(`OpenAI chat failed: ${errorMessage}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function* chatStreamWithOpenAIChatCompletions(
|
|
60
|
+
input: IOpenAIChatCompletionsOptions,
|
|
61
|
+
): AsyncIterable<TUniversalMessage> {
|
|
62
|
+
const client = requireClient(input.client);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const requestParams: OpenAI.Chat.ChatCompletionCreateParamsStreaming = {
|
|
66
|
+
...buildChatRequestParams(input),
|
|
67
|
+
stream: true,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
await logPayload(input, requestParams, 'stream');
|
|
71
|
+
input.chatOptions?.onProviderNativeRawPayload?.({
|
|
72
|
+
provider: 'openai',
|
|
73
|
+
apiSurface: 'chat-completions',
|
|
74
|
+
payloadKind: 'request',
|
|
75
|
+
payload: requestParams,
|
|
76
|
+
});
|
|
77
|
+
const stream = await client.chat.completions.create(
|
|
78
|
+
requestParams,
|
|
79
|
+
input.chatOptions?.signal ? { signal: input.chatOptions.signal } : undefined,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
for await (const chunk of observeProviderNativeRawPayloadStream(stream, {
|
|
83
|
+
provider: 'openai',
|
|
84
|
+
apiSurface: 'chat-completions',
|
|
85
|
+
onProviderNativeRawPayload: input.chatOptions?.onProviderNativeRawPayload,
|
|
86
|
+
})) {
|
|
87
|
+
const universalMessage = input.responseParser.parseStreamingChunk(chunk);
|
|
88
|
+
if (universalMessage) {
|
|
89
|
+
yield universalMessage;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const openaiError = error as IOpenAIError;
|
|
94
|
+
const errorMessage = openaiError.message || 'OpenAI API request failed';
|
|
95
|
+
throw new Error(`OpenAI stream failed: ${errorMessage}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildChatRequestParams(
|
|
100
|
+
input: IOpenAIChatCompletionsOptions,
|
|
101
|
+
): OpenAI.Chat.ChatCompletionCreateParamsNonStreaming {
|
|
102
|
+
const openaiMessages = convertToOpenAIMessages(input.messages);
|
|
103
|
+
const model = input.chatOptions?.model ?? input.providerOptions.defaultModel;
|
|
104
|
+
if (!model) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
'Model is required in chat options. Please specify a model in defaultModel configuration.',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const responseFormat = buildOpenAIChatResponseFormat(input.providerOptions);
|
|
111
|
+
return {
|
|
112
|
+
model,
|
|
113
|
+
messages: openaiMessages,
|
|
114
|
+
...(input.chatOptions?.temperature !== undefined && {
|
|
115
|
+
temperature: input.chatOptions.temperature,
|
|
116
|
+
}),
|
|
117
|
+
...(input.chatOptions?.maxTokens !== undefined && { max_tokens: input.chatOptions.maxTokens }),
|
|
118
|
+
...(input.chatOptions?.tools && {
|
|
119
|
+
tools: convertToOpenAITools(input.chatOptions.tools),
|
|
120
|
+
tool_choice: 'auto',
|
|
121
|
+
}),
|
|
122
|
+
...(responseFormat !== undefined && { response_format: responseFormat }),
|
|
123
|
+
} as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function chatWithStreamingAssembly(
|
|
127
|
+
client: OpenAI,
|
|
128
|
+
input: IOpenAIChatCompletionsOptions,
|
|
129
|
+
requestParams: OpenAI.Chat.ChatCompletionCreateParamsStreaming,
|
|
130
|
+
): Promise<TUniversalMessage> {
|
|
131
|
+
try {
|
|
132
|
+
await logPayload(input, requestParams, 'stream');
|
|
133
|
+
input.chatOptions?.onProviderNativeRawPayload?.({
|
|
134
|
+
provider: 'openai',
|
|
135
|
+
apiSurface: 'chat-completions',
|
|
136
|
+
payloadKind: 'request',
|
|
137
|
+
payload: requestParams,
|
|
138
|
+
});
|
|
139
|
+
const stream = await client.chat.completions.create(
|
|
140
|
+
requestParams,
|
|
141
|
+
input.chatOptions?.signal ? { signal: input.chatOptions.signal } : undefined,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return assembleOpenAIStream({
|
|
145
|
+
stream: observeProviderNativeRawPayloadStream(stream, {
|
|
146
|
+
provider: 'openai',
|
|
147
|
+
apiSurface: 'chat-completions',
|
|
148
|
+
onProviderNativeRawPayload: input.chatOptions?.onProviderNativeRawPayload,
|
|
149
|
+
}),
|
|
150
|
+
onTextDelta: input.chatOptions?.onTextDelta ?? input.onTextDelta,
|
|
151
|
+
signal: input.chatOptions?.signal,
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const openaiError = error as IOpenAIError;
|
|
155
|
+
const errorMessage = openaiError.message || 'OpenAI streaming request failed';
|
|
156
|
+
throw new Error(`OpenAI stream failed: ${errorMessage}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function logPayload(
|
|
161
|
+
input: IOpenAIChatCompletionsOptions,
|
|
162
|
+
requestParams:
|
|
163
|
+
| OpenAI.Chat.ChatCompletionCreateParamsNonStreaming
|
|
164
|
+
| OpenAI.Chat.ChatCompletionCreateParamsStreaming,
|
|
165
|
+
type: 'chat' | 'stream',
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
if (!input.payloadLogger?.isEnabled()) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const logData: IOpenAILogData = {
|
|
172
|
+
model: requestParams.model,
|
|
173
|
+
messagesCount: requestParams.messages.length,
|
|
174
|
+
hasTools: !!requestParams.tools,
|
|
175
|
+
temperature: requestParams.temperature ?? undefined,
|
|
176
|
+
maxTokens: requestParams.max_tokens ?? undefined,
|
|
177
|
+
timestamp: new Date().toISOString(),
|
|
178
|
+
};
|
|
179
|
+
await input.payloadLogger.logPayload(logData, type);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function requireClient(client: OpenAI | undefined): OpenAI {
|
|
183
|
+
if (!client) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
'OpenAI client not available. Either provide a client/apiKey or use an executor.',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return client;
|
|
189
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import { OpenAIProvider } from './provider';
|
|
3
|
+
import { LocalExecutor } from '@robota-sdk/agent-core';
|
|
4
|
+
import type { TUniversalMessage } from '@robota-sdk/agent-core';
|
|
5
|
+
|
|
6
|
+
describe('OpenAI Provider Executor Integration', () => {
|
|
7
|
+
let localExecutor: LocalExecutor;
|
|
8
|
+
let provider: OpenAIProvider;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
localExecutor = new LocalExecutor();
|
|
12
|
+
|
|
13
|
+
// Create a mock provider to register with the executor
|
|
14
|
+
const mockProvider = {
|
|
15
|
+
name: 'openai',
|
|
16
|
+
async chat(messages: TUniversalMessage[]): Promise<TUniversalMessage> {
|
|
17
|
+
return {
|
|
18
|
+
id: 'msg-1',
|
|
19
|
+
state: 'complete' as const,
|
|
20
|
+
role: 'assistant' as const,
|
|
21
|
+
content: `Mock response: ${messages[messages.length - 1]?.content}`,
|
|
22
|
+
timestamp: new Date(),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
async *chatStream(messages: TUniversalMessage[]): AsyncIterable<TUniversalMessage> {
|
|
26
|
+
const chunks = ['Mock', ' streaming', ' response'];
|
|
27
|
+
for (const chunk of chunks) {
|
|
28
|
+
yield {
|
|
29
|
+
id: 'msg-1',
|
|
30
|
+
state: 'complete' as const,
|
|
31
|
+
role: 'assistant' as const,
|
|
32
|
+
content: chunk,
|
|
33
|
+
timestamp: new Date(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
supportsTools: () => true,
|
|
38
|
+
validateConfig: () => true,
|
|
39
|
+
dispose: async () => {},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
localExecutor.registerProvider('openai', mockProvider);
|
|
43
|
+
|
|
44
|
+
// Create provider with executor (no API key needed)
|
|
45
|
+
provider = new OpenAIProvider({
|
|
46
|
+
executor: localExecutor,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(async () => {
|
|
51
|
+
await provider.dispose();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('Provider with Executor', () => {
|
|
55
|
+
it('should use executor for chat requests', async () => {
|
|
56
|
+
const messages: TUniversalMessage[] = [
|
|
57
|
+
{
|
|
58
|
+
id: 'msg-1',
|
|
59
|
+
state: 'complete' as const,
|
|
60
|
+
role: 'user',
|
|
61
|
+
content: 'Hello!',
|
|
62
|
+
timestamp: new Date(),
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const options = {
|
|
67
|
+
model: 'gpt-4',
|
|
68
|
+
temperature: 0.7,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const response = await provider.chat(messages, options);
|
|
72
|
+
|
|
73
|
+
expect(response.role).toBe('assistant');
|
|
74
|
+
expect(response.content).toContain('Mock response: Hello!');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should use executor for streaming chat requests', async () => {
|
|
78
|
+
const messages: TUniversalMessage[] = [
|
|
79
|
+
{
|
|
80
|
+
id: 'msg-1',
|
|
81
|
+
state: 'complete' as const,
|
|
82
|
+
role: 'user',
|
|
83
|
+
content: 'Tell me a story',
|
|
84
|
+
timestamp: new Date(),
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const options = {
|
|
89
|
+
model: 'gpt-4',
|
|
90
|
+
temperature: 0.7,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const chunks: string[] = [];
|
|
94
|
+
for await (const chunk of provider.chatStream(messages, options)) {
|
|
95
|
+
if (chunk.content) {
|
|
96
|
+
chunks.push(chunk.content);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
expect(chunks).toEqual(['Mock', ' streaming', ' response']);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should handle executor errors gracefully', async () => {
|
|
104
|
+
// Create provider with executor that doesn't have registered provider
|
|
105
|
+
const errorExecutor = new LocalExecutor();
|
|
106
|
+
const errorProvider = new OpenAIProvider({
|
|
107
|
+
executor: errorExecutor,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const messages: TUniversalMessage[] = [
|
|
111
|
+
{
|
|
112
|
+
id: 'msg-1',
|
|
113
|
+
state: 'complete' as const,
|
|
114
|
+
role: 'user',
|
|
115
|
+
content: 'Hello!',
|
|
116
|
+
timestamp: new Date(),
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
await expect(errorProvider.chat(messages, { model: 'gpt-4' })).rejects.toThrow(
|
|
121
|
+
'Provider "openai" not registered with LocalExecutor',
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
await errorProvider.dispose();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should support tools when executor supports tools', () => {
|
|
128
|
+
expect(provider.supportsTools()).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should validate configuration with executor', () => {
|
|
132
|
+
// Provider with executor should validate successfully
|
|
133
|
+
expect(provider.supportsTools()).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should clean up executor when provider is disposed', async () => {
|
|
137
|
+
// Provider calls dispose on its executor when disposed
|
|
138
|
+
await provider.dispose();
|
|
139
|
+
|
|
140
|
+
// Check that provider was disposed successfully
|
|
141
|
+
expect(true).toBe(true); // Basic check that dispose completed
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('Provider without Executor', () => {
|
|
146
|
+
it('should require either client, apiKey, or executor', () => {
|
|
147
|
+
expect(() => {
|
|
148
|
+
new OpenAIProvider({});
|
|
149
|
+
}).toThrow('Either OpenAI client, apiKey, or executor is required');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should work with API key (traditional mode)', () => {
|
|
153
|
+
const apiKeyProvider = new OpenAIProvider({
|
|
154
|
+
apiKey: 'sk-test-key',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(apiKeyProvider.name).toBe('openai');
|
|
158
|
+
expect(apiKeyProvider.version).toBe('1.0.0');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should work with client (traditional mode)', () => {
|
|
162
|
+
const mockClient = {} as any; // Mock OpenAI client
|
|
163
|
+
const clientProvider = new OpenAIProvider({
|
|
164
|
+
client: mockClient,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(clientProvider.name).toBe('openai');
|
|
168
|
+
expect(clientProvider.version).toBe('1.0.0');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('Mixed Mode Validation', () => {
|
|
173
|
+
it('should prioritize executor over direct API when both are provided', async () => {
|
|
174
|
+
const mixedProvider = new OpenAIProvider({
|
|
175
|
+
apiKey: 'sk-test-key',
|
|
176
|
+
executor: localExecutor,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Should use executor, not direct API
|
|
180
|
+
const messages: TUniversalMessage[] = [
|
|
181
|
+
{
|
|
182
|
+
id: 'msg-1',
|
|
183
|
+
state: 'complete' as const,
|
|
184
|
+
role: 'user',
|
|
185
|
+
content: 'Hello!',
|
|
186
|
+
timestamp: new Date(),
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const response = await mixedProvider.chat(messages, { model: 'gpt-4' });
|
|
191
|
+
expect(response.content).toContain('Mock response:');
|
|
192
|
+
|
|
193
|
+
await mixedProvider.dispose();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should handle executor initialization properly', () => {
|
|
197
|
+
const provider = new OpenAIProvider({
|
|
198
|
+
executor: localExecutor,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Check that executor was set correctly
|
|
202
|
+
expect((provider as any).executor).toBe(localExecutor);
|
|
203
|
+
expect((provider as any).client).toBeUndefined();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @robota-sdk/agent-provider (openai)
|
|
3
|
+
*
|
|
4
|
+
* Provides Provider implementation for using OpenAI API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Export main provider (new architecture)
|
|
8
|
+
export { OpenAIProvider } from './provider';
|
|
9
|
+
|
|
10
|
+
// Export types and utilities
|
|
11
|
+
export * from './types';
|
|
12
|
+
export * from './adapter';
|
|
13
|
+
export * from './model-catalog-refresh';
|
|
14
|
+
export * from './provider-definition';
|
|
15
|
+
|
|
16
|
+
// Export payload logging interfaces only (implementations are in separate subpaths)
|
|
17
|
+
export type { IPayloadLogger, IPayloadLoggerOptions } from './interfaces/payload-logger';
|
|
18
|
+
|
|
19
|
+
// Export modular components (optional - for advanced users)
|
|
20
|
+
// export { OpenAIStreamHandler } from './streaming/stream-handler';
|
|
21
|
+
// export { OpenAIResponseParser } from './parsers/response-parser';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { IOpenAILogData } from '../types/api-types';
|
|
2
|
+
import type { ILogger } from '@robota-sdk/agent-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* IPayloadLogger interface for logging OpenAI API payloads
|
|
6
|
+
*
|
|
7
|
+
* This interface provides a contract for different logging implementations:
|
|
8
|
+
* - FilePayloadLogger: Node.js file-based logging
|
|
9
|
+
* - ConsolePayloadLogger: Browser console-based logging
|
|
10
|
+
* - Custom implementations: User-defined loggers
|
|
11
|
+
*/
|
|
12
|
+
export interface IPayloadLogger {
|
|
13
|
+
/**
|
|
14
|
+
* Check if logging is enabled
|
|
15
|
+
* @returns true if logging is active, false otherwise
|
|
16
|
+
*/
|
|
17
|
+
isEnabled(): boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Log API payload data
|
|
21
|
+
* @param payload - The API request/response payload data
|
|
22
|
+
* @param type - Type of operation ('chat' or 'stream')
|
|
23
|
+
*/
|
|
24
|
+
logPayload(payload: IOpenAILogData, type: 'chat' | 'stream'): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Configuration options for payload loggers
|
|
29
|
+
*/
|
|
30
|
+
export interface IPayloadLoggerOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Whether logging is enabled
|
|
33
|
+
* @defaultValue true
|
|
34
|
+
*/
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Include timestamp in log entries
|
|
39
|
+
* @defaultValue true
|
|
40
|
+
*/
|
|
41
|
+
includeTimestamp?: boolean;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Logger instance for console output
|
|
45
|
+
* @defaultValue SilentLogger
|
|
46
|
+
*/
|
|
47
|
+
logger?: ILogger;
|
|
48
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { ConsolePayloadLogger } from './console-payload-logger';
|
|
3
|
+
import type { ILogger } from '@robota-sdk/agent-core';
|
|
4
|
+
import type { IOpenAILogData } from '../types/api-types';
|
|
5
|
+
|
|
6
|
+
function createMockLogger(): ILogger & {
|
|
7
|
+
group: ReturnType<typeof vi.fn>;
|
|
8
|
+
groupEnd: ReturnType<typeof vi.fn>;
|
|
9
|
+
} {
|
|
10
|
+
return {
|
|
11
|
+
info: vi.fn(),
|
|
12
|
+
warn: vi.fn(),
|
|
13
|
+
error: vi.fn(),
|
|
14
|
+
debug: vi.fn(),
|
|
15
|
+
log: vi.fn(),
|
|
16
|
+
group: vi.fn(),
|
|
17
|
+
groupEnd: vi.fn(),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createSamplePayload(): IOpenAILogData {
|
|
22
|
+
return {
|
|
23
|
+
model: 'gpt-4',
|
|
24
|
+
messagesCount: 3,
|
|
25
|
+
hasTools: true,
|
|
26
|
+
temperature: 0.7,
|
|
27
|
+
maxTokens: 1000,
|
|
28
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('ConsolePayloadLogger', () => {
|
|
33
|
+
describe('constructor', () => {
|
|
34
|
+
it('should default to enabled', () => {
|
|
35
|
+
const logger = new ConsolePayloadLogger();
|
|
36
|
+
expect(logger.isEnabled()).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should respect enabled option set to false', () => {
|
|
40
|
+
const logger = new ConsolePayloadLogger({ enabled: false });
|
|
41
|
+
expect(logger.isEnabled()).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should accept custom logger', () => {
|
|
45
|
+
const mockLogger = createMockLogger();
|
|
46
|
+
const logger = new ConsolePayloadLogger({ logger: mockLogger });
|
|
47
|
+
expect(logger.isEnabled()).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('isEnabled', () => {
|
|
52
|
+
it('should return true when enabled', () => {
|
|
53
|
+
const logger = new ConsolePayloadLogger({ enabled: true });
|
|
54
|
+
expect(logger.isEnabled()).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return false when disabled', () => {
|
|
58
|
+
const logger = new ConsolePayloadLogger({ enabled: false });
|
|
59
|
+
expect(logger.isEnabled()).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('logPayload', () => {
|
|
64
|
+
it('should log payload to console with chat type', async () => {
|
|
65
|
+
const mockLogger = createMockLogger();
|
|
66
|
+
const logger = new ConsolePayloadLogger({ logger: mockLogger });
|
|
67
|
+
const payload = createSamplePayload();
|
|
68
|
+
|
|
69
|
+
await logger.logPayload(payload, 'chat');
|
|
70
|
+
|
|
71
|
+
expect(mockLogger.group).toHaveBeenCalledWith(expect.stringContaining('[OpenAI CHAT]'));
|
|
72
|
+
expect(mockLogger.info).toHaveBeenCalled();
|
|
73
|
+
expect(mockLogger.debug).toHaveBeenCalled();
|
|
74
|
+
expect(mockLogger.groupEnd).toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should log payload with stream type', async () => {
|
|
78
|
+
const mockLogger = createMockLogger();
|
|
79
|
+
const logger = new ConsolePayloadLogger({ logger: mockLogger });
|
|
80
|
+
const payload = createSamplePayload();
|
|
81
|
+
|
|
82
|
+
await logger.logPayload(payload, 'stream');
|
|
83
|
+
|
|
84
|
+
expect(mockLogger.group).toHaveBeenCalledWith(expect.stringContaining('[OpenAI STREAM]'));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should include timestamp in group title when includeTimestamp is true', async () => {
|
|
88
|
+
const mockLogger = createMockLogger();
|
|
89
|
+
const logger = new ConsolePayloadLogger({
|
|
90
|
+
logger: mockLogger,
|
|
91
|
+
includeTimestamp: true,
|
|
92
|
+
});
|
|
93
|
+
const payload = createSamplePayload();
|
|
94
|
+
|
|
95
|
+
await logger.logPayload(payload, 'chat');
|
|
96
|
+
|
|
97
|
+
expect(mockLogger.group).toHaveBeenCalledWith(expect.stringContaining(payload.timestamp));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should not include timestamp when includeTimestamp is false', async () => {
|
|
101
|
+
const mockLogger = createMockLogger();
|
|
102
|
+
const logger = new ConsolePayloadLogger({
|
|
103
|
+
logger: mockLogger,
|
|
104
|
+
includeTimestamp: false,
|
|
105
|
+
});
|
|
106
|
+
const payload = createSamplePayload();
|
|
107
|
+
|
|
108
|
+
await logger.logPayload(payload, 'chat');
|
|
109
|
+
|
|
110
|
+
const groupCall = mockLogger.group.mock.calls[0][0] as string;
|
|
111
|
+
expect(groupCall).not.toContain(payload.timestamp);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should skip logging when disabled', async () => {
|
|
115
|
+
const mockLogger = createMockLogger();
|
|
116
|
+
const logger = new ConsolePayloadLogger({
|
|
117
|
+
logger: mockLogger,
|
|
118
|
+
enabled: false,
|
|
119
|
+
});
|
|
120
|
+
const payload = createSamplePayload();
|
|
121
|
+
|
|
122
|
+
await logger.logPayload(payload, 'chat');
|
|
123
|
+
|
|
124
|
+
expect(mockLogger.group).not.toHaveBeenCalled();
|
|
125
|
+
expect(mockLogger.info).not.toHaveBeenCalled();
|
|
126
|
+
expect(mockLogger.debug).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should not throw on internal error and log the error', async () => {
|
|
130
|
+
const mockLogger = createMockLogger();
|
|
131
|
+
// Make group throw an error
|
|
132
|
+
mockLogger.group.mockImplementation(() => {
|
|
133
|
+
throw new Error('Console error');
|
|
134
|
+
});
|
|
135
|
+
const logger = new ConsolePayloadLogger({ logger: mockLogger });
|
|
136
|
+
const payload = createSamplePayload();
|
|
137
|
+
|
|
138
|
+
// Should not throw
|
|
139
|
+
await expect(logger.logPayload(payload, 'chat')).resolves.toBeUndefined();
|
|
140
|
+
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
141
|
+
expect.stringContaining('[ConsolePayloadLogger]'),
|
|
142
|
+
expect.any(String),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should log request details with payload fields', async () => {
|
|
147
|
+
const mockLogger = createMockLogger();
|
|
148
|
+
const logger = new ConsolePayloadLogger({ logger: mockLogger });
|
|
149
|
+
const payload = createSamplePayload();
|
|
150
|
+
|
|
151
|
+
await logger.logPayload(payload, 'chat');
|
|
152
|
+
|
|
153
|
+
expect(mockLogger.info).toHaveBeenCalledWith(
|
|
154
|
+
expect.any(String),
|
|
155
|
+
expect.objectContaining({
|
|
156
|
+
model: 'gpt-4',
|
|
157
|
+
messagesCount: 3,
|
|
158
|
+
hasTools: true,
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should default type to chat', async () => {
|
|
164
|
+
const mockLogger = createMockLogger();
|
|
165
|
+
const logger = new ConsolePayloadLogger({ logger: mockLogger });
|
|
166
|
+
const payload = createSamplePayload();
|
|
167
|
+
|
|
168
|
+
await logger.logPayload(payload, 'chat');
|
|
169
|
+
|
|
170
|
+
expect(mockLogger.group).toHaveBeenCalledWith(expect.stringContaining('CHAT'));
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|