@robota-sdk/agent-provider-openai 3.0.0-beta.82 → 3.0.0-beta.84
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/CHANGELOG.md +231 -0
- package/README.md +135 -0
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.cts +8 -3
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +8 -3
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -1
- package/dist/node/index.js.map +1 -1
- package/dist/node/loggers/index.d.cts +6 -6
- package/dist/node/loggers/index.d.cts.map +1 -1
- package/dist/node/loggers/index.d.ts +6 -6
- package/dist/node/loggers/index.d.ts.map +1 -1
- package/dist/node/loggers/index.js.map +1 -1
- package/dist/node/{payload-logger-BaW0K8yI.d.ts → payload-logger-n89AQdUn.d.cts} +6 -4
- package/dist/node/payload-logger-n89AQdUn.d.cts.map +1 -0
- package/dist/node/{payload-logger-BaW0K8yI.d.cts → payload-logger-n89AQdUn.d.ts} +6 -4
- package/dist/node/payload-logger-n89AQdUn.d.ts.map +1 -0
- package/package.json +11 -5
- package/src/openai/__tests__/abort-signal-wire.test.ts +210 -0
- package/src/openai/__tests__/provider-errors.test.ts +34 -5
- package/src/openai/__tests__/strict-tools-closure.test.ts +49 -2
- package/src/openai/__tests__/tool-schema-projection.test.ts +16 -2
- package/src/openai/chat-completions-chat.ts +2 -2
- package/src/openai/interfaces/payload-logger.ts +5 -3
- package/src/openai/loggers/console-payload-logger.ts +2 -2
- package/src/openai/loggers/file-payload-logger.ts +6 -5
- package/src/openai/message-converter.ts +7 -3
- package/src/openai/responses-parser.test.ts +87 -0
- package/src/openai/responses-parser.ts +15 -9
- package/src/openai/responses-types.ts +2 -0
- package/src/openai/types.ts +7 -2
- package/dist/node/payload-logger-BaW0K8yI.d.cts.map +0 -1
- package/dist/node/payload-logger-BaW0K8yI.d.ts.map +0 -1
|
@@ -113,16 +113,23 @@ interface ISentTool {
|
|
|
113
113
|
name: string;
|
|
114
114
|
description: string;
|
|
115
115
|
parameters: unknown;
|
|
116
|
+
/** Whether the tool was declared strict on the wire (absent counts as not strict). */
|
|
117
|
+
strict: boolean;
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
function sentToolsFromResponses(client: IFakeClient): ISentTool[] {
|
|
119
121
|
const [requestParams] = client.responses.create.mock.calls[
|
|
120
122
|
client.responses.create.mock.calls.length - 1
|
|
121
|
-
] as [
|
|
123
|
+
] as [
|
|
124
|
+
{
|
|
125
|
+
tools?: Array<{ name: string; description: string; parameters: unknown; strict?: boolean }>;
|
|
126
|
+
},
|
|
127
|
+
];
|
|
122
128
|
return (requestParams.tools ?? []).map((tool) => ({
|
|
123
129
|
name: tool.name,
|
|
124
130
|
description: tool.description,
|
|
125
131
|
parameters: tool.parameters,
|
|
132
|
+
strict: tool.strict === true,
|
|
126
133
|
}));
|
|
127
134
|
}
|
|
128
135
|
|
|
@@ -131,13 +138,16 @@ function sentToolsFromChatCompletions(client: IFakeClient): ISentTool[] {
|
|
|
131
138
|
client.chat.completions.create.mock.calls.length - 1
|
|
132
139
|
] as [
|
|
133
140
|
{
|
|
134
|
-
tools?: Array<{
|
|
141
|
+
tools?: Array<{
|
|
142
|
+
function: { name: string; description: string; parameters: unknown; strict?: boolean };
|
|
143
|
+
}>;
|
|
135
144
|
},
|
|
136
145
|
];
|
|
137
146
|
return (requestParams.tools ?? []).map((tool) => ({
|
|
138
147
|
name: tool.function.name,
|
|
139
148
|
description: tool.function.description,
|
|
140
149
|
parameters: tool.function.parameters,
|
|
150
|
+
strict: tool.function.strict === true,
|
|
141
151
|
}));
|
|
142
152
|
}
|
|
143
153
|
|
|
@@ -173,6 +183,7 @@ describe.each([
|
|
|
173
183
|
|
|
174
184
|
const sent = sentTools(client);
|
|
175
185
|
expect(sent).toHaveLength(3);
|
|
186
|
+
expect(sent.every((tool) => !tool.strict)).toBe(true);
|
|
176
187
|
for (const [index, tool] of tools.entries()) {
|
|
177
188
|
const projection = projectToolSchema(tool, PERMISSIVE_PROFILE);
|
|
178
189
|
expect(sent[index]?.parameters).toEqual(projection.tool.parameters);
|
|
@@ -242,6 +253,7 @@ describe.each([
|
|
|
242
253
|
|
|
243
254
|
const sent = sentTools(client);
|
|
244
255
|
expect(sent.map((tool) => tool.name)).toEqual(['good_tool_one', 'good_tool_two']);
|
|
256
|
+
expect(sent.every((tool) => !tool.strict)).toBe(true);
|
|
245
257
|
});
|
|
246
258
|
},
|
|
247
259
|
);
|
|
@@ -274,6 +286,7 @@ describe.each([
|
|
|
274
286
|
const projection = projectToolSchema(tool, STRICT_PROFILE);
|
|
275
287
|
expect(sent[index]?.parameters).toEqual(projection.tool.parameters);
|
|
276
288
|
expect(sent[index]?.description).toEqual(projection.tool.description);
|
|
289
|
+
expect(sent[index]?.strict).toBe(true);
|
|
277
290
|
}
|
|
278
291
|
});
|
|
279
292
|
|
|
@@ -297,6 +310,7 @@ describe.each([
|
|
|
297
310
|
const sent = sentTools(client);
|
|
298
311
|
const projection = projectToolSchema(fixtures.subsetOnly, STRICT_PROFILE);
|
|
299
312
|
expect(sent[0]?.parameters).toEqual(projection.tool.parameters);
|
|
313
|
+
expect(sent[0]?.strict).toBe(true);
|
|
300
314
|
});
|
|
301
315
|
},
|
|
302
316
|
);
|
|
@@ -56,7 +56,7 @@ export async function chatWithOpenAIChatCompletions(
|
|
|
56
56
|
payloadKind: 'request',
|
|
57
57
|
payload: requestParams,
|
|
58
58
|
});
|
|
59
|
-
const requestOptions = openAIRequestOptions(
|
|
59
|
+
const requestOptions = openAIRequestOptions(input.chatOptions?.signal, input.requestHeaders);
|
|
60
60
|
const response = requestOptions
|
|
61
61
|
? await client.chat.completions.create(requestParams, requestOptions)
|
|
62
62
|
: await client.chat.completions.create(requestParams);
|
|
@@ -151,7 +151,7 @@ function buildChatRequestParams(
|
|
|
151
151
|
}),
|
|
152
152
|
...(input.chatOptions?.maxTokens !== undefined && { max_tokens: input.chatOptions.maxTokens }),
|
|
153
153
|
...(input.chatOptions?.tools && {
|
|
154
|
-
tools: convertToOpenAITools(input.chatOptions.tools),
|
|
154
|
+
tools: convertToOpenAITools(input.chatOptions.tools, input.providerOptions.strictTools),
|
|
155
155
|
tool_choice: toOpenAICompatibleToolChoice(input.chatOptions.toolChoice),
|
|
156
156
|
}),
|
|
157
157
|
...(responseFormat !== undefined && { response_format: responseFormat }),
|
|
@@ -2,7 +2,9 @@ import type { IOpenAILogData } from '../types/api-types';
|
|
|
2
2
|
import type { ILogger } from '@robota-sdk/agent-core';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* IPayloadLogger interface for logging OpenAI
|
|
5
|
+
* IPayloadLogger interface for logging a summary of each OpenAI Chat Completions request
|
|
6
|
+
* (request metadata such as model, message count and whether tools were sent — not prompt or
|
|
7
|
+
* response content).
|
|
6
8
|
*
|
|
7
9
|
* This interface provides a contract for different logging implementations:
|
|
8
10
|
* - FilePayloadLogger: Node.js file-based logging
|
|
@@ -17,8 +19,8 @@ export interface IPayloadLogger {
|
|
|
17
19
|
isEnabled(): boolean;
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
|
-
* Log
|
|
21
|
-
* @param payload -
|
|
22
|
+
* Log a request summary
|
|
23
|
+
* @param payload - Summary of the outgoing Chat Completions request
|
|
22
24
|
* @param type - Type of operation ('chat' or 'stream')
|
|
23
25
|
*/
|
|
24
26
|
logPayload(payload: IOpenAILogData, type: 'chat' | 'stream'): Promise<void>;
|
|
@@ -8,7 +8,7 @@ import type { IOpenAILogData } from '../types/api-types';
|
|
|
8
8
|
/**
|
|
9
9
|
* Console-based payload logger for browser environments
|
|
10
10
|
*
|
|
11
|
-
* This logger outputs
|
|
11
|
+
* This logger outputs a summary of each Chat Completions request to the browser console
|
|
12
12
|
* using structured logging. It's designed specifically for browser environments
|
|
13
13
|
* and development/debugging scenarios.
|
|
14
14
|
*
|
|
@@ -47,7 +47,7 @@ export class ConsolePayloadLogger implements IPayloadLogger {
|
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Log API payload to browser console
|
|
50
|
-
* @param payload -
|
|
50
|
+
* @param payload - Summary of the outgoing Chat Completions request
|
|
51
51
|
* @param type - Type of request ('chat' or 'stream')
|
|
52
52
|
*/
|
|
53
53
|
async logPayload(payload: IOpenAILogData, type: 'chat' | 'stream' = 'chat'): Promise<void> {
|
|
@@ -16,7 +16,7 @@ const OWNER_ONLY_DIR_MODE = 0o700;
|
|
|
16
16
|
/**
|
|
17
17
|
* File-based payload logger for Node.js environments
|
|
18
18
|
*
|
|
19
|
-
* This logger saves
|
|
19
|
+
* This logger saves a summary of each Chat Completions request to a JSON file on disk.
|
|
20
20
|
* It's designed specifically for Node.js environments with filesystem access.
|
|
21
21
|
*
|
|
22
22
|
* @example
|
|
@@ -65,8 +65,8 @@ export class FilePayloadLogger implements IPayloadLogger {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* Log
|
|
69
|
-
* @param payload -
|
|
68
|
+
* Log a request summary to file
|
|
69
|
+
* @param payload - Summary of the outgoing Chat Completions request
|
|
70
70
|
* @param type - Type of request ('chat' or 'stream')
|
|
71
71
|
*/
|
|
72
72
|
async logPayload(payload: IOpenAILogData, type: 'chat' | 'stream' = 'chat'): Promise<void> {
|
|
@@ -89,8 +89,9 @@ export class FilePayloadLogger implements IPayloadLogger {
|
|
|
89
89
|
payload: sanitizeOpenAILogData(payload),
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
-
// SEC-003: payload logs
|
|
93
|
-
// caller-supplied, so create them owner-only
|
|
92
|
+
// SEC-003: payload logs record the caller's request history (a request summary, not
|
|
93
|
+
// prompt/response content) and `logDir` is caller-supplied, so create them owner-only
|
|
94
|
+
// rather than under the process umask.
|
|
94
95
|
await fs.promises.writeFile(filepath, JSON.stringify(logData, null, 2), {
|
|
95
96
|
encoding: 'utf8',
|
|
96
97
|
mode: OWNER_ONLY_FILE_MODE,
|
|
@@ -16,8 +16,12 @@ export function convertToOpenAIMessages(
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Convert tool schemas to OpenAI function tool format.
|
|
19
|
+
* Convert tool schemas to OpenAI function tool format. With `strictTools` each function is declared
|
|
20
|
+
* `strict: true`, as the Responses surface does, so strict mode is requested on both surfaces.
|
|
20
21
|
*/
|
|
21
|
-
export function convertToOpenAITools(
|
|
22
|
-
|
|
22
|
+
export function convertToOpenAITools(
|
|
23
|
+
tools: IToolSchema[],
|
|
24
|
+
strictTools?: boolean,
|
|
25
|
+
): OpenAI.Chat.ChatCompletionTool[] {
|
|
26
|
+
return convertToOpenAICompatibleTools(tools, { strict: strictTools === true });
|
|
23
27
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { assembleOpenAIResponsesStream, parseOpenAIResponsesResponse } from './responses-parser';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
IOpenAIResponsesResponse,
|
|
7
|
+
IOpenAIResponsesUsage,
|
|
8
|
+
TOpenAIResponsesStreamEvent,
|
|
9
|
+
} from './responses-types';
|
|
10
|
+
|
|
11
|
+
function responseWithUsage(usage: IOpenAIResponsesUsage): IOpenAIResponsesResponse {
|
|
12
|
+
return {
|
|
13
|
+
id: 'resp-cached',
|
|
14
|
+
model: 'gpt-4o',
|
|
15
|
+
output_text: 'ok',
|
|
16
|
+
output: [],
|
|
17
|
+
status: 'completed',
|
|
18
|
+
usage,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function usageOf(message: unknown): Record<string, number> | undefined {
|
|
23
|
+
return (message as { usage?: Record<string, number> }).usage;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function* eventsFrom(
|
|
27
|
+
events: TOpenAIResponsesStreamEvent[],
|
|
28
|
+
): AsyncIterable<TOpenAIResponsesStreamEvent> {
|
|
29
|
+
for (const event of events) yield event;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('OpenAI Responses usage', () => {
|
|
33
|
+
it('surfaces input_tokens_details.cached_tokens as cacheReadTokens on the message usage', () => {
|
|
34
|
+
const message = parseOpenAIResponsesResponse(
|
|
35
|
+
responseWithUsage({
|
|
36
|
+
input_tokens: 1000,
|
|
37
|
+
output_tokens: 20,
|
|
38
|
+
total_tokens: 1020,
|
|
39
|
+
input_tokens_details: { cached_tokens: 800 },
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(usageOf(message)).toEqual({
|
|
44
|
+
promptTokens: 1000,
|
|
45
|
+
completionTokens: 20,
|
|
46
|
+
totalTokens: 1020,
|
|
47
|
+
cacheReadTokens: 800,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('adds no cacheReadTokens when the usage carries no cached-token details', () => {
|
|
52
|
+
const message = parseOpenAIResponsesResponse(
|
|
53
|
+
responseWithUsage({ input_tokens: 1000, output_tokens: 20, total_tokens: 1020 }),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(usageOf(message)).toBeDefined();
|
|
57
|
+
expect(usageOf(message)).not.toHaveProperty('cacheReadTokens');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('leaves totalTokens absent when the response omits total_tokens', () => {
|
|
61
|
+
const message = parseOpenAIResponsesResponse(
|
|
62
|
+
responseWithUsage({ input_tokens: 5000, output_tokens: 100 }),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(usageOf(message)).toEqual({ promptTokens: 5000, completionTokens: 100 });
|
|
66
|
+
expect(message.metadata?.['usageProvenance']).toBe('partial');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('carries the cached tokens of a streamed response.completed usage', async () => {
|
|
70
|
+
const message = await assembleOpenAIResponsesStream({
|
|
71
|
+
stream: eventsFrom([
|
|
72
|
+
{ type: 'response.output_text.delta', delta: 'ok' },
|
|
73
|
+
{
|
|
74
|
+
type: 'response.completed',
|
|
75
|
+
response: responseWithUsage({
|
|
76
|
+
input_tokens: 1000,
|
|
77
|
+
output_tokens: 20,
|
|
78
|
+
total_tokens: 1020,
|
|
79
|
+
input_tokens_details: { cached_tokens: 800 },
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
]),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(usageOf(message)).toMatchObject({ promptTokens: 1000, cacheReadTokens: 800 });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -13,7 +13,16 @@ import type {
|
|
|
13
13
|
TOpenAIResponsesOutputItem,
|
|
14
14
|
TOpenAIResponsesStreamEvent,
|
|
15
15
|
} from './responses-types';
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
IToolCall,
|
|
18
|
+
ITokenUsageWithCacheRead,
|
|
19
|
+
TTextDeltaCallback,
|
|
20
|
+
TUniversalMessage,
|
|
21
|
+
} from '@robota-sdk/agent-core';
|
|
22
|
+
|
|
23
|
+
/** Message usage; `totalTokens` is absent when the response omitted `total_tokens`. */
|
|
24
|
+
type TResponsesMessageUsage = Omit<ITokenUsageWithCacheRead, 'totalTokens'> &
|
|
25
|
+
Partial<Pick<ITokenUsageWithCacheRead, 'totalTokens'>>;
|
|
17
26
|
|
|
18
27
|
interface IOpenAIResponsesStreamAssemblyOptions {
|
|
19
28
|
stream: AsyncIterable<TOpenAIResponsesStreamEvent>;
|
|
@@ -27,12 +36,6 @@ interface IOpenAIResponsesReasoningMetadata {
|
|
|
27
36
|
hasEncryptedReasoning: boolean;
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
interface IOpenAIResponseUsage {
|
|
31
|
-
promptTokens: number;
|
|
32
|
-
completionTokens: number;
|
|
33
|
-
totalTokens: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
39
|
interface IOpenAIResponsesStreamState {
|
|
37
40
|
textParts: string[];
|
|
38
41
|
toolCalls: IToolCall[];
|
|
@@ -191,11 +194,14 @@ function buildMetadata(
|
|
|
191
194
|
};
|
|
192
195
|
}
|
|
193
196
|
|
|
194
|
-
function mapUsage(usage: IOpenAIResponsesUsage):
|
|
197
|
+
function mapUsage(usage: IOpenAIResponsesUsage): TResponsesMessageUsage {
|
|
198
|
+
const cachedTokens = usage.input_tokens_details?.cached_tokens;
|
|
195
199
|
return {
|
|
196
200
|
promptTokens: usage.input_tokens ?? 0,
|
|
197
201
|
completionTokens: usage.output_tokens ?? 0,
|
|
198
|
-
|
|
202
|
+
// An omitted total stays omitted: a 0 reads downstream as a real, empty call.
|
|
203
|
+
...(usage.total_tokens !== undefined && { totalTokens: usage.total_tokens }),
|
|
204
|
+
...(typeof cachedTokens === 'number' && { cacheReadTokens: cachedTokens }),
|
|
199
205
|
};
|
|
200
206
|
}
|
|
201
207
|
|
|
@@ -148,6 +148,8 @@ export interface IOpenAIResponsesUsage {
|
|
|
148
148
|
input_tokens?: number;
|
|
149
149
|
output_tokens?: number;
|
|
150
150
|
total_tokens?: number;
|
|
151
|
+
/** `cached_tokens` is the part of `input_tokens` served from the prompt cache. */
|
|
152
|
+
input_tokens_details?: { cached_tokens?: number } | null;
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
export interface IOpenAIResponsesErrorBody {
|
package/src/openai/types.ts
CHANGED
|
@@ -93,7 +93,7 @@ export interface IOpenAIProviderOptions {
|
|
|
93
93
|
* @example
|
|
94
94
|
* ```ts
|
|
95
95
|
* // Vercel AI Gateway with a non-OpenAI model slug
|
|
96
|
-
*
|
|
96
|
+
* new OpenAIProvider({
|
|
97
97
|
* apiKey: process.env.AI_GATEWAY_API_KEY,
|
|
98
98
|
* baseURL: 'https://ai-gateway.vercel.sh/v1',
|
|
99
99
|
* defaultModel: 'anthropic/claude-sonnet-4-5',
|
|
@@ -156,6 +156,9 @@ export interface IOpenAIProviderOptions {
|
|
|
156
156
|
* schema marked optional becomes required and gains a `null` branch. The model must then supply
|
|
157
157
|
* the key explicitly, with `null` standing for "not provided". A tool whose handler distinguishes
|
|
158
158
|
* an absent key from a null value will see the difference.
|
|
159
|
+
*
|
|
160
|
+
* Both surfaces declare the functions strict (`strict: true` on each tool), so an OpenAI-compatible
|
|
161
|
+
* endpoint that rejects that field needs `strictTools` left off.
|
|
159
162
|
*/
|
|
160
163
|
strictTools?: boolean;
|
|
161
164
|
|
|
@@ -174,7 +177,9 @@ export interface IOpenAIProviderOptions {
|
|
|
174
177
|
client?: OpenAI;
|
|
175
178
|
|
|
176
179
|
/**
|
|
177
|
-
* Payload logger
|
|
180
|
+
* Payload logger that receives a summary of each Chat Completions request (model, message
|
|
181
|
+
* count, whether tools were sent, temperature, max tokens) for debugging — not prompt or
|
|
182
|
+
* response content. Not called on the Responses API surface.
|
|
178
183
|
*
|
|
179
184
|
* Use different implementations based on your environment:
|
|
180
185
|
* - FilePayloadLogger: Node.js file-based logging
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"payload-logger-BaW0K8yI.d.cts","names":[],"sources":["../../src/openai/types/api-types.ts","../../src/openai/interfaces/payload-logger.ts"],"mappings":";;;;;;UAwGiB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;;UCpGe;;;;;EAKf;;;;;;EAOA,WAAW,SAAS,gBAAgB,0BAA0B;;;;;UAM/C;;;;;EAKf;;;;;EAMA;;;;;EAMA,SAAS"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"payload-logger-BaW0K8yI.d.ts","names":[],"sources":["../../src/openai/types/api-types.ts","../../src/openai/interfaces/payload-logger.ts"],"mappings":";;;;;;UAwGiB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;;UCpGe;;;;;EAKf;;;;;;EAOA,WAAW,SAAS,gBAAgB,0BAA0B;;;;;UAM/C;;;;;EAKf;;;;;EAMA;;;;;EAMA,SAAS"}
|