@robota-sdk/agent-provider-openai 3.0.0-beta.63 → 3.0.0-beta.82
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 +661 -21
- package/dist/node/index.cjs +1 -2
- package/dist/node/index.d.cts +271 -267
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +271 -267
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2 -2
- package/dist/node/index.js.map +1 -0
- package/dist/node/loggers/index.cjs +1 -0
- package/dist/node/loggers/index.d.cts +94 -0
- package/dist/node/loggers/index.d.cts.map +1 -0
- package/dist/node/loggers/index.d.ts +94 -0
- package/dist/node/loggers/index.d.ts.map +1 -0
- package/dist/node/loggers/index.js +2 -0
- package/dist/node/loggers/index.js.map +1 -0
- package/dist/node/payload-logger-BaW0K8yI.d.cts +61 -0
- package/dist/node/payload-logger-BaW0K8yI.d.cts.map +1 -0
- package/dist/node/payload-logger-BaW0K8yI.d.ts +61 -0
- package/dist/node/payload-logger-BaW0K8yI.d.ts.map +1 -0
- package/dist/node/rolldown-runtime-CMqjfN_6.cjs +1 -0
- package/package.json +69 -86
- package/src/index.ts +1 -0
- package/src/openai/__tests__/endpoint-provenance.test.ts +44 -0
- package/src/openai/__tests__/provider-errors.test.ts +114 -0
- package/src/openai/__tests__/request-format.test.ts +59 -0
- package/src/openai/__tests__/strict-tools-closure.test.ts +136 -0
- package/src/openai/__tests__/tool-schema-projection.test.ts +302 -0
- package/src/openai/__tests__/trace-context-wire.test.ts +114 -0
- package/src/openai/adapter.test.ts +494 -0
- package/src/openai/adapter.ts +145 -0
- package/src/openai/chat-completions-chat.ts +225 -0
- package/src/openai/executor-integration.test.ts +213 -0
- package/src/openai/index.ts +19 -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 +96 -0
- package/src/openai/loggers/console.ts +9 -0
- package/src/openai/loggers/file-payload-logger.test.ts +243 -0
- package/src/openai/loggers/file-payload-logger.ts +124 -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 +23 -0
- package/src/openai/model-effort-table.ts +19 -0
- package/src/openai/model-effort-verification-config.test.ts +45 -0
- package/src/openai/model-effort-verification-config.ts +35 -0
- package/src/openai/openai-request-format.ts +116 -0
- package/src/openai/parsers/response-parser.test.ts +407 -0
- package/src/openai/parsers/response-parser.ts +48 -0
- package/src/openai/provider-definition.test.ts +74 -0
- package/src/openai/provider-definition.ts +139 -0
- package/src/openai/provider.test.ts +1686 -0
- package/src/openai/provider.ts +392 -0
- package/src/openai/reasoning-effort.test.ts +272 -0
- package/src/openai/reasoning-effort.ts +49 -0
- package/src/openai/request-id.ts +39 -0
- package/src/openai/request-options.ts +14 -0
- package/src/openai/responses-chat.ts +285 -0
- package/src/openai/responses-converter.ts +122 -0
- package/src/openai/responses-parser.ts +297 -0
- package/src/openai/responses-stream-utils.ts +45 -0
- package/src/openai/responses-types.ts +193 -0
- package/src/openai/streaming/stream-assembler.ts +3 -0
- package/src/openai/types/api-types.ts +113 -0
- package/src/openai/types.ts +235 -0
- package/CHANGELOG.md +0 -614
- package/README.md +0 -467
- package/dist/browser/index.d.ts +0 -307
- package/dist/browser/index.js +0 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { IOpenAIResponsesReasoningOptions } from './types';
|
|
2
|
+
import type { IModelEffortResolution, TModelEffort } from '@robota-sdk/agent-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Map the framework's per-call reasoning-effort dial onto the OpenAI Responses API
|
|
6
|
+
* `reasoning.effort` parameter.
|
|
7
|
+
*
|
|
8
|
+
* Per-model support and downward clamping are resolved by the Core table before this boundary.
|
|
9
|
+
* This function only serializes the verified concrete value, without reinterpreting it.
|
|
10
|
+
*/
|
|
11
|
+
export function mapEffortToOpenAIReasoningEffort(
|
|
12
|
+
effort: TModelEffort,
|
|
13
|
+
): IOpenAIResponsesReasoningOptions['effort'] {
|
|
14
|
+
return effort;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Merge the per-call effort dial into the provider's static reasoning options.
|
|
19
|
+
*
|
|
20
|
+
* A verified concrete resolution takes precedence over static native control; a conflicting static
|
|
21
|
+
* control is rejected rather than silently overwritten. `auto` and `not-applied` resolutions leave
|
|
22
|
+
* the provider default untouched and never manufacture a native effort field.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveOpenAIReasoningOptions(
|
|
25
|
+
staticReasoning: IOpenAIResponsesReasoningOptions | undefined,
|
|
26
|
+
resolution: IModelEffortResolution | undefined,
|
|
27
|
+
): IOpenAIResponsesReasoningOptions | undefined {
|
|
28
|
+
if (resolution === undefined) {
|
|
29
|
+
return staticReasoning;
|
|
30
|
+
}
|
|
31
|
+
if (resolution.effective === null || resolution.disposition === 'model-default') {
|
|
32
|
+
if (staticReasoning?.effort !== undefined) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Static reasoning.effort ${staticReasoning.effort} conflicts with ${resolution.disposition === 'model-default' ? 'provider-default selection' : 'an effort resolution that does not apply a native control'}.`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return staticReasoning;
|
|
38
|
+
}
|
|
39
|
+
const effort = mapEffortToOpenAIReasoningEffort(resolution.effective);
|
|
40
|
+
if (staticReasoning?.effort !== undefined && staticReasoning.effort !== effort) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Resolved effort ${effort} conflicts with static reasoning.effort ${staticReasoning.effort}.`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
...staticReasoning,
|
|
47
|
+
effort,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TUniversalMessage } from '@robota-sdk/agent-core';
|
|
2
|
+
|
|
3
|
+
/** The subset of the OpenAI SDK's `APIPromise` this module depends on. */
|
|
4
|
+
interface IWithResponseCapable<T> {
|
|
5
|
+
withResponse?: () => Promise<{ data: T; request_id?: string | null }>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Await an OpenAI `create()` call, reading the server's own request ID off `.withResponse()` when
|
|
10
|
+
* the client exposes it. A test double or older client that resolves to a plain value has no
|
|
11
|
+
* `withResponse`, so this falls back to the awaited value with no ID — never a fabricated one.
|
|
12
|
+
*/
|
|
13
|
+
export async function awaitWithProviderRequestId<T>(
|
|
14
|
+
call: PromiseLike<T> & IWithResponseCapable<T>,
|
|
15
|
+
): Promise<{ data: T; providerRequestId?: string }> {
|
|
16
|
+
if (typeof call.withResponse === 'function') {
|
|
17
|
+
const { data, request_id } = await call.withResponse();
|
|
18
|
+
return {
|
|
19
|
+
data,
|
|
20
|
+
...(typeof request_id === 'string' && request_id.length > 0 && { providerRequestId: request_id }),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return { data: await call };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The non-stream response shape the SDK attaches its own request ID to directly. */
|
|
27
|
+
export function readOpenAIRequestId(response: unknown): string | undefined {
|
|
28
|
+
const id = (response as { _request_id?: string | null } | null | undefined)?._request_id;
|
|
29
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Merge a provider-returned request ID into a message's metadata; a no-op when there isn't one. */
|
|
33
|
+
export function withProviderRequestId<T extends TUniversalMessage>(
|
|
34
|
+
message: T,
|
|
35
|
+
providerRequestId: string | undefined,
|
|
36
|
+
): T {
|
|
37
|
+
if (providerRequestId === undefined) return message;
|
|
38
|
+
return { ...message, metadata: { ...message.metadata, providerRequestId } };
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type OpenAI from 'openai';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-request SDK options, or none at all when there is neither a signal nor a header to send, so
|
|
5
|
+
* a call without either keeps the exact shape it always had.
|
|
6
|
+
*/
|
|
7
|
+
export function openAIRequestOptions(
|
|
8
|
+
signal: AbortSignal | undefined,
|
|
9
|
+
headers: Readonly<Record<string, string>> | undefined,
|
|
10
|
+
): OpenAI.RequestOptions | undefined {
|
|
11
|
+
const hasHeaders = headers !== undefined && Object.keys(headers).length > 0;
|
|
12
|
+
if (!signal && !hasHeaders) return undefined;
|
|
13
|
+
return { ...(signal ? { signal } : {}), ...(hasHeaders ? { headers: { ...headers } } : {}) };
|
|
14
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { toProviderError } from '@robota-sdk/agent-core';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
observeProviderNativeRawPayloadStream,
|
|
7
|
+
toOpenAIResponsesToolChoice,
|
|
8
|
+
} from '@robota-sdk/agent-provider-openai-compatible/shared';
|
|
9
|
+
|
|
10
|
+
import { buildOpenAIResponsesTextConfig } from './openai-request-format';
|
|
11
|
+
import { resolveOpenAIReasoningOptions } from './reasoning-effort';
|
|
12
|
+
import {
|
|
13
|
+
awaitWithProviderRequestId,
|
|
14
|
+
readOpenAIRequestId,
|
|
15
|
+
withProviderRequestId,
|
|
16
|
+
} from './request-id';
|
|
17
|
+
import {
|
|
18
|
+
convertToOpenAIResponsesInput,
|
|
19
|
+
convertToOpenAIResponsesTools,
|
|
20
|
+
} from './responses-converter';
|
|
21
|
+
import { openAIRequestOptions } from './request-options';
|
|
22
|
+
import { assembleOpenAIResponsesStream, parseOpenAIResponsesResponse } from './responses-parser';
|
|
23
|
+
|
|
24
|
+
import type {
|
|
25
|
+
IOpenAIResponsesRequestNonStreaming,
|
|
26
|
+
IOpenAIResponsesRequestStreaming,
|
|
27
|
+
TOpenAIResponsesStreamEvent,
|
|
28
|
+
} from './responses-types';
|
|
29
|
+
import type { IOpenAIProviderOptions } from './types';
|
|
30
|
+
import type { IChatOptions, TTextDeltaCallback, TUniversalMessage } from '@robota-sdk/agent-core';
|
|
31
|
+
import type OpenAI from 'openai';
|
|
32
|
+
|
|
33
|
+
export interface IOpenAIResponsesChatOptions {
|
|
34
|
+
client?: OpenAI;
|
|
35
|
+
messages: TUniversalMessage[];
|
|
36
|
+
chatOptions?: IChatOptions;
|
|
37
|
+
providerOptions: IOpenAIProviderOptions;
|
|
38
|
+
onTextDelta?: TTextDeltaCallback;
|
|
39
|
+
/** Per-request headers (trusted `traceparent`), sent after the raw request payload is captured. */
|
|
40
|
+
requestHeaders?: Readonly<Record<string, string>>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface IResponsesStreamMessageQueue {
|
|
44
|
+
deltas: TUniversalMessage[];
|
|
45
|
+
finalMessage?: TUniversalMessage;
|
|
46
|
+
error?: Error;
|
|
47
|
+
wake?: () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function chatWithOpenAIResponsesApi(
|
|
51
|
+
input: IOpenAIResponsesChatOptions,
|
|
52
|
+
): Promise<TUniversalMessage> {
|
|
53
|
+
const textDeltaCb = input.chatOptions?.onTextDelta ?? input.onTextDelta;
|
|
54
|
+
if (textDeltaCb) {
|
|
55
|
+
return chatWithOpenAIResponsesStreamingAssembly({
|
|
56
|
+
...input,
|
|
57
|
+
chatOptions: {
|
|
58
|
+
...input.chatOptions,
|
|
59
|
+
onTextDelta: textDeltaCb,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!input.client) {
|
|
65
|
+
throw new Error('OpenAI Responses client not available.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const requestParams = buildResponsesRequestParams(input);
|
|
70
|
+
input.chatOptions?.onProviderNativeRawPayload?.({
|
|
71
|
+
provider: 'openai',
|
|
72
|
+
apiSurface: 'responses',
|
|
73
|
+
payloadKind: 'request',
|
|
74
|
+
payload: requestParams,
|
|
75
|
+
});
|
|
76
|
+
const response = await input.client.responses.create(
|
|
77
|
+
requestParams as OpenAI.Responses.ResponseCreateParamsNonStreaming,
|
|
78
|
+
openAIRequestOptions(input.chatOptions?.signal, input.requestHeaders),
|
|
79
|
+
);
|
|
80
|
+
input.chatOptions?.onProviderNativeRawPayload?.({
|
|
81
|
+
provider: 'openai',
|
|
82
|
+
apiSurface: 'responses',
|
|
83
|
+
payloadKind: 'response',
|
|
84
|
+
payload: response,
|
|
85
|
+
});
|
|
86
|
+
return withProviderRequestId(
|
|
87
|
+
parseOpenAIResponsesResponse(response),
|
|
88
|
+
readOpenAIRequestId(response),
|
|
89
|
+
);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw toProviderError(error, 'openai', 'OpenAI responses failed');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function* chatStreamWithOpenAIResponsesApi(
|
|
96
|
+
input: IOpenAIResponsesChatOptions,
|
|
97
|
+
): AsyncIterable<TUniversalMessage> {
|
|
98
|
+
const queue: IResponsesStreamMessageQueue = { deltas: [] };
|
|
99
|
+
const textDeltaCb = input.chatOptions?.onTextDelta ?? input.onTextDelta;
|
|
100
|
+
const assembly = chatWithOpenAIResponsesStreamingAssembly({
|
|
101
|
+
...input,
|
|
102
|
+
chatOptions: {
|
|
103
|
+
...input.chatOptions,
|
|
104
|
+
onTextDelta: (delta) => {
|
|
105
|
+
textDeltaCb?.(delta);
|
|
106
|
+
enqueueStreamDelta(queue, createStreamDeltaMessage(delta));
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
.then((result) => finishStreamQueue(queue, result))
|
|
111
|
+
.catch((error) => failStreamQueue(queue, toError(error)));
|
|
112
|
+
|
|
113
|
+
yield* drainResponsesStreamQueue(queue);
|
|
114
|
+
await assembly;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function chatWithOpenAIResponsesStreamingAssembly(
|
|
118
|
+
input: IOpenAIResponsesChatOptions,
|
|
119
|
+
): Promise<TUniversalMessage> {
|
|
120
|
+
if (!input.client) {
|
|
121
|
+
throw new Error('OpenAI Responses client not available.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const requestParams = buildResponsesStreamingRequestParams(input);
|
|
126
|
+
input.chatOptions?.onProviderNativeRawPayload?.({
|
|
127
|
+
provider: 'openai',
|
|
128
|
+
apiSurface: 'responses',
|
|
129
|
+
payloadKind: 'request',
|
|
130
|
+
payload: requestParams,
|
|
131
|
+
});
|
|
132
|
+
const { data: stream, providerRequestId } = await awaitWithProviderRequestId(
|
|
133
|
+
input.client.responses.create(
|
|
134
|
+
requestParams as OpenAI.Responses.ResponseCreateParamsStreaming,
|
|
135
|
+
openAIRequestOptions(input.chatOptions?.signal, input.requestHeaders),
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
const assembled = await assembleOpenAIResponsesStream({
|
|
139
|
+
stream: observeProviderNativeRawPayloadStream(
|
|
140
|
+
stream as AsyncIterable<TOpenAIResponsesStreamEvent>,
|
|
141
|
+
{
|
|
142
|
+
provider: 'openai',
|
|
143
|
+
apiSurface: 'responses',
|
|
144
|
+
onProviderNativeRawPayload: input.chatOptions?.onProviderNativeRawPayload,
|
|
145
|
+
},
|
|
146
|
+
),
|
|
147
|
+
onTextDelta: input.chatOptions?.onTextDelta,
|
|
148
|
+
signal: input.chatOptions?.signal,
|
|
149
|
+
});
|
|
150
|
+
return withProviderRequestId(assembled, providerRequestId);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw toProviderError(error, 'openai', 'OpenAI responses stream failed');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildResponsesRequestParams(
|
|
157
|
+
input: IOpenAIResponsesChatOptions,
|
|
158
|
+
): IOpenAIResponsesRequestNonStreaming {
|
|
159
|
+
const model = input.chatOptions?.model ?? input.providerOptions.defaultModel;
|
|
160
|
+
if (!model) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
'Model is required in chat options. Please specify a model in defaultModel configuration.',
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const tools = convertToOpenAIResponsesTools(
|
|
167
|
+
input.chatOptions?.tools,
|
|
168
|
+
input.providerOptions.strictTools,
|
|
169
|
+
);
|
|
170
|
+
const textConfig = buildOpenAIResponsesTextConfig(input.providerOptions);
|
|
171
|
+
// Native effort mapping: thread the per-call reasoning-effort dial onto the Responses
|
|
172
|
+
// API `reasoning.effort` parameter, merged with any static reasoning options.
|
|
173
|
+
const reasoning = resolveOpenAIReasoningOptions(
|
|
174
|
+
input.providerOptions.reasoning,
|
|
175
|
+
input.chatOptions?.effortResolution,
|
|
176
|
+
);
|
|
177
|
+
return {
|
|
178
|
+
model,
|
|
179
|
+
input: convertToOpenAIResponsesInput(input.messages),
|
|
180
|
+
...(tools !== undefined && {
|
|
181
|
+
tools,
|
|
182
|
+
tool_choice: toOpenAIResponsesToolChoice(input.chatOptions?.toolChoice),
|
|
183
|
+
}),
|
|
184
|
+
...(input.chatOptions?.temperature !== undefined && {
|
|
185
|
+
temperature: input.chatOptions.temperature,
|
|
186
|
+
}),
|
|
187
|
+
...(input.chatOptions?.maxTokens !== undefined && {
|
|
188
|
+
max_output_tokens: input.chatOptions.maxTokens,
|
|
189
|
+
}),
|
|
190
|
+
...(textConfig !== undefined && { text: textConfig }),
|
|
191
|
+
...(reasoning !== undefined && {
|
|
192
|
+
reasoning,
|
|
193
|
+
}),
|
|
194
|
+
...(input.providerOptions.includeEncryptedReasoning === true && {
|
|
195
|
+
include: ['reasoning.encrypted_content'],
|
|
196
|
+
}),
|
|
197
|
+
...(input.providerOptions.store !== undefined && { store: input.providerOptions.store }),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildResponsesStreamingRequestParams(
|
|
202
|
+
input: IOpenAIResponsesChatOptions,
|
|
203
|
+
): IOpenAIResponsesRequestStreaming {
|
|
204
|
+
return {
|
|
205
|
+
...buildResponsesRequestParams(input),
|
|
206
|
+
stream: true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function createStreamDeltaMessage(delta: string): TUniversalMessage {
|
|
211
|
+
return {
|
|
212
|
+
id: randomUUID(),
|
|
213
|
+
role: 'assistant',
|
|
214
|
+
content: delta,
|
|
215
|
+
state: 'complete',
|
|
216
|
+
timestamp: new Date(),
|
|
217
|
+
metadata: {
|
|
218
|
+
providerApiSurface: 'responses',
|
|
219
|
+
isStreamChunk: true,
|
|
220
|
+
isComplete: false,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function* drainResponsesStreamQueue(
|
|
226
|
+
queue: IResponsesStreamMessageQueue,
|
|
227
|
+
): AsyncIterable<TUniversalMessage> {
|
|
228
|
+
while (true) {
|
|
229
|
+
const next = queue.deltas.shift();
|
|
230
|
+
if (next !== undefined) {
|
|
231
|
+
yield next;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (queue.error !== undefined) {
|
|
235
|
+
throw queue.error;
|
|
236
|
+
}
|
|
237
|
+
if (queue.finalMessage !== undefined) {
|
|
238
|
+
yield createFinalStreamMessage(queue.finalMessage);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
await waitForStreamQueue(queue);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function enqueueStreamDelta(queue: IResponsesStreamMessageQueue, message: TUniversalMessage): void {
|
|
246
|
+
queue.deltas.push(message);
|
|
247
|
+
wakeStreamQueue(queue);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function finishStreamQueue(queue: IResponsesStreamMessageQueue, result: TUniversalMessage): void {
|
|
251
|
+
queue.finalMessage = result;
|
|
252
|
+
wakeStreamQueue(queue);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function failStreamQueue(queue: IResponsesStreamMessageQueue, error: Error): void {
|
|
256
|
+
queue.error = error;
|
|
257
|
+
wakeStreamQueue(queue);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function waitForStreamQueue(queue: IResponsesStreamMessageQueue): Promise<void> {
|
|
261
|
+
return new Promise((resolve) => {
|
|
262
|
+
queue.wake = resolve;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function wakeStreamQueue(queue: IResponsesStreamMessageQueue): void {
|
|
267
|
+
queue.wake?.();
|
|
268
|
+
queue.wake = undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function createFinalStreamMessage(result: TUniversalMessage): TUniversalMessage {
|
|
272
|
+
return {
|
|
273
|
+
...result,
|
|
274
|
+
content: '',
|
|
275
|
+
metadata: {
|
|
276
|
+
...result.metadata,
|
|
277
|
+
isStreamChunk: true,
|
|
278
|
+
isComplete: true,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function toError(error: Error | string): Error {
|
|
284
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
285
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IOpenAIResponsesFunctionTool,
|
|
3
|
+
IOpenAIResponsesMessageInput,
|
|
4
|
+
TOpenAIResponsesInputContent,
|
|
5
|
+
TOpenAIResponsesInputItem,
|
|
6
|
+
} from './responses-types';
|
|
7
|
+
import type {
|
|
8
|
+
IAssistantMessage,
|
|
9
|
+
IToolSchema,
|
|
10
|
+
TUniversalMessage,
|
|
11
|
+
TUniversalMessagePart,
|
|
12
|
+
} from '@robota-sdk/agent-core';
|
|
13
|
+
|
|
14
|
+
export function convertToOpenAIResponsesInput(
|
|
15
|
+
messages: TUniversalMessage[],
|
|
16
|
+
): TOpenAIResponsesInputItem[] {
|
|
17
|
+
return messages.flatMap((message) => convertMessage(message));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* MCP-005: `tools` arrives ALREADY projected — `AbstractAIProvider.projectTools()` ran the strict or
|
|
22
|
+
* permissive profile (`STRICT_TOOL_SCHEMA_PROFILE` / `PERMISSIVE_TOOL_SCHEMA_PROFILE`) over every
|
|
23
|
+
* schema before this converter ever sees it, closing objects for strict mode where PROV-007's
|
|
24
|
+
* `closeObjectSchemas` used to run inline here. This function does no schema transformation of its
|
|
25
|
+
* own — it only shapes the already-projected `parameters` into the wire tool definition and keeps
|
|
26
|
+
* `strict: true` on the emitted field when `strictTools` is on (a request-field concern, not a schema
|
|
27
|
+
* one: the schema was already closed by the projection step).
|
|
28
|
+
*/
|
|
29
|
+
export function convertToOpenAIResponsesTools(
|
|
30
|
+
tools: IToolSchema[] | undefined,
|
|
31
|
+
strictTools: boolean | undefined,
|
|
32
|
+
): IOpenAIResponsesFunctionTool[] | undefined {
|
|
33
|
+
const strict = strictTools ?? false;
|
|
34
|
+
const converted =
|
|
35
|
+
tools?.map((tool) => ({
|
|
36
|
+
type: 'function' as const,
|
|
37
|
+
name: tool.name,
|
|
38
|
+
description: tool.description,
|
|
39
|
+
parameters: tool.parameters,
|
|
40
|
+
strict,
|
|
41
|
+
})) ?? [];
|
|
42
|
+
return converted.length > 0 ? converted : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function convertMessage(message: TUniversalMessage): TOpenAIResponsesInputItem[] {
|
|
46
|
+
if (message.role === 'user') {
|
|
47
|
+
return [createMessageInput('user', getUserContent(message.content, message.parts))];
|
|
48
|
+
}
|
|
49
|
+
if (message.role === 'system') {
|
|
50
|
+
return [createMessageInput('system', message.content)];
|
|
51
|
+
}
|
|
52
|
+
if (message.role === 'tool') {
|
|
53
|
+
if (!message.toolCallId || message.toolCallId.trim().length === 0) {
|
|
54
|
+
throw new Error(`Tool message missing toolCallId: ${JSON.stringify(message)}`);
|
|
55
|
+
}
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
type: 'function_call_output',
|
|
59
|
+
call_id: message.toolCallId,
|
|
60
|
+
output: message.content || '',
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
return convertAssistantMessage(message);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function convertAssistantMessage(message: IAssistantMessage): TOpenAIResponsesInputItem[] {
|
|
68
|
+
const items: TOpenAIResponsesInputItem[] = [];
|
|
69
|
+
if (message.content && message.content.length > 0) {
|
|
70
|
+
items.push(createMessageInput('assistant', message.content));
|
|
71
|
+
}
|
|
72
|
+
for (const toolCall of message.toolCalls ?? []) {
|
|
73
|
+
items.push({
|
|
74
|
+
type: 'function_call',
|
|
75
|
+
call_id: toolCall.id,
|
|
76
|
+
name: toolCall.function.name,
|
|
77
|
+
arguments: toolCall.function.arguments,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (items.length === 0) {
|
|
81
|
+
items.push(createMessageInput('assistant', ''));
|
|
82
|
+
}
|
|
83
|
+
return items;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getUserContent(
|
|
87
|
+
content: string,
|
|
88
|
+
parts: TUniversalMessagePart[] | undefined,
|
|
89
|
+
): string | TOpenAIResponsesInputContent[] {
|
|
90
|
+
if (!parts || parts.length === 0) {
|
|
91
|
+
return content;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const converted = parts.map((part) => convertPart(part));
|
|
95
|
+
if (content.length > 0 && !parts.some((part) => part.type === 'text')) {
|
|
96
|
+
return [{ type: 'input_text', text: content }, ...converted];
|
|
97
|
+
}
|
|
98
|
+
return converted;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function convertPart(part: TUniversalMessagePart): TOpenAIResponsesInputContent {
|
|
102
|
+
if (part.type === 'text') {
|
|
103
|
+
return { type: 'input_text', text: part.text };
|
|
104
|
+
}
|
|
105
|
+
if (part.type === 'image_uri') {
|
|
106
|
+
return { type: 'input_image', image_url: part.uri };
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
type: 'input_image',
|
|
110
|
+
image_url: `data:${part.mimeType};base64,${part.data}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createMessageInput(
|
|
115
|
+
role: IOpenAIResponsesMessageInput['role'],
|
|
116
|
+
content: string | TOpenAIResponsesInputContent[],
|
|
117
|
+
): IOpenAIResponsesMessageInput {
|
|
118
|
+
return {
|
|
119
|
+
role,
|
|
120
|
+
content,
|
|
121
|
+
};
|
|
122
|
+
}
|