@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.
Files changed (70) hide show
  1. package/LICENSE +661 -21
  2. package/dist/node/index.cjs +1 -2
  3. package/dist/node/index.d.cts +271 -267
  4. package/dist/node/index.d.cts.map +1 -0
  5. package/dist/node/index.d.ts +271 -267
  6. package/dist/node/index.d.ts.map +1 -0
  7. package/dist/node/index.js +2 -2
  8. package/dist/node/index.js.map +1 -0
  9. package/dist/node/loggers/index.cjs +1 -0
  10. package/dist/node/loggers/index.d.cts +94 -0
  11. package/dist/node/loggers/index.d.cts.map +1 -0
  12. package/dist/node/loggers/index.d.ts +94 -0
  13. package/dist/node/loggers/index.d.ts.map +1 -0
  14. package/dist/node/loggers/index.js +2 -0
  15. package/dist/node/loggers/index.js.map +1 -0
  16. package/dist/node/payload-logger-BaW0K8yI.d.cts +61 -0
  17. package/dist/node/payload-logger-BaW0K8yI.d.cts.map +1 -0
  18. package/dist/node/payload-logger-BaW0K8yI.d.ts +61 -0
  19. package/dist/node/payload-logger-BaW0K8yI.d.ts.map +1 -0
  20. package/dist/node/rolldown-runtime-CMqjfN_6.cjs +1 -0
  21. package/package.json +69 -86
  22. package/src/index.ts +1 -0
  23. package/src/openai/__tests__/endpoint-provenance.test.ts +44 -0
  24. package/src/openai/__tests__/provider-errors.test.ts +114 -0
  25. package/src/openai/__tests__/request-format.test.ts +59 -0
  26. package/src/openai/__tests__/strict-tools-closure.test.ts +136 -0
  27. package/src/openai/__tests__/tool-schema-projection.test.ts +302 -0
  28. package/src/openai/__tests__/trace-context-wire.test.ts +114 -0
  29. package/src/openai/adapter.test.ts +494 -0
  30. package/src/openai/adapter.ts +145 -0
  31. package/src/openai/chat-completions-chat.ts +225 -0
  32. package/src/openai/executor-integration.test.ts +213 -0
  33. package/src/openai/index.ts +19 -0
  34. package/src/openai/interfaces/payload-logger.ts +48 -0
  35. package/src/openai/loggers/console-payload-logger.test.ts +173 -0
  36. package/src/openai/loggers/console-payload-logger.ts +96 -0
  37. package/src/openai/loggers/console.ts +9 -0
  38. package/src/openai/loggers/file-payload-logger.test.ts +243 -0
  39. package/src/openai/loggers/file-payload-logger.ts +124 -0
  40. package/src/openai/loggers/file.ts +9 -0
  41. package/src/openai/loggers/index.ts +12 -0
  42. package/src/openai/loggers/sanitize-openai-log-data.test.ts +89 -0
  43. package/src/openai/loggers/sanitize-openai-log-data.ts +14 -0
  44. package/src/openai/message-converter.ts +23 -0
  45. package/src/openai/model-effort-table.ts +19 -0
  46. package/src/openai/model-effort-verification-config.test.ts +45 -0
  47. package/src/openai/model-effort-verification-config.ts +35 -0
  48. package/src/openai/openai-request-format.ts +116 -0
  49. package/src/openai/parsers/response-parser.test.ts +407 -0
  50. package/src/openai/parsers/response-parser.ts +48 -0
  51. package/src/openai/provider-definition.test.ts +74 -0
  52. package/src/openai/provider-definition.ts +139 -0
  53. package/src/openai/provider.test.ts +1686 -0
  54. package/src/openai/provider.ts +392 -0
  55. package/src/openai/reasoning-effort.test.ts +272 -0
  56. package/src/openai/reasoning-effort.ts +49 -0
  57. package/src/openai/request-id.ts +39 -0
  58. package/src/openai/request-options.ts +14 -0
  59. package/src/openai/responses-chat.ts +285 -0
  60. package/src/openai/responses-converter.ts +122 -0
  61. package/src/openai/responses-parser.ts +297 -0
  62. package/src/openai/responses-stream-utils.ts +45 -0
  63. package/src/openai/responses-types.ts +193 -0
  64. package/src/openai/streaming/stream-assembler.ts +3 -0
  65. package/src/openai/types/api-types.ts +113 -0
  66. package/src/openai/types.ts +235 -0
  67. package/CHANGELOG.md +0 -614
  68. package/README.md +0 -467
  69. package/dist/browser/index.d.ts +0 -307
  70. package/dist/browser/index.js +0 -2
