@robota-sdk/agent-provider 3.0.0-beta.64

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 (220) hide show
  1. package/LICENSE +21 -0
  2. package/dist/browser/index.d.ts +1104 -0
  3. package/dist/browser/index.d.ts.map +1 -0
  4. package/dist/browser/index.js +7 -0
  5. package/dist/browser/index.js.map +1 -0
  6. package/dist/loggers/index.cjs +1 -0
  7. package/dist/loggers/index.d.ts +151 -0
  8. package/dist/loggers/index.d.ts.map +1 -0
  9. package/dist/loggers/index.js +2 -0
  10. package/dist/loggers/index.js.map +1 -0
  11. package/dist/node/anthropic/index.cjs +1 -0
  12. package/dist/node/anthropic/index.d.ts +158 -0
  13. package/dist/node/anthropic/index.d.ts.map +1 -0
  14. package/dist/node/anthropic/index.js +1 -0
  15. package/dist/node/anthropic--1vgLC-e.js +5 -0
  16. package/dist/node/anthropic--1vgLC-e.js.map +1 -0
  17. package/dist/node/anthropic-BFQ6DSCP.cjs +4 -0
  18. package/dist/node/bytedance/index.cjs +1 -0
  19. package/dist/node/bytedance/index.d.ts +74 -0
  20. package/dist/node/bytedance/index.d.ts.map +1 -0
  21. package/dist/node/bytedance/index.js +1 -0
  22. package/dist/node/bytedance-C_0sF_pJ.js +2 -0
  23. package/dist/node/bytedance-C_0sF_pJ.js.map +1 -0
  24. package/dist/node/bytedance-DVPxqEiC.cjs +1 -0
  25. package/dist/node/chunk-Bmb41Sf3.cjs +1 -0
  26. package/dist/node/deepseek/index.cjs +1 -0
  27. package/dist/node/deepseek/index.d.ts +2 -0
  28. package/dist/node/deepseek/index.js +1 -0
  29. package/dist/node/deepseek-_8Ixx7rA.js +2 -0
  30. package/dist/node/deepseek-_8Ixx7rA.js.map +1 -0
  31. package/dist/node/deepseek-oA2Y6bD0.cjs +1 -0
  32. package/dist/node/gemini/index.cjs +1 -0
  33. package/dist/node/gemini/index.d.ts +173 -0
  34. package/dist/node/gemini/index.d.ts.map +1 -0
  35. package/dist/node/gemini/index.js +1 -0
  36. package/dist/node/gemini-Bh2U87MY.js +4 -0
  37. package/dist/node/gemini-Bh2U87MY.js.map +1 -0
  38. package/dist/node/gemini-DSaNCxZj.cjs +3 -0
  39. package/dist/node/gemma/index.cjs +1 -0
  40. package/dist/node/gemma/index.d.ts +2 -0
  41. package/dist/node/gemma/index.js +1 -0
  42. package/dist/node/gemma-Dp_AfCUR.js +2 -0
  43. package/dist/node/gemma-Dp_AfCUR.js.map +1 -0
  44. package/dist/node/gemma-G-Pf_PnX.cjs +1 -0
  45. package/dist/node/google/index.cjs +1 -0
  46. package/dist/node/google/index.d.ts +14 -0
  47. package/dist/node/google/index.d.ts.map +1 -0
  48. package/dist/node/google/index.js +2 -0
  49. package/dist/node/google/index.js.map +1 -0
  50. package/dist/node/index-B6PnlDMd.d.ts +82 -0
  51. package/dist/node/index-B6PnlDMd.d.ts.map +1 -0
  52. package/dist/node/index-B7UvPJcI.d.ts +315 -0
  53. package/dist/node/index-B7UvPJcI.d.ts.map +1 -0
  54. package/dist/node/index-BLPOTNb5.d.ts +98 -0
  55. package/dist/node/index-BLPOTNb5.d.ts.map +1 -0
  56. package/dist/node/index-BqixM_XD.d.ts +231 -0
  57. package/dist/node/index-BqixM_XD.d.ts.map +1 -0
  58. package/dist/node/index-C3beaqKO.d.ts +231 -0
  59. package/dist/node/index-C3beaqKO.d.ts.map +1 -0
  60. package/dist/node/index-Cp2XRh9G.d.ts +82 -0
  61. package/dist/node/index-Cp2XRh9G.d.ts.map +1 -0
  62. package/dist/node/index-DSv5xruI.d.ts +98 -0
  63. package/dist/node/index-DSv5xruI.d.ts.map +1 -0
  64. package/dist/node/index-w0bV1uaP.d.ts +315 -0
  65. package/dist/node/index-w0bV1uaP.d.ts.map +1 -0
  66. package/dist/node/index.cjs +1 -0
  67. package/dist/node/index.d.ts +8 -0
  68. package/dist/node/index.js +1 -0
  69. package/dist/node/openai/index.cjs +1 -0
  70. package/dist/node/openai/index.d.ts +2 -0
  71. package/dist/node/openai/index.js +1 -0
  72. package/dist/node/openai-CRQjg4xF.js +2 -0
  73. package/dist/node/openai-CRQjg4xF.js.map +1 -0
  74. package/dist/node/openai-compatible-BYfyY5lb.cjs +1 -0
  75. package/dist/node/openai-compatible-Dm4Sof9e.js +2 -0
  76. package/dist/node/openai-compatible-Dm4Sof9e.js.map +1 -0
  77. package/dist/node/openai-xWC6pY7r.cjs +1 -0
  78. package/dist/node/qwen/index.cjs +1 -0
  79. package/dist/node/qwen/index.d.ts +2 -0
  80. package/dist/node/qwen/index.js +1 -0
  81. package/dist/node/qwen-ChUZobTL.js +2 -0
  82. package/dist/node/qwen-ChUZobTL.js.map +1 -0
  83. package/dist/node/qwen-CjT71vSM.cjs +1 -0
  84. package/package.json +157 -0
  85. package/src/anthropic/__tests__/abort-streaming.test.ts +199 -0
  86. package/src/anthropic/__tests__/model-catalog-refresh.test.ts +92 -0
  87. package/src/anthropic/__tests__/provider-definition.test.ts +55 -0
  88. package/src/anthropic/__tests__/provider.test.ts +1357 -0
  89. package/src/anthropic/__tests__/response-parser.test.ts +326 -0
  90. package/src/anthropic/index.ts +22 -0
  91. package/src/anthropic/message-converter.ts +181 -0
  92. package/src/anthropic/model-catalog-refresh.ts +128 -0
  93. package/src/anthropic/parsers/response-parser.ts +184 -0
  94. package/src/anthropic/provider-definition.ts +93 -0
  95. package/src/anthropic/provider.ts +290 -0
  96. package/src/anthropic/streaming-handler.ts +204 -0
  97. package/src/anthropic/types/api-types.ts +158 -0
  98. package/src/anthropic/types.ts +79 -0
  99. package/src/bytedance/http-client.test.ts +288 -0
  100. package/src/bytedance/http-client.ts +163 -0
  101. package/src/bytedance/index.ts +2 -0
  102. package/src/bytedance/provider.spec.ts +320 -0
  103. package/src/bytedance/provider.ts +171 -0
  104. package/src/bytedance/status-mapper.test.ts +299 -0
  105. package/src/bytedance/status-mapper.ts +141 -0
  106. package/src/bytedance/types.ts +68 -0
  107. package/src/deepseek/defaults.ts +4 -0
  108. package/src/deepseek/index.ts +22 -0
  109. package/src/deepseek/model-catalog-refresh.test.ts +57 -0
  110. package/src/deepseek/model-catalog-refresh.ts +105 -0
  111. package/src/deepseek/model-catalog.ts +55 -0
  112. package/src/deepseek/provider-definition.test.ts +109 -0
  113. package/src/deepseek/provider-definition.ts +132 -0
  114. package/src/deepseek/provider.test.ts +324 -0
  115. package/src/deepseek/provider.ts +298 -0
  116. package/src/deepseek/types.ts +37 -0
  117. package/src/gemini/execution-helpers.ts +233 -0
  118. package/src/gemini/genai-transport.test.ts +208 -0
  119. package/src/gemini/image-operations.test.ts +448 -0
  120. package/src/gemini/image-operations.ts +261 -0
  121. package/src/gemini/index.ts +11 -0
  122. package/src/gemini/message-converter.test.ts +616 -0
  123. package/src/gemini/message-converter.ts +140 -0
  124. package/src/gemini/model-catalog-refresh.test.ts +107 -0
  125. package/src/gemini/model-catalog-refresh.ts +92 -0
  126. package/src/gemini/provider-definition.test.ts +70 -0
  127. package/src/gemini/provider-definition.ts +78 -0
  128. package/src/gemini/provider-extended.test.ts +898 -0
  129. package/src/gemini/provider.spec.ts +216 -0
  130. package/src/gemini/provider.ts +279 -0
  131. package/src/gemini/request-converter.ts +226 -0
  132. package/src/gemini/tool-schema-converter.ts +78 -0
  133. package/src/gemini/types/api-types.ts +235 -0
  134. package/src/gemini/types.ts +121 -0
  135. package/src/gemma/index.ts +5 -0
  136. package/src/gemma/message-factory.ts +38 -0
  137. package/src/gemma/provider-definition.test.ts +43 -0
  138. package/src/gemma/provider-definition.ts +84 -0
  139. package/src/gemma/provider-projection.ts +49 -0
  140. package/src/gemma/provider.test.ts +628 -0
  141. package/src/gemma/provider.ts +308 -0
  142. package/src/gemma/pseudo-command-envelope.ts +58 -0
  143. package/src/gemma/pseudo-tool-call-projector.ts +243 -0
  144. package/src/gemma/pseudo-tool-call-tag-parser.ts +153 -0
  145. package/src/gemma/pseudo-tool-call-types.ts +31 -0
  146. package/src/gemma/reasoning-projector.test.ts +52 -0
  147. package/src/gemma/reasoning-projector.ts +144 -0
  148. package/src/gemma/streaming-projection.ts +79 -0
  149. package/src/gemma/tool-call-argument-parser.ts +126 -0
  150. package/src/gemma/tool-call-projector.test.ts +227 -0
  151. package/src/gemma/tool-call-projector.ts +264 -0
  152. package/src/gemma/types.ts +27 -0
  153. package/src/google/index.ts +11 -0
  154. package/src/google/provider-compat.test.ts +19 -0
  155. package/src/google/provider-definition.ts +6 -0
  156. package/src/google/provider.ts +10 -0
  157. package/src/google/types.ts +5 -0
  158. package/src/index.ts +9 -0
  159. package/src/openai/adapter.test.ts +494 -0
  160. package/src/openai/adapter.ts +145 -0
  161. package/src/openai/chat-completions-chat.ts +189 -0
  162. package/src/openai/executor-integration.test.ts +206 -0
  163. package/src/openai/index.ts +21 -0
  164. package/src/openai/interfaces/payload-logger.ts +48 -0
  165. package/src/openai/loggers/console-payload-logger.test.ts +173 -0
  166. package/src/openai/loggers/console-payload-logger.ts +94 -0
  167. package/src/openai/loggers/console.ts +9 -0
  168. package/src/openai/loggers/file-payload-logger.test.ts +238 -0
  169. package/src/openai/loggers/file-payload-logger.ts +112 -0
  170. package/src/openai/loggers/file.ts +9 -0
  171. package/src/openai/loggers/index.ts +12 -0
  172. package/src/openai/loggers/sanitize-openai-log-data.test.ts +89 -0
  173. package/src/openai/loggers/sanitize-openai-log-data.ts +14 -0
  174. package/src/openai/message-converter.ts +22 -0
  175. package/src/openai/model-catalog-refresh.test.ts +92 -0
  176. package/src/openai/model-catalog-refresh.ts +115 -0
  177. package/src/openai/openai-request-format.ts +92 -0
  178. package/src/openai/parsers/response-parser.test.ts +407 -0
  179. package/src/openai/parsers/response-parser.ts +47 -0
  180. package/src/openai/provider-definition.test.ts +75 -0
  181. package/src/openai/provider-definition.ts +132 -0
  182. package/src/openai/provider.test.ts +1402 -0
  183. package/src/openai/provider.ts +237 -0
  184. package/src/openai/responses-chat.ts +258 -0
  185. package/src/openai/responses-converter.ts +112 -0
  186. package/src/openai/responses-parser.ts +285 -0
  187. package/src/openai/responses-stream-utils.ts +45 -0
  188. package/src/openai/responses-types.ts +195 -0
  189. package/src/openai/streaming/stream-assembler.ts +3 -0
  190. package/src/openai/streaming/stream-handler.test.ts +367 -0
  191. package/src/openai/streaming/stream-handler.ts +119 -0
  192. package/src/openai/types/api-types.ts +112 -0
  193. package/src/openai/types.ts +194 -0
  194. package/src/qwen/defaults.ts +26 -0
  195. package/src/qwen/index.ts +5 -0
  196. package/src/qwen/model-catalog-refresh.test.ts +91 -0
  197. package/src/qwen/model-catalog-refresh.ts +97 -0
  198. package/src/qwen/provider-capabilities.ts +34 -0
  199. package/src/qwen/provider-definition.test.ts +139 -0
  200. package/src/qwen/provider-definition.ts +173 -0
  201. package/src/qwen/provider-streaming-assembly.ts +40 -0
  202. package/src/qwen/provider.test.ts +640 -0
  203. package/src/qwen/provider.ts +293 -0
  204. package/src/qwen/responses-chat.ts +194 -0
  205. package/src/qwen/responses-converter.ts +104 -0
  206. package/src/qwen/responses-parser.ts +299 -0
  207. package/src/qwen/responses-stream-utils.ts +38 -0
  208. package/src/qwen/types.ts +228 -0
  209. package/src/shared/openai-compatible/endpoint-probe.test.ts +52 -0
  210. package/src/shared/openai-compatible/endpoint-probe.ts +43 -0
  211. package/src/shared/openai-compatible/index.ts +6 -0
  212. package/src/shared/openai-compatible/message-converter.test.ts +111 -0
  213. package/src/shared/openai-compatible/message-converter.ts +84 -0
  214. package/src/shared/openai-compatible/native-payload-observer.test.ts +43 -0
  215. package/src/shared/openai-compatible/native-payload-observer.ts +26 -0
  216. package/src/shared/openai-compatible/response-parser.test.ts +172 -0
  217. package/src/shared/openai-compatible/response-parser.ts +180 -0
  218. package/src/shared/openai-compatible/stream-assembler.test.ts +266 -0
  219. package/src/shared/openai-compatible/stream-assembler.ts +248 -0
  220. package/src/shared/openai-compatible/types.ts +59 -0
@@ -0,0 +1,94 @@
1
+ import type { IPayloadLogger, IPayloadLoggerOptions } from '../interfaces/payload-logger';
2
+ import type { IOpenAILogData } from '../types/api-types';
3
+ import { SilentLogger, type ILogger } from '@robota-sdk/agent-core';
4
+ import { sanitizeOpenAILogData } from './sanitize-openai-log-data';
5
+
6
+ /**
7
+ * Console-based payload logger for browser environments
8
+ *
9
+ * This logger outputs API request/response payloads to the browser console
10
+ * using structured logging. It's designed specifically for browser environments
11
+ * and development/debugging scenarios.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { ConsolePayloadLogger } from '@robota-sdk/agent-provider/openai/loggers';
16
+ *
17
+ * const logger = new ConsolePayloadLogger({
18
+ * enabled: true,
19
+ * includeTimestamp: true
20
+ * });
21
+ *
22
+ * const provider = new OpenAIProvider({
23
+ * client: openaiClient,
24
+ * payloadLogger: logger
25
+ * });
26
+ * ```
27
+ */
28
+ export class ConsolePayloadLogger implements IPayloadLogger {
29
+ private readonly enabled: boolean;
30
+ private readonly includeTimestamp: boolean;
31
+ private readonly logger: ILogger;
32
+
33
+ constructor(options: IPayloadLoggerOptions = {}) {
34
+ this.enabled = options.enabled ?? true;
35
+ this.includeTimestamp = options.includeTimestamp ?? true;
36
+ this.logger = options.logger || SilentLogger;
37
+ }
38
+
39
+ /**
40
+ * Check if logging is enabled
41
+ */
42
+ isEnabled(): boolean {
43
+ return this.enabled;
44
+ }
45
+
46
+ /**
47
+ * Log API payload to browser console
48
+ * @param payload - The API request payload
49
+ * @param type - Type of request ('chat' or 'stream')
50
+ */
51
+ async logPayload(payload: IOpenAILogData, type: 'chat' | 'stream' = 'chat'): Promise<void> {
52
+ if (!this.enabled) {
53
+ return;
54
+ }
55
+
56
+ try {
57
+ const sanitizedPayload = sanitizeOpenAILogData(payload);
58
+
59
+ // Use structured console logging for better browser developer tools integration
60
+ const title = `[OpenAI ${type.toUpperCase()}] API Payload`;
61
+ const timeInfo = this.includeTimestamp ? ` (${sanitizedPayload.timestamp})` : '';
62
+
63
+ // Group related log entries for better organization
64
+ this.logger.group?.(`${title}${timeInfo}`);
65
+
66
+ // Log different aspects with appropriate console methods
67
+ this.logger.info('📋 Request Details:', {
68
+ model: payload.model,
69
+ messagesCount: payload.messagesCount,
70
+ hasTools: payload.hasTools,
71
+ temperature: payload.temperature,
72
+ maxTokens: payload.maxTokens,
73
+ });
74
+
75
+ this.logger.debug('🔍 Full Payload:', { type, provider: 'openai', ...sanitizedPayload });
76
+
77
+ this.logger.groupEnd?.();
78
+ } catch (error) {
79
+ // Don't throw errors - just log them and continue
80
+ // This ensures that API logging failures don't break the main functionality
81
+ this.logger.error(
82
+ '[ConsolePayloadLogger] Failed to log payload:',
83
+ error instanceof Error ? error.message : 'Unknown error',
84
+ );
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Sanitize payload to remove sensitive information
90
+ * @param payload - Raw payload object
91
+ * @returns Sanitized payload
92
+ */
93
+ // Sanitization intentionally lives in ./sanitize-openai-log-data.ts (SSOT utility).
94
+ }
@@ -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,238 @@
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
+ function createMockLogger(): ILogger {
19
+ return {
20
+ info: vi.fn(),
21
+ warn: vi.fn(),
22
+ error: vi.fn(),
23
+ debug: vi.fn(),
24
+ log: vi.fn(),
25
+ };
26
+ }
27
+
28
+ function createSamplePayload(): IOpenAILogData {
29
+ return {
30
+ model: 'gpt-4',
31
+ messagesCount: 3,
32
+ hasTools: true,
33
+ temperature: 0.7,
34
+ maxTokens: 1000,
35
+ timestamp: '2026-01-01T00:00:00.000Z',
36
+ };
37
+ }
38
+
39
+ describe('FilePayloadLogger', () => {
40
+ beforeEach(() => {
41
+ vi.clearAllMocks();
42
+ // Default: directory exists
43
+ vi.mocked(fs.existsSync).mockReturnValue(true);
44
+ vi.mocked(fs.promises.writeFile).mockResolvedValue(undefined);
45
+ });
46
+
47
+ describe('constructor', () => {
48
+ it('should create logger with required logDir option', () => {
49
+ const logger = new FilePayloadLogger({ logDir: '/tmp/logs' });
50
+ expect(logger.isEnabled()).toBe(true);
51
+ });
52
+
53
+ it('should default to enabled when not specified', () => {
54
+ const logger = new FilePayloadLogger({ logDir: '/tmp/logs' });
55
+ expect(logger.isEnabled()).toBe(true);
56
+ });
57
+
58
+ it('should respect enabled false option', () => {
59
+ const logger = new FilePayloadLogger({ logDir: '/tmp/logs', enabled: false });
60
+ expect(logger.isEnabled()).toBe(false);
61
+ });
62
+
63
+ it('should create log directory if it does not exist and enabled', () => {
64
+ vi.mocked(fs.existsSync).mockReturnValue(false);
65
+ new FilePayloadLogger({ logDir: '/tmp/logs' });
66
+
67
+ expect(fs.mkdirSync).toHaveBeenCalledWith('/tmp/logs', { recursive: true });
68
+ });
69
+
70
+ it('should not create log directory when disabled', () => {
71
+ vi.mocked(fs.existsSync).mockReturnValue(false);
72
+ new FilePayloadLogger({ logDir: '/tmp/logs', enabled: false });
73
+
74
+ expect(fs.mkdirSync).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it('should not create log directory if it already exists', () => {
78
+ vi.mocked(fs.existsSync).mockReturnValue(true);
79
+ new FilePayloadLogger({ logDir: '/tmp/logs' });
80
+
81
+ expect(fs.mkdirSync).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it('should handle directory creation error gracefully', () => {
85
+ const mockLogger = createMockLogger();
86
+ vi.mocked(fs.existsSync).mockReturnValue(false);
87
+ vi.mocked(fs.mkdirSync).mockImplementation(() => {
88
+ throw new Error('Permission denied');
89
+ });
90
+
91
+ // Should not throw
92
+ const logger = new FilePayloadLogger({ logDir: '/root/logs', logger: mockLogger });
93
+ expect(logger.isEnabled()).toBe(true);
94
+ expect(mockLogger.error).toHaveBeenCalledWith(
95
+ expect.stringContaining('[FilePayloadLogger]'),
96
+ expect.objectContaining({ error: 'Permission denied' }),
97
+ );
98
+ });
99
+ });
100
+
101
+ describe('isEnabled', () => {
102
+ it('should return true when enabled', () => {
103
+ const logger = new FilePayloadLogger({ logDir: '/tmp/logs', enabled: true });
104
+ expect(logger.isEnabled()).toBe(true);
105
+ });
106
+
107
+ it('should return false when disabled', () => {
108
+ const logger = new FilePayloadLogger({ logDir: '/tmp/logs', enabled: false });
109
+ expect(logger.isEnabled()).toBe(false);
110
+ });
111
+ });
112
+
113
+ describe('logPayload', () => {
114
+ it('should write payload to a JSON file', async () => {
115
+ const logger = new FilePayloadLogger({ logDir: '/tmp/logs' });
116
+ const payload = createSamplePayload();
117
+
118
+ await logger.logPayload(payload, 'chat');
119
+
120
+ expect(fs.promises.writeFile).toHaveBeenCalledWith(
121
+ expect.stringContaining('/tmp/logs/openai-chat-'),
122
+ expect.any(String),
123
+ 'utf8',
124
+ );
125
+ });
126
+
127
+ it('should write valid JSON content', async () => {
128
+ const logger = new FilePayloadLogger({ logDir: '/tmp/logs' });
129
+ const payload = createSamplePayload();
130
+
131
+ await logger.logPayload(payload, 'chat');
132
+
133
+ const writeCall = vi.mocked(fs.promises.writeFile).mock.calls[0];
134
+ const content = writeCall[1] as string;
135
+ const parsed = JSON.parse(content);
136
+
137
+ expect(parsed.type).toBe('chat');
138
+ expect(parsed.provider).toBe('openai');
139
+ expect(parsed.payload.model).toBe('gpt-4');
140
+ expect(parsed.timestamp).toBeDefined();
141
+ });
142
+
143
+ it('should use stream type in filename when type is stream', async () => {
144
+ const logger = new FilePayloadLogger({ logDir: '/tmp/logs' });
145
+ const payload = createSamplePayload();
146
+
147
+ await logger.logPayload(payload, 'stream');
148
+
149
+ const writeCall = vi.mocked(fs.promises.writeFile).mock.calls[0];
150
+ const filepath = writeCall[0] as string;
151
+ expect(filepath).toContain('openai-stream-');
152
+ });
153
+
154
+ it('should include timestamp in filename when includeTimestamp is true', async () => {
155
+ const logger = new FilePayloadLogger({
156
+ logDir: '/tmp/logs',
157
+ includeTimestamp: true,
158
+ });
159
+ const payload = createSamplePayload();
160
+
161
+ await logger.logPayload(payload, 'chat');
162
+
163
+ const writeCall = vi.mocked(fs.promises.writeFile).mock.calls[0];
164
+ const filepath = writeCall[0] as string;
165
+ // Timestamp format replaces : and . with -
166
+ expect(filepath).toMatch(/openai-chat-\d{4}-\d{2}-\d{2}T/);
167
+ });
168
+
169
+ it('should use Date.now() in filename when includeTimestamp is false', async () => {
170
+ const logger = new FilePayloadLogger({
171
+ logDir: '/tmp/logs',
172
+ includeTimestamp: false,
173
+ });
174
+ const payload = createSamplePayload();
175
+
176
+ await logger.logPayload(payload, 'chat');
177
+
178
+ const writeCall = vi.mocked(fs.promises.writeFile).mock.calls[0];
179
+ const filepath = writeCall[0] as string;
180
+ // Should contain a numeric timestamp
181
+ expect(filepath).toMatch(/openai-chat-\d+\.json$/);
182
+ });
183
+
184
+ it('should skip logging when disabled', async () => {
185
+ const logger = new FilePayloadLogger({ logDir: '/tmp/logs', enabled: false });
186
+ const payload = createSamplePayload();
187
+
188
+ await logger.logPayload(payload, 'chat');
189
+
190
+ expect(fs.promises.writeFile).not.toHaveBeenCalled();
191
+ });
192
+
193
+ it('should not throw on write error and log the error', async () => {
194
+ const mockLogger = createMockLogger();
195
+ vi.mocked(fs.promises.writeFile).mockRejectedValue(new Error('Disk full'));
196
+
197
+ const logger = new FilePayloadLogger({ logDir: '/tmp/logs', logger: mockLogger });
198
+ const payload = createSamplePayload();
199
+
200
+ // Should not throw
201
+ await expect(logger.logPayload(payload, 'chat')).resolves.toBeUndefined();
202
+ expect(mockLogger.error).toHaveBeenCalledWith(
203
+ expect.stringContaining('[FilePayloadLogger]'),
204
+ expect.objectContaining({ error: 'Disk full' }),
205
+ );
206
+ });
207
+
208
+ it('should sanitize payload before writing', async () => {
209
+ const logger = new FilePayloadLogger({ logDir: '/tmp/logs' });
210
+ const payload = createSamplePayload();
211
+
212
+ await logger.logPayload(payload, 'chat');
213
+
214
+ const writeCall = vi.mocked(fs.promises.writeFile).mock.calls[0];
215
+ const content = writeCall[1] as string;
216
+ const parsed = JSON.parse(content);
217
+
218
+ // Verify the payload is a sanitized copy
219
+ expect(parsed.payload).toEqual(
220
+ expect.objectContaining({
221
+ model: 'gpt-4',
222
+ messagesCount: 3,
223
+ }),
224
+ );
225
+ });
226
+
227
+ it('should default type to chat', async () => {
228
+ const logger = new FilePayloadLogger({ logDir: '/tmp/logs' });
229
+ const payload = createSamplePayload();
230
+
231
+ await logger.logPayload(payload, 'chat');
232
+
233
+ const writeCall = vi.mocked(fs.promises.writeFile).mock.calls[0];
234
+ const filepath = writeCall[0] as string;
235
+ expect(filepath).toContain('openai-chat-');
236
+ });
237
+ });
238
+ });
@@ -0,0 +1,112 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import type { IPayloadLogger } from '../interfaces/payload-logger';
4
+ import type { IOpenAILogData } from '../types/api-types';
5
+ import type { ILogger } from '@robota-sdk/agent-core';
6
+ import { SilentLogger } from '@robota-sdk/agent-core';
7
+ import { sanitizeOpenAILogData } from './sanitize-openai-log-data';
8
+
9
+ /**
10
+ * File-based payload logger for Node.js environments
11
+ *
12
+ * This logger saves API request/response payloads to JSON files on disk.
13
+ * It's designed specifically for Node.js environments with filesystem access.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { FilePayloadLogger } from '@robota-sdk/agent-provider/openai/loggers';
18
+ *
19
+ * const logger = new FilePayloadLogger({
20
+ * logDir: './logs/api-payloads',
21
+ * enabled: true,
22
+ * includeTimestamp: true
23
+ * });
24
+ *
25
+ * const provider = new OpenAIProvider({
26
+ * client: openaiClient,
27
+ * payloadLogger: logger
28
+ * });
29
+ * ```
30
+ */
31
+ export class FilePayloadLogger implements IPayloadLogger {
32
+ private readonly enabled: boolean;
33
+ private readonly logDir: string;
34
+ private readonly includeTimestamp: boolean;
35
+ private readonly logger: ILogger;
36
+
37
+ constructor(options: {
38
+ logDir: string;
39
+ enabled?: boolean;
40
+ includeTimestamp?: boolean;
41
+ logger?: ILogger;
42
+ }) {
43
+ this.enabled = options.enabled ?? true;
44
+ this.logDir = options.logDir;
45
+ this.includeTimestamp = options.includeTimestamp ?? true;
46
+ this.logger = options.logger || SilentLogger;
47
+
48
+ if (this.enabled) {
49
+ this.ensureLogDirectoryExists();
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Check if logging is enabled
55
+ */
56
+ isEnabled(): boolean {
57
+ return this.enabled;
58
+ }
59
+
60
+ /**
61
+ * Log API payload to file
62
+ * @param payload - The API request payload
63
+ * @param type - Type of request ('chat' or 'stream')
64
+ */
65
+ async logPayload(payload: IOpenAILogData, type: 'chat' | 'stream' = 'chat'): Promise<void> {
66
+ if (!this.enabled) {
67
+ return;
68
+ }
69
+
70
+ try {
71
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
72
+ const filename = this.includeTimestamp
73
+ ? `openai-${type}-${timestamp}.json`
74
+ : `openai-${type}-${Date.now()}.json`;
75
+
76
+ const filepath = path.join(this.logDir, filename);
77
+
78
+ const logData = {
79
+ timestamp: new Date().toISOString(),
80
+ type,
81
+ provider: 'openai',
82
+ payload: sanitizeOpenAILogData(payload),
83
+ };
84
+
85
+ await fs.promises.writeFile(filepath, JSON.stringify(logData, null, 2), 'utf8');
86
+
87
+ // Payload saved successfully (silent operation)
88
+ } catch (error) {
89
+ // Don't throw errors - just log them and continue
90
+ // This ensures that API logging failures don't break the main functionality
91
+ this.logger.error('[FilePayloadLogger] Failed to save payload log:', {
92
+ error: error instanceof Error ? error.message : String(error),
93
+ });
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Ensure log directory exists
99
+ */
100
+ private ensureLogDirectoryExists(): void {
101
+ try {
102
+ if (!fs.existsSync(this.logDir)) {
103
+ fs.mkdirSync(this.logDir, { recursive: true });
104
+ }
105
+ } catch (error) {
106
+ this.logger.error('[FilePayloadLogger] Failed to create log directory:', {
107
+ error: error instanceof Error ? error.message : String(error),
108
+ });
109
+ }
110
+ }
111
+ // Sanitization intentionally lives in ./sanitize-openai-log-data.ts (SSOT utility).
112
+ }
@@ -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,22 @@
1
+ import type OpenAI from 'openai';
2
+ import type { IToolSchema, TUniversalMessage } from '@robota-sdk/agent-core';
3
+ import {
4
+ convertToOpenAICompatibleMessages,
5
+ convertToOpenAICompatibleTools,
6
+ } from '../shared/openai-compatible/index.js';
7
+
8
+ /**
9
+ * Convert TUniversalMessage array to OpenAI chat completion message format.
10
+ */
11
+ export function convertToOpenAIMessages(
12
+ messages: TUniversalMessage[],
13
+ ): OpenAI.Chat.ChatCompletionMessageParam[] {
14
+ return convertToOpenAICompatibleMessages(messages);
15
+ }
16
+
17
+ /**
18
+ * Convert tool schemas to OpenAI function tool format.
19
+ */
20
+ export function convertToOpenAITools(tools: IToolSchema[]): OpenAI.Chat.ChatCompletionTool[] {
21
+ return convertToOpenAICompatibleTools(tools);
22
+ }