@robota-sdk/agent-provider-openai 3.0.0-beta.82 → 3.0.0-beta.83
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 +207 -0
- package/README.md +110 -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 +10 -5
- package/src/openai/__tests__/abort-signal-wire.test.ts +210 -0
- 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
|
@@ -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"}
|