@@ -0,0 +1,96 @@
1
+ import { SilentLogger, type ILogger } from '@robota-sdk/agent-core';
2
+
3
+ import { sanitizeOpenAILogData } from './sanitize-openai-log-data';
4
+
5
+ import type { IPayloadLogger, IPayloadLoggerOptions } from '../interfaces/payload-logger';
6
+ import type { IOpenAILogData } from '../types/api-types';
7
+
8
+ /**
9
+ * Console-based payload logger for browser environments
10
+ *
11
+ * This logger outputs API request/response payloads to the browser console
12
+ * using structured logging. It's designed specifically for browser environments
13
+ * and development/debugging scenarios.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { ConsolePayloadLogger } from '@robota-sdk/agent-provider-openai/loggers';
18
+ *
19
+ * const logger = new ConsolePayloadLogger({
20
+ * enabled: true,
21
+ * includeTimestamp: true
22
+ * });
23
+ *
24
+ * const provider = new OpenAIProvider({
25
+ * client: openaiClient,
26
+ * payloadLogger: logger
27
+ * });
28
+ * ```
29
+ */
30
+ export class ConsolePayloadLogger implements IPayloadLogger {
31
+ private readonly enabled: boolean;
32
+ private readonly includeTimestamp: boolean;
33
+ private readonly logger: ILogger;
34
+
35
+ constructor(options: IPayloadLoggerOptions = {}) {
36
+ this.enabled = options.enabled ?? true;
37
+ this.includeTimestamp = options.includeTimestamp ?? true;
38
+ this.logger = options.logger || SilentLogger;
39
+ }
40
+
41
+ /**
42
+ * Check if logging is enabled
43
+ */
44
+ isEnabled(): boolean {
45
+ return this.enabled;
46
+ }
47
+
48
+ /**
49
+ * Log API payload to browser console
50
+ * @param payload - The API request payload
51
+ * @param type - Type of request ('chat' or 'stream')
52
+ */
53
+ async logPayload(payload: IOpenAILogData, type: 'chat' | 'stream' = 'chat'): Promise<void> {
54
+ if (!this.enabled) {
55
+ return;
56
+ }
57
+
58
+ try {
59
+ const sanitizedPayload = sanitizeOpenAILogData(payload);
60
+
61
+ // Use structured console logging for better browser developer tools integration
62
+ const title = `[OpenAI ${type.toUpperCase()}] API Payload`;
63
+ const timeInfo = this.includeTimestamp ? ` (${sanitizedPayload.timestamp})` : '';
64
+
65
+ // Group related log entries for better organization
66
+ this.logger.group?.(`${title}${timeInfo}`);
67
+
68
+ // Log different aspects with appropriate console methods
69
+ this.logger.info('📋 Request Details:', {
70
+ model: payload.model,
71
+ messagesCount: payload.messagesCount,
72
+ hasTools: payload.hasTools,
73
+ temperature: payload.temperature,
74
+ maxTokens: payload.maxTokens,
75
+ });
76
+
77
+ this.logger.debug('🔍 Full Payload:', { type, provider: 'openai', ...sanitizedPayload });
78
+
79
+ this.logger.groupEnd?.();
80
+ } catch (error) {
81
+ // Don't throw errors - just log them and continue
82
+ // This ensures that API logging failures don't break the main functionality
83
+ this.logger.error(
84
+ '[ConsolePayloadLogger] Failed to log payload:',
85
+ error instanceof Error ? error.message : 'Unknown error',
86
+ );
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Sanitize payload to remove sensitive information
92
+ * @param payload - Raw payload object
93
+ * @returns Sanitized payload
94
+ */
95
+ // Sanitization intentionally lives in ./sanitize-openai-log-data.ts (SSOT utility).
96
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Browser console-based payload logger
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { ConsolePayloadLogger } from '@robota-sdk/agent-provider-openai/loggers';
7
+ * ```
8
+ */
9
+ export { ConsolePayloadLogger } from './console-payload-logger';
@@ -0,0 +1,243 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import { FilePayloadLogger } from './file-payload-logger';
4
+ import type { ILogger } from '@robota-sdk/agent-core';
5
+ import type { IOpenAILogData } from '../types/api-types';
6
+
7
+ // Mock fs module
8
+ vi.mock('fs', () => {
9
+ return {
10
+ existsSync: vi.fn(),
11
+ mkdirSync: vi.fn(),
12
+ promises: {
13
+ writeFile: vi.fn(),
14
+ },
15
+ };
16
+ });
17
+
18
+ // SEC-003: `fs` is mocked, so this is an inert fixture path; it deliberately does not
19
+ // live under the OS temp dir.
20
+ const LOG_DIR = '/var/robota-test/openai-payload-logs';
21
+
22
+ function createMockLogger(): ILogger {
23
+ return {
24
+ info: vi.fn(),
25
+ warn: vi.fn(),
26
+ error: vi.fn(),
27
+ debug: vi.fn(),
28
+ log: vi.fn(),
29
+ };
30
+ }
31
+
32
+ function createSamplePayload(): IOpenAILogData {
33
+ return {
34
+ model: 'gpt-4',
35
+ messagesCount: 3,
36
+ hasTools: true,
37
+ temperature: 0.7,
38
+ maxTokens: 1000,
39
+ timestamp: '2026-01-01T00:00:00.000Z',
40
+ };
41
+ }
42
+
43
+ describe('FilePayloadLogger', () => {
44
+ beforeEach(() => {
45
+ vi.clearAllMocks();
46
+ // Default: directory exists
47
+ vi.mocked(fs.existsSync).mockReturnValue(true);
48
+ vi.mocked(fs.promises.writeFile).mockResolvedValue(undefined);
49
+ });
50
+
51
+ describe('constructor', () => {
52
+ it('should create logger with required logDir option', () => {
53
+ const logger = new FilePayloadLogger({ logDir: LOG_DIR });
54
+ expect(logger.isEnabled()).toBe(true);
55
+ });
56
+
57
+ it('should default to enabled when not specified', () => {
58
+ const logger = new FilePayloadLogger({ logDir: LOG_DIR });
59
+ expect(logger.isEnabled()).toBe(true);
60
+ });
61
+
62
+ it('should respect enabled false option', () => {
63
+ const logger = new FilePayloadLogger({ logDir: LOG_DIR, enabled: false });
64
+ expect(logger.isEnabled()).toBe(false);
65
+ });
66
+
67
+ it('should create log directory if it does not exist and enabled', () => {
68
+ vi.mocked(fs.existsSync).mockReturnValue(false);
69
+ new FilePayloadLogger({ logDir: LOG_DIR });
70
+
71
+ expect(fs.mkdirSync).toHaveBeenCalledWith(LOG_DIR, { recursive: true, mode: 0o700 });
72
+ });
73
+
74
+ it('should not create log directory when disabled', () => {
75
+ vi.mocked(fs.existsSync).mockReturnValue(false);
76
+ new FilePayloadLogger({ logDir: LOG_DIR, enabled: false });
77
+
78
+ expect(fs.mkdirSync).not.toHaveBeenCalled();
79
+ });
80
+
81
+ it('should not create log directory if it already exists', () => {
82
+ vi.mocked(fs.existsSync).mockReturnValue(true);
83
+ new FilePayloadLogger({ logDir: LOG_DIR });
84
+
85
+ expect(fs.mkdirSync).not.toHaveBeenCalled();
86
+ });
87
+
88
+ it('should handle directory creation error gracefully', () => {
89
+ const mockLogger = createMockLogger();
90
+ vi.mocked(fs.existsSync).mockReturnValue(false);
91
+ vi.mocked(fs.mkdirSync).mockImplementation(() => {
92
+ throw new Error('Permission denied');
93
+ });
94
+
95
+ // Should not throw
96
+ const logger = new FilePayloadLogger({ logDir: '/root/logs', logger: mockLogger });
97
+ expect(logger.isEnabled()).toBe(true);
98
+ expect(mockLogger.error).toHaveBeenCalledWith(
99
+ expect.stringContaining('[FilePayloadLogger]'),
100
+ expect.objectContaining({ error: 'Permission denied' }),
101
+ );
102
+ });
103
+ });
104
+
105
+ describe('isEnabled', () => {
106
+ it('should return true when enabled', () => {
107
+ const logger = new FilePayloadLogger({ logDir: LOG_DIR, enabled: true });
108
+ expect(logger.isEnabled()).toBe(true);
109
+ });
110
+
111
+ it('should return false when disabled', () => {
112
+ const logger = new FilePayloadLogger({ logDir: LOG_DIR, enabled: false });
113
+ expect(logger.isEnabled()).toBe(false);
114
+ });
115
+ });
116
+
117
+ describe('logPayload', () => {
118
+ it('should write payload to a JSON file', async () => {
119
+ const logger = new FilePayloadLogger({ logDir: LOG_DIR });
120
+ const payload = createSamplePayload();
121
+
122
+ await logger.logPayload(payload, 'chat');
123
+
124
+ expect(fs.promises.writeFile).toHaveBeenCalledWith(
125
+ expect.stringContaining(`${LOG_DIR}/openai-chat-`),
126
+ expect.any(String),
127
+ // SEC-003: payload logs hold prompt content, so they must be owner-only.
128
+ { encoding: 'utf8', mode: 0o600 },
129
+ );
130
+ });
131
+
132
+ it('should write valid JSON content', async () => {
133
+ const logger = new FilePayloadLogger({ logDir: LOG_DIR });
134
+ const payload = createSamplePayload();
135
+
136
+ await logger.logPayload(payload, 'chat');
137
+
138
+ const writeCall = vi.mocked(fs.promises.writeFile).mock.calls[0];
139
+ const content = writeCall[1] as string;
140
+ const parsed = JSON.parse(content);
141
+
142
+ expect(parsed.type).toBe('chat');
143
+ expect(parsed.provider).toBe('openai');
144
+ expect(parsed.payload.model).toBe('gpt-4');
145
+ expect(parsed.timestamp).toBeDefined();
146
+ });
147
+
148
+ it('should use stream type in filename when type is stream', async () => {
149
+ const logger = new FilePayloadLogger({ logDir: LOG_DIR });
150
+ const payload = createSamplePayload();
151
+
152
+ await logger.logPayload(payload, 'stream');
153
+
154
+ const writeCall = vi.mocked(fs.promises.writeFile).mock.calls[0];
155
+ const filepath = writeCall[0] as string;
156
+ expect(filepath).toContain('openai-stream-');
157
+ });
158
+
159
+ it('should include timestamp in filename when includeTimestamp is true', async () => {
160
+ const logger = new FilePayloadLogger({
161
+ logDir: LOG_DIR,
162
+ includeTimestamp: true,
163
+ });
164
+ const payload = createSamplePayload();
165
+
166
+ await logger.logPayload(payload, 'chat');
167
+
168
+ const writeCall = vi.mocked(fs.promises.writeFile).mock.calls[0];
169
+ const filepath = writeCall[0] as string;
170
+ // Timestamp format replaces : and . with -
171
+ expect(filepath).toMatch(/openai-chat-\d{4}-\d{2}-\d{2}T/);
172
+ });
173
+
174
+ it('should use Date.now() in filename when includeTimestamp is false', async () => {
175
+ const logger = new FilePayloadLogger({
176
+ logDir: LOG_DIR,
177
+ includeTimestamp: false,
178
+ });
179
+ const payload = createSamplePayload();
180
+
181
+ await logger.logPayload(payload, 'chat');
182
+
183
+ const writeCall = vi.mocked(fs.promises.writeFile).mock.calls[0];
184
+ const filepath = writeCall[0] as string;
185
+ // Should contain a numeric timestamp
186
+ expect(filepath).toMatch(/openai-chat-\d+\.json$/);
187
+ });
188
+
189
+ it('should skip logging when disabled', async () => {
190
+ const logger = new FilePayloadLogger({ logDir: LOG_DIR, enabled: false });
191
+ const payload = createSamplePayload();
192
+
193
+ await logger.logPayload(payload, 'chat');
194
+
195
+ expect(fs.promises.writeFile).not.toHaveBeenCalled();
196
+ });
197
+
198
+ it('should not throw on write error and log the error', async () => {
199
+ const mockLogger = createMockLogger();
200
+ vi.mocked(fs.promises.writeFile).mockRejectedValue(new Error('Disk full'));
201
+
202
+ const logger = new FilePayloadLogger({ logDir: LOG_DIR, logger: mockLogger });
203
+ const payload = createSamplePayload();
204
+
205
+ // Should not throw
206
+ await expect(logger.logPayload(payload, 'chat')).resolves.toBeUndefined();
207
+ expect(mockLogger.error).toHaveBeenCalledWith(
208
+ expect.stringContaining('[FilePayloadLogger]'),
209
+ expect.objectContaining({ error: 'Disk full' }),
210
+ );
211
+ });
212
+
213
+ it('should sanitize payload before writing', async () => {
214
+ const logger = new FilePayloadLogger({ logDir: LOG_DIR });
215
+ const payload = createSamplePayload();
216
+
217
+ await logger.logPayload(payload, 'chat');
218
+
219
+ const writeCall = vi.mocked(fs.promises.writeFile).mock.calls[0];
220
+ const content = writeCall[1] as string;
221
+ const parsed = JSON.parse(content);
222
+
223
+ // Verify the payload is a sanitized copy
224
+ expect(parsed.payload).toEqual(
225
+ expect.objectContaining({
226
+ model: 'gpt-4',
227
+ messagesCount: 3,
228
+ }),
229
+ );
230
+ });
231
+
232
+ it('should default type to chat', async () => {
233
+ const logger = new FilePayloadLogger({ logDir: LOG_DIR });
234
+ const payload = createSamplePayload();
235
+
236
+ await logger.logPayload(payload, 'chat');
237
+
238
+ const writeCall = vi.mocked(fs.promises.writeFile).mock.calls[0];
239
+ const filepath = writeCall[0] as string;
240
+ expect(filepath).toContain('openai-chat-');
241
+ });
242
+ });
243
+ });
@@ -0,0 +1,124 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ import { SilentLogger } from '@robota-sdk/agent-core';
5
+
6
+ import { sanitizeOpenAILogData } from './sanitize-openai-log-data';
7
+
8
+ import type { IPayloadLogger } from '../interfaces/payload-logger';
9
+ import type { IOpenAILogData } from '../types/api-types';
10
+ import type { ILogger } from '@robota-sdk/agent-core';
11
+
12
+ /** Owner-only access — no group or other bits (SEC-003 / CWE-377). */
13
+ const OWNER_ONLY_FILE_MODE = 0o600;
14
+ const OWNER_ONLY_DIR_MODE = 0o700;
15
+
16
+ /**
17
+ * File-based payload logger for Node.js environments
18
+ *
19
+ * This logger saves API request/response payloads to JSON files on disk.
20
+ * It's designed specifically for Node.js environments with filesystem access.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * import { FilePayloadLogger } from '@robota-sdk/agent-provider-openai/loggers';
25
+ *
26
+ * const logger = new FilePayloadLogger({
27
+ * logDir: './logs/api-payloads',
28
+ * enabled: true,
29
+ * includeTimestamp: true
30
+ * });
31
+ *
32
+ * const provider = new OpenAIProvider({
33
+ * client: openaiClient,
34
+ * payloadLogger: logger
35
+ * });
36
+ * ```
37
+ */
38
+ export class FilePayloadLogger implements IPayloadLogger {
39
+ private readonly enabled: boolean;
40
+ private readonly logDir: string;
41
+ private readonly includeTimestamp: boolean;
42
+ private readonly logger: ILogger;
43
+
44
+ constructor(options: {
45
+ logDir: string;
46
+ enabled?: boolean;
47
+ includeTimestamp?: boolean;
48
+ logger?: ILogger;
49
+ }) {
50
+ this.enabled = options.enabled ?? true;
51
+ this.logDir = options.logDir;
52
+ this.includeTimestamp = options.includeTimestamp ?? true;
53
+ this.logger = options.logger || SilentLogger;
54
+
55
+ if (this.enabled) {
56
+ this.ensureLogDirectoryExists();
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Check if logging is enabled
62
+ */
63
+ isEnabled(): boolean {
64
+ return this.enabled;
65
+ }
66
+
67
+ /**
68
+ * Log API payload to file
69
+ * @param payload - The API request payload
70
+ * @param type - Type of request ('chat' or 'stream')
71
+ */
72
+ async logPayload(payload: IOpenAILogData, type: 'chat' | 'stream' = 'chat'): Promise<void> {
73
+ if (!this.enabled) {
74
+ return;
75
+ }
76
+
77
+ try {
78
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
79
+ const filename = this.includeTimestamp
80
+ ? `openai-${type}-${timestamp}.json`
81
+ : `openai-${type}-${Date.now()}.json`;
82
+
83
+ const filepath = path.join(this.logDir, filename);
84
+
85
+ const logData = {
86
+ timestamp: new Date().toISOString(),
87
+ type,
88
+ provider: 'openai',
89
+ payload: sanitizeOpenAILogData(payload),
90
+ };
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.
94
+ await fs.promises.writeFile(filepath, JSON.stringify(logData, null, 2), {
95
+ encoding: 'utf8',
96
+ mode: OWNER_ONLY_FILE_MODE,
97
+ });
98
+
99
+ // Payload saved successfully (silent operation)
100
+ } catch (error) {
101
+ // Don't throw errors - just log them and continue
102
+ // This ensures that API logging failures don't break the main functionality
103
+ this.logger.error('[FilePayloadLogger] Failed to save payload log:', {
104
+ error: error instanceof Error ? error.message : String(error),
105
+ });
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Ensure log directory exists
111
+ */
112
+ private ensureLogDirectoryExists(): void {
113
+ try {
114
+ if (!fs.existsSync(this.logDir)) {
115
+ fs.mkdirSync(this.logDir, { recursive: true, mode: OWNER_ONLY_DIR_MODE });
116
+ }
117
+ } catch (error) {
118
+ this.logger.error('[FilePayloadLogger] Failed to create log directory:', {
119
+ error: error instanceof Error ? error.message : String(error),
120
+ });
121
+ }
122
+ }
123
+ // Sanitization intentionally lives in ./sanitize-openai-log-data.ts (SSOT utility).
124
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Node.js file-based payload logger
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { FilePayloadLogger } from '@robota-sdk/agent-provider-openai/loggers';
7
+ * ```
8
+ */
9
+ export { FilePayloadLogger } from './file-payload-logger';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * IPayloadLogger implementations for different environments
3
+ */
4
+
5
+ // Interfaces
6
+ export type { IPayloadLogger, IPayloadLoggerOptions } from '../interfaces/payload-logger';
7
+
8
+ // Node.js implementation
9
+ export { FilePayloadLogger } from './file-payload-logger';
10
+
11
+ // Browser implementation
12
+ export { ConsolePayloadLogger } from './console-payload-logger';
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { sanitizeOpenAILogData } from './sanitize-openai-log-data';
3
+ import type { IOpenAILogData } from '../types/api-types';
4
+
5
+ describe('sanitizeOpenAILogData', () => {
6
+ it('should return a deep copy of the payload', () => {
7
+ const payload: IOpenAILogData = {
8
+ model: 'gpt-4',
9
+ messagesCount: 3,
10
+ hasTools: true,
11
+ temperature: 0.7,
12
+ maxTokens: 1000,
13
+ timestamp: '2026-01-01T00:00:00.000Z',
14
+ };
15
+
16
+ const result = sanitizeOpenAILogData(payload);
17
+
18
+ expect(result).toEqual(payload);
19
+ expect(result).not.toBe(payload);
20
+ });
21
+
22
+ it('should not modify the original payload', () => {
23
+ const payload: IOpenAILogData = {
24
+ model: 'gpt-4',
25
+ messagesCount: 5,
26
+ hasTools: false,
27
+ timestamp: '2026-01-01T00:00:00.000Z',
28
+ };
29
+
30
+ const result = sanitizeOpenAILogData(payload);
31
+ result.model = 'gpt-3.5-turbo';
32
+ result.messagesCount = 999;
33
+
34
+ expect(payload.model).toBe('gpt-4');
35
+ expect(payload.messagesCount).toBe(5);
36
+ });
37
+
38
+ it('should preserve all fields including optional ones', () => {
39
+ const payload: IOpenAILogData = {
40
+ model: 'gpt-4o',
41
+ messagesCount: 1,
42
+ hasTools: true,
43
+ temperature: 0.5,
44
+ maxTokens: 500,
45
+ timestamp: '2026-03-10T12:00:00.000Z',
46
+ requestId: 'req-123',
47
+ };
48
+
49
+ const result = sanitizeOpenAILogData(payload);
50
+
51
+ expect(result.model).toBe('gpt-4o');
52
+ expect(result.messagesCount).toBe(1);
53
+ expect(result.hasTools).toBe(true);
54
+ expect(result.temperature).toBe(0.5);
55
+ expect(result.maxTokens).toBe(500);
56
+ expect(result.timestamp).toBe('2026-03-10T12:00:00.000Z');
57
+ expect(result.requestId).toBe('req-123');
58
+ });
59
+
60
+ it('should handle payload with undefined optional fields', () => {
61
+ const payload: IOpenAILogData = {
62
+ model: 'gpt-4',
63
+ messagesCount: 2,
64
+ hasTools: false,
65
+ timestamp: '2026-01-01T00:00:00.000Z',
66
+ temperature: undefined,
67
+ maxTokens: undefined,
68
+ };
69
+
70
+ const result = sanitizeOpenAILogData(payload);
71
+
72
+ // JSON.parse(JSON.stringify(...)) strips undefined fields
73
+ expect(result.model).toBe('gpt-4');
74
+ expect(result.messagesCount).toBe(2);
75
+ expect(result.hasTools).toBe(false);
76
+ });
77
+
78
+ it('should handle minimal payload', () => {
79
+ const payload: IOpenAILogData = {
80
+ model: 'gpt-3.5-turbo',
81
+ messagesCount: 0,
82
+ hasTools: false,
83
+ timestamp: '',
84
+ };
85
+
86
+ const result = sanitizeOpenAILogData(payload);
87
+ expect(result).toEqual(payload);
88
+ });
89
+ });
@@ -0,0 +1,14 @@
1
+ import type { IOpenAILogData } from '../types/api-types';
2
+
3
+ /**
4
+ * Creates a defensive deep copy of OpenAI log data.
5
+ * SSOT utility shared by payload loggers.
6
+ */
7
+ export function sanitizeOpenAILogData(payload: IOpenAILogData): IOpenAILogData {
8
+ // Create a deep copy to avoid modifying original
9
+ const sanitized = JSON.parse(JSON.stringify(payload)) as IOpenAILogData;
10
+
11
+ // Remove or mask sensitive data if needed.
12
+ // For now, we keep everything as OpenAI payloads don't contain API keys.
13
+ return sanitized;
14
+ }
@@ -0,0 +1,23 @@
1
+ import {
2
+ convertToOpenAICompatibleMessages,
3
+ convertToOpenAICompatibleTools,
4
+ } from '@robota-sdk/agent-provider-openai-compatible/shared';
5
+
6
+ import type { IToolSchema, TUniversalMessage } from '@robota-sdk/agent-core';
7
+ import type OpenAI from 'openai';
8
+
9
+ /**
10
+ * Convert TUniversalMessage array to OpenAI chat completion message format.
11
+ */
12
+ export function convertToOpenAIMessages(
13
+ messages: TUniversalMessage[],
14
+ ): OpenAI.Chat.ChatCompletionMessageParam[] {
15
+ return convertToOpenAICompatibleMessages(messages);
16
+ }
17
+
18
+ /**
19
+ * Convert tool schemas to OpenAI function tool format.
20
+ */
21
+ export function convertToOpenAITools(tools: IToolSchema[]): OpenAI.Chat.ChatCompletionTool[] {
22
+ return convertToOpenAICompatibleTools(tools);
23
+ }
@@ -0,0 +1,19 @@
1
+ import type { IProviderModelEffortTable } from '@robota-sdk/agent-core';
2
+
3
+ /**
4
+ * Exact model facts verified against OpenAI's Responses reference on 2026-09-11.
5
+ *
6
+ * The adapter deliberately declares no prefix or vendor-wide fallback. A different model id must
7
+ * remain `not-applied` until its documented controls and default are added here.
8
+ */
9
+ export const OPENAI_MODEL_EFFORT_TABLE: IProviderModelEffortTable = {
10
+ verifiedAt: '2026-09-11',
11
+ sourceUrl: 'https://platform.openai.com/docs/api-reference/responses',
12
+ models: {
13
+ 'gpt-5.1': {
14
+ supportedEfforts: ['none', 'low', 'medium', 'high'],
15
+ defaultEffort: 'none',
16
+ nativeControlId: 'responses.reasoning.effort',
17
+ },
18
+ },
19
+ };
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import {
4
+ loadVercelAiGatewayModelEffortConfig,
5
+ resolveVercelAiGatewayModelEffortConfig,
6
+ VERCEL_AI_GATEWAY_BASE_URL,
7
+ VERCEL_AI_GATEWAY_MODEL,
8
+ } from './model-effort-verification-config';
9
+
10
+ describe('resolveVercelAiGatewayModelEffortConfig', () => {
11
+ it('uses the Gateway credential while selecting the model itself', () => {
12
+ expect(resolveVercelAiGatewayModelEffortConfig({ AI_GATEWAY_API_KEY: 'test-key' })).toEqual({
13
+ apiKey: 'test-key',
14
+ baseURL: VERCEL_AI_GATEWAY_BASE_URL,
15
+ model: VERCEL_AI_GATEWAY_MODEL,
16
+ });
17
+ });
18
+
19
+ it('rejects a missing Gateway credential without accepting a user-supplied model', () => {
20
+ expect(() => resolveVercelAiGatewayModelEffortConfig({})).toThrow(
21
+ 'AI_GATEWAY_API_KEY is required',
22
+ );
23
+ });
24
+
25
+ it('loads the ignored environment file before resolving the Gateway credential', () => {
26
+ const environment: Record<string, string | undefined> = {};
27
+ const loadEnvironmentFile = vi.fn(() => {
28
+ environment.AI_GATEWAY_API_KEY = 'test-key';
29
+ });
30
+
31
+ expect(
32
+ loadVercelAiGatewayModelEffortConfig(
33
+ environment,
34
+ '/workspace/.env.local',
35
+ () => true,
36
+ loadEnvironmentFile,
37
+ ),
38
+ ).toMatchObject({
39
+ apiKey: 'test-key',
40
+ baseURL: VERCEL_AI_GATEWAY_BASE_URL,
41
+ model: VERCEL_AI_GATEWAY_MODEL,
42
+ });
43
+ expect(loadEnvironmentFile).toHaveBeenCalledWith('/workspace/.env.local');
44
+ });
45
+ });