@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.
- package/LICENSE +21 -0
- package/dist/browser/index.d.ts +1104 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +7 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/loggers/index.cjs +1 -0
- package/dist/loggers/index.d.ts +151 -0
- package/dist/loggers/index.d.ts.map +1 -0
- package/dist/loggers/index.js +2 -0
- package/dist/loggers/index.js.map +1 -0
- package/dist/node/anthropic/index.cjs +1 -0
- package/dist/node/anthropic/index.d.ts +158 -0
- package/dist/node/anthropic/index.d.ts.map +1 -0
- package/dist/node/anthropic/index.js +1 -0
- package/dist/node/anthropic--1vgLC-e.js +5 -0
- package/dist/node/anthropic--1vgLC-e.js.map +1 -0
- package/dist/node/anthropic-BFQ6DSCP.cjs +4 -0
- package/dist/node/bytedance/index.cjs +1 -0
- package/dist/node/bytedance/index.d.ts +74 -0
- package/dist/node/bytedance/index.d.ts.map +1 -0
- package/dist/node/bytedance/index.js +1 -0
- package/dist/node/bytedance-C_0sF_pJ.js +2 -0
- package/dist/node/bytedance-C_0sF_pJ.js.map +1 -0
- package/dist/node/bytedance-DVPxqEiC.cjs +1 -0
- package/dist/node/chunk-Bmb41Sf3.cjs +1 -0
- package/dist/node/deepseek/index.cjs +1 -0
- package/dist/node/deepseek/index.d.ts +2 -0
- package/dist/node/deepseek/index.js +1 -0
- package/dist/node/deepseek-_8Ixx7rA.js +2 -0
- package/dist/node/deepseek-_8Ixx7rA.js.map +1 -0
- package/dist/node/deepseek-oA2Y6bD0.cjs +1 -0
- package/dist/node/gemini/index.cjs +1 -0
- package/dist/node/gemini/index.d.ts +173 -0
- package/dist/node/gemini/index.d.ts.map +1 -0
- package/dist/node/gemini/index.js +1 -0
- package/dist/node/gemini-Bh2U87MY.js +4 -0
- package/dist/node/gemini-Bh2U87MY.js.map +1 -0
- package/dist/node/gemini-DSaNCxZj.cjs +3 -0
- package/dist/node/gemma/index.cjs +1 -0
- package/dist/node/gemma/index.d.ts +2 -0
- package/dist/node/gemma/index.js +1 -0
- package/dist/node/gemma-Dp_AfCUR.js +2 -0
- package/dist/node/gemma-Dp_AfCUR.js.map +1 -0
- package/dist/node/gemma-G-Pf_PnX.cjs +1 -0
- package/dist/node/google/index.cjs +1 -0
- package/dist/node/google/index.d.ts +14 -0
- package/dist/node/google/index.d.ts.map +1 -0
- package/dist/node/google/index.js +2 -0
- package/dist/node/google/index.js.map +1 -0
- package/dist/node/index-B6PnlDMd.d.ts +82 -0
- package/dist/node/index-B6PnlDMd.d.ts.map +1 -0
- package/dist/node/index-B7UvPJcI.d.ts +315 -0
- package/dist/node/index-B7UvPJcI.d.ts.map +1 -0
- package/dist/node/index-BLPOTNb5.d.ts +98 -0
- package/dist/node/index-BLPOTNb5.d.ts.map +1 -0
- package/dist/node/index-BqixM_XD.d.ts +231 -0
- package/dist/node/index-BqixM_XD.d.ts.map +1 -0
- package/dist/node/index-C3beaqKO.d.ts +231 -0
- package/dist/node/index-C3beaqKO.d.ts.map +1 -0
- package/dist/node/index-Cp2XRh9G.d.ts +82 -0
- package/dist/node/index-Cp2XRh9G.d.ts.map +1 -0
- package/dist/node/index-DSv5xruI.d.ts +98 -0
- package/dist/node/index-DSv5xruI.d.ts.map +1 -0
- package/dist/node/index-w0bV1uaP.d.ts +315 -0
- package/dist/node/index-w0bV1uaP.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.ts +8 -0
- package/dist/node/index.js +1 -0
- package/dist/node/openai/index.cjs +1 -0
- package/dist/node/openai/index.d.ts +2 -0
- package/dist/node/openai/index.js +1 -0
- package/dist/node/openai-CRQjg4xF.js +2 -0
- package/dist/node/openai-CRQjg4xF.js.map +1 -0
- package/dist/node/openai-compatible-BYfyY5lb.cjs +1 -0
- package/dist/node/openai-compatible-Dm4Sof9e.js +2 -0
- package/dist/node/openai-compatible-Dm4Sof9e.js.map +1 -0
- package/dist/node/openai-xWC6pY7r.cjs +1 -0
- package/dist/node/qwen/index.cjs +1 -0
- package/dist/node/qwen/index.d.ts +2 -0
- package/dist/node/qwen/index.js +1 -0
- package/dist/node/qwen-ChUZobTL.js +2 -0
- package/dist/node/qwen-ChUZobTL.js.map +1 -0
- package/dist/node/qwen-CjT71vSM.cjs +1 -0
- package/package.json +157 -0
- package/src/anthropic/__tests__/abort-streaming.test.ts +199 -0
- package/src/anthropic/__tests__/model-catalog-refresh.test.ts +92 -0
- package/src/anthropic/__tests__/provider-definition.test.ts +55 -0
- package/src/anthropic/__tests__/provider.test.ts +1357 -0
- package/src/anthropic/__tests__/response-parser.test.ts +326 -0
- package/src/anthropic/index.ts +22 -0
- package/src/anthropic/message-converter.ts +181 -0
- package/src/anthropic/model-catalog-refresh.ts +128 -0
- package/src/anthropic/parsers/response-parser.ts +184 -0
- package/src/anthropic/provider-definition.ts +93 -0
- package/src/anthropic/provider.ts +290 -0
- package/src/anthropic/streaming-handler.ts +204 -0
- package/src/anthropic/types/api-types.ts +158 -0
- package/src/anthropic/types.ts +79 -0
- package/src/bytedance/http-client.test.ts +288 -0
- package/src/bytedance/http-client.ts +163 -0
- package/src/bytedance/index.ts +2 -0
- package/src/bytedance/provider.spec.ts +320 -0
- package/src/bytedance/provider.ts +171 -0
- package/src/bytedance/status-mapper.test.ts +299 -0
- package/src/bytedance/status-mapper.ts +141 -0
- package/src/bytedance/types.ts +68 -0
- package/src/deepseek/defaults.ts +4 -0
- package/src/deepseek/index.ts +22 -0
- package/src/deepseek/model-catalog-refresh.test.ts +57 -0
- package/src/deepseek/model-catalog-refresh.ts +105 -0
- package/src/deepseek/model-catalog.ts +55 -0
- package/src/deepseek/provider-definition.test.ts +109 -0
- package/src/deepseek/provider-definition.ts +132 -0
- package/src/deepseek/provider.test.ts +324 -0
- package/src/deepseek/provider.ts +298 -0
- package/src/deepseek/types.ts +37 -0
- package/src/gemini/execution-helpers.ts +233 -0
- package/src/gemini/genai-transport.test.ts +208 -0
- package/src/gemini/image-operations.test.ts +448 -0
- package/src/gemini/image-operations.ts +261 -0
- package/src/gemini/index.ts +11 -0
- package/src/gemini/message-converter.test.ts +616 -0
- package/src/gemini/message-converter.ts +140 -0
- package/src/gemini/model-catalog-refresh.test.ts +107 -0
- package/src/gemini/model-catalog-refresh.ts +92 -0
- package/src/gemini/provider-definition.test.ts +70 -0
- package/src/gemini/provider-definition.ts +78 -0
- package/src/gemini/provider-extended.test.ts +898 -0
- package/src/gemini/provider.spec.ts +216 -0
- package/src/gemini/provider.ts +279 -0
- package/src/gemini/request-converter.ts +226 -0
- package/src/gemini/tool-schema-converter.ts +78 -0
- package/src/gemini/types/api-types.ts +235 -0
- package/src/gemini/types.ts +121 -0
- package/src/gemma/index.ts +5 -0
- package/src/gemma/message-factory.ts +38 -0
- package/src/gemma/provider-definition.test.ts +43 -0
- package/src/gemma/provider-definition.ts +84 -0
- package/src/gemma/provider-projection.ts +49 -0
- package/src/gemma/provider.test.ts +628 -0
- package/src/gemma/provider.ts +308 -0
- package/src/gemma/pseudo-command-envelope.ts +58 -0
- package/src/gemma/pseudo-tool-call-projector.ts +243 -0
- package/src/gemma/pseudo-tool-call-tag-parser.ts +153 -0
- package/src/gemma/pseudo-tool-call-types.ts +31 -0
- package/src/gemma/reasoning-projector.test.ts +52 -0
- package/src/gemma/reasoning-projector.ts +144 -0
- package/src/gemma/streaming-projection.ts +79 -0
- package/src/gemma/tool-call-argument-parser.ts +126 -0
- package/src/gemma/tool-call-projector.test.ts +227 -0
- package/src/gemma/tool-call-projector.ts +264 -0
- package/src/gemma/types.ts +27 -0
- package/src/google/index.ts +11 -0
- package/src/google/provider-compat.test.ts +19 -0
- package/src/google/provider-definition.ts +6 -0
- package/src/google/provider.ts +10 -0
- package/src/google/types.ts +5 -0
- package/src/index.ts +9 -0
- package/src/openai/adapter.test.ts +494 -0
- package/src/openai/adapter.ts +145 -0
- package/src/openai/chat-completions-chat.ts +189 -0
- package/src/openai/executor-integration.test.ts +206 -0
- package/src/openai/index.ts +21 -0
- package/src/openai/interfaces/payload-logger.ts +48 -0
- package/src/openai/loggers/console-payload-logger.test.ts +173 -0
- package/src/openai/loggers/console-payload-logger.ts +94 -0
- package/src/openai/loggers/console.ts +9 -0
- package/src/openai/loggers/file-payload-logger.test.ts +238 -0
- package/src/openai/loggers/file-payload-logger.ts +112 -0
- package/src/openai/loggers/file.ts +9 -0
- package/src/openai/loggers/index.ts +12 -0
- package/src/openai/loggers/sanitize-openai-log-data.test.ts +89 -0
- package/src/openai/loggers/sanitize-openai-log-data.ts +14 -0
- package/src/openai/message-converter.ts +22 -0
- package/src/openai/model-catalog-refresh.test.ts +92 -0
- package/src/openai/model-catalog-refresh.ts +115 -0
- package/src/openai/openai-request-format.ts +92 -0
- package/src/openai/parsers/response-parser.test.ts +407 -0
- package/src/openai/parsers/response-parser.ts +47 -0
- package/src/openai/provider-definition.test.ts +75 -0
- package/src/openai/provider-definition.ts +132 -0
- package/src/openai/provider.test.ts +1402 -0
- package/src/openai/provider.ts +237 -0
- package/src/openai/responses-chat.ts +258 -0
- package/src/openai/responses-converter.ts +112 -0
- package/src/openai/responses-parser.ts +285 -0
- package/src/openai/responses-stream-utils.ts +45 -0
- package/src/openai/responses-types.ts +195 -0
- package/src/openai/streaming/stream-assembler.ts +3 -0
- package/src/openai/streaming/stream-handler.test.ts +367 -0
- package/src/openai/streaming/stream-handler.ts +119 -0
- package/src/openai/types/api-types.ts +112 -0
- package/src/openai/types.ts +194 -0
- package/src/qwen/defaults.ts +26 -0
- package/src/qwen/index.ts +5 -0
- package/src/qwen/model-catalog-refresh.test.ts +91 -0
- package/src/qwen/model-catalog-refresh.ts +97 -0
- package/src/qwen/provider-capabilities.ts +34 -0
- package/src/qwen/provider-definition.test.ts +139 -0
- package/src/qwen/provider-definition.ts +173 -0
- package/src/qwen/provider-streaming-assembly.ts +40 -0
- package/src/qwen/provider.test.ts +640 -0
- package/src/qwen/provider.ts +293 -0
- package/src/qwen/responses-chat.ts +194 -0
- package/src/qwen/responses-converter.ts +104 -0
- package/src/qwen/responses-parser.ts +299 -0
- package/src/qwen/responses-stream-utils.ts +38 -0
- package/src/qwen/types.ts +228 -0
- package/src/shared/openai-compatible/endpoint-probe.test.ts +52 -0
- package/src/shared/openai-compatible/endpoint-probe.ts +43 -0
- package/src/shared/openai-compatible/index.ts +6 -0
- package/src/shared/openai-compatible/message-converter.test.ts +111 -0
- package/src/shared/openai-compatible/message-converter.ts +84 -0
- package/src/shared/openai-compatible/native-payload-observer.test.ts +43 -0
- package/src/shared/openai-compatible/native-payload-observer.ts +26 -0
- package/src/shared/openai-compatible/response-parser.test.ts +172 -0
- package/src/shared/openai-compatible/response-parser.ts +180 -0
- package/src/shared/openai-compatible/stream-assembler.test.ts +266 -0
- package/src/shared/openai-compatible/stream-assembler.ts +248 -0
- 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,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,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
|
+
}
|