@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/README.md +110 -0
  3. package/dist/node/index.cjs +1 -1
  4. package/dist/node/index.d.cts +8 -3
  5. package/dist/node/index.d.cts.map +1 -1
  6. package/dist/node/index.d.ts +8 -3
  7. package/dist/node/index.d.ts.map +1 -1
  8. package/dist/node/index.js +1 -1
  9. package/dist/node/index.js.map +1 -1
  10. package/dist/node/loggers/index.d.cts +6 -6
  11. package/dist/node/loggers/index.d.cts.map +1 -1
  12. package/dist/node/loggers/index.d.ts +6 -6
  13. package/dist/node/loggers/index.d.ts.map +1 -1
  14. package/dist/node/loggers/index.js.map +1 -1
  15. package/dist/node/{payload-logger-BaW0K8yI.d.ts → payload-logger-n89AQdUn.d.cts} +6 -4
  16. package/dist/node/payload-logger-n89AQdUn.d.cts.map +1 -0
  17. package/dist/node/{payload-logger-BaW0K8yI.d.cts → payload-logger-n89AQdUn.d.ts} +6 -4
  18. package/dist/node/payload-logger-n89AQdUn.d.ts.map +1 -0
  19. package/package.json +10 -5
  20. package/src/openai/__tests__/abort-signal-wire.test.ts +210 -0
  21. package/src/openai/__tests__/strict-tools-closure.test.ts +49 -2
  22. package/src/openai/__tests__/tool-schema-projection.test.ts +16 -2
  23. package/src/openai/chat-completions-chat.ts +2 -2
  24. package/src/openai/interfaces/payload-logger.ts +5 -3
  25. package/src/openai/loggers/console-payload-logger.ts +2 -2
  26. package/src/openai/loggers/file-payload-logger.ts +6 -5
  27. package/src/openai/message-converter.ts +7 -3
  28. package/src/openai/responses-parser.test.ts +87 -0
  29. package/src/openai/responses-parser.ts +15 -9
  30. package/src/openai/responses-types.ts +2 -0
  31. package/src/openai/types.ts +7 -2
  32. package/dist/node/payload-logger-BaW0K8yI.d.cts.map +0 -1
  33. 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 API request/response payloads to the browser console
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 - The API request 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 API request/response payloads to JSON files on disk.
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 API payload to file
69
- * @param payload - The API request 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 contain prompt/response content and `logDir` is
93
- // caller-supplied, so create them owner-only rather than under the process umask.
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(tools: IToolSchema[]): OpenAI.Chat.ChatCompletionTool[] {
22
- return convertToOpenAICompatibleTools(tools);
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 { IToolCall, TTextDeltaCallback, TUniversalMessage } from '@robota-sdk/agent-core';
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): IOpenAIResponseUsage {
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
- totalTokens: usage.total_tokens ?? 0,
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 {
@@ -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
- * createOpenAIProvider({
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 instance for debugging API requests/responses
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"}