@robota-sdk/agent-provider-openai 3.0.0-beta.82 → 3.0.0-beta.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/README.md +110 -0
  3. package/dist/node/index.cjs +1 -1
  4. package/dist/node/index.d.cts +8 -3
  5. package/dist/node/index.d.cts.map +1 -1
  6. package/dist/node/index.d.ts +8 -3
  7. package/dist/node/index.d.ts.map +1 -1
  8. package/dist/node/index.js +1 -1
  9. package/dist/node/index.js.map +1 -1
  10. package/dist/node/loggers/index.d.cts +6 -6
  11. package/dist/node/loggers/index.d.cts.map +1 -1
  12. package/dist/node/loggers/index.d.ts +6 -6
  13. package/dist/node/loggers/index.d.ts.map +1 -1
  14. package/dist/node/loggers/index.js.map +1 -1
  15. package/dist/node/{payload-logger-BaW0K8yI.d.ts → payload-logger-n89AQdUn.d.cts} +6 -4
  16. package/dist/node/payload-logger-n89AQdUn.d.cts.map +1 -0
  17. package/dist/node/{payload-logger-BaW0K8yI.d.cts → payload-logger-n89AQdUn.d.ts} +6 -4
  18. package/dist/node/payload-logger-n89AQdUn.d.ts.map +1 -0
  19. package/package.json +10 -5
  20. package/src/openai/__tests__/abort-signal-wire.test.ts +210 -0
  21. package/src/openai/__tests__/strict-tools-closure.test.ts +49 -2
  22. package/src/openai/__tests__/tool-schema-projection.test.ts +16 -2
  23. package/src/openai/chat-completions-chat.ts +2 -2
  24. package/src/openai/interfaces/payload-logger.ts +5 -3
  25. package/src/openai/loggers/console-payload-logger.ts +2 -2
  26. package/src/openai/loggers/file-payload-logger.ts +6 -5
  27. package/src/openai/message-converter.ts +7 -3
  28. package/src/openai/responses-parser.test.ts +87 -0
  29. package/src/openai/responses-parser.ts +15 -9
  30. package/src/openai/responses-types.ts +2 -0
  31. package/src/openai/types.ts +7 -2
  32. package/dist/node/payload-logger-BaW0K8yI.d.cts.map +0 -1
  33. package/dist/node/payload-logger-BaW0K8yI.d.ts.map +0 -1
@@ -1,10 +1,10 @@
1
- import { n as IPayloadLoggerOptions, r as IOpenAILogData, t as IPayloadLogger } from "../payload-logger-BaW0K8yI.cjs";
1
+ import { n as IPayloadLoggerOptions, r as IOpenAILogData, t as IPayloadLogger } from "../payload-logger-n89AQdUn.cjs";
2
2
  import { ILogger } from "@robota-sdk/agent-core";
3
3
  //#region src/openai/loggers/file-payload-logger.d.ts
4
4
  /**
5
5
  * File-based payload logger for Node.js environments
6
6
  *
7
- * This logger saves API request/response payloads to JSON files on disk.
7
+ * This logger saves a summary of each Chat Completions request to a JSON file on disk.
8
8
  * It's designed specifically for Node.js environments with filesystem access.
9
9
  *
10
10
  * @example
@@ -39,8 +39,8 @@ declare class FilePayloadLogger implements IPayloadLogger {
39
39
  */
40
40
  isEnabled(): boolean;
41
41
  /**
42
- * Log API payload to file
43
- * @param payload - The API request payload
42
+ * Log a request summary to file
43
+ * @param payload - Summary of the outgoing Chat Completions request
44
44
  * @param type - Type of request ('chat' or 'stream')
45
45
  */
46
46
  logPayload(payload: IOpenAILogData, type?: 'chat' | 'stream'): Promise<void>;
@@ -54,7 +54,7 @@ declare class FilePayloadLogger implements IPayloadLogger {
54
54
  /**
55
55
  * Console-based payload logger for browser environments
56
56
  *
57
- * This logger outputs API request/response payloads to the browser console
57
+ * This logger outputs a summary of each Chat Completions request to the browser console
58
58
  * using structured logging. It's designed specifically for browser environments
59
59
  * and development/debugging scenarios.
60
60
  *
@@ -84,7 +84,7 @@ declare class ConsolePayloadLogger implements IPayloadLogger {
84
84
  isEnabled(): boolean;
85
85
  /**
86
86
  * Log API payload to browser console
87
- * @param payload - The API request payload
87
+ * @param payload - Summary of the outgoing Chat Completions request
88
88
  * @param type - Type of request ('chat' or 'stream')
89
89
  */
90
90
  logPayload(payload: IOpenAILogData, type?: 'chat' | 'stream'): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../../src/openai/loggers/file-payload-logger.ts","../../../src/openai/loggers/console-payload-logger.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;cAqCa,6BAA6B;mBACvB;mBACA;mBACA;mBACA;EAEL,YAAA;IACV;IACA;IACA;IACA,SAAS;;;;;EAeX;;;;;;EASM,WAAW,SAAS,gBAAgB,2BAAmC;;;;UAwCrE;;;;;;;;;;;;;;;;;;;;;;;;;;cClFG,gCAAgC;mBAC1B;mBACA;mBACA;EAEL,YAAA,UAAS;;;;EASrB;;;;;;EASM,WAAW,SAAS,gBAAgB,2BAAmC"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../../src/openai/loggers/file-payload-logger.ts","../../../src/openai/loggers/console-payload-logger.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;cAqCa,6BAA6B;mBACvB;mBACA;mBACA;mBACA;EAEL,YAAA;IACV;IACA;IACA;IACA,SAAS;;;;;EAeX;;;;;;EASM,WAAW,SAAS,gBAAgB,2BAAmC;;;;UAyCrE;;;;;;;;;;;;;;;;;;;;;;;;;;cCnFG,gCAAgC;mBAC1B;mBACA;mBACA;EAEL,YAAA,UAAS;;;;EASrB;;;;;;EASM,WAAW,SAAS,gBAAgB,2BAAmC"}
@@ -1,10 +1,10 @@
1
- import { n as IPayloadLoggerOptions, r as IOpenAILogData, t as IPayloadLogger } from "../payload-logger-BaW0K8yI.js";
1
+ import { n as IPayloadLoggerOptions, r as IOpenAILogData, t as IPayloadLogger } from "../payload-logger-n89AQdUn.js";
2
2
  import { ILogger } from "@robota-sdk/agent-core";
3
3
  //#region src/openai/loggers/file-payload-logger.d.ts
4
4
  /**
5
5
  * File-based payload logger for Node.js environments
6
6
  *
7
- * This logger saves API request/response payloads to JSON files on disk.
7
+ * This logger saves a summary of each Chat Completions request to a JSON file on disk.
8
8
  * It's designed specifically for Node.js environments with filesystem access.
9
9
  *
10
10
  * @example
@@ -39,8 +39,8 @@ declare class FilePayloadLogger implements IPayloadLogger {
39
39
  */
40
40
  isEnabled(): boolean;
41
41
  /**
42
- * Log API payload to file
43
- * @param payload - The API request payload
42
+ * Log a request summary to file
43
+ * @param payload - Summary of the outgoing Chat Completions request
44
44
  * @param type - Type of request ('chat' or 'stream')
45
45
  */
46
46
  logPayload(payload: IOpenAILogData, type?: 'chat' | 'stream'): Promise<void>;
@@ -54,7 +54,7 @@ declare class FilePayloadLogger implements IPayloadLogger {
54
54
  /**
55
55
  * Console-based payload logger for browser environments
56
56
  *
57
- * This logger outputs API request/response payloads to the browser console
57
+ * This logger outputs a summary of each Chat Completions request to the browser console
58
58
  * using structured logging. It's designed specifically for browser environments
59
59
  * and development/debugging scenarios.
60
60
  *
@@ -84,7 +84,7 @@ declare class ConsolePayloadLogger implements IPayloadLogger {
84
84
  isEnabled(): boolean;
85
85
  /**
86
86
  * Log API payload to browser console
87
- * @param payload - The API request payload
87
+ * @param payload - Summary of the outgoing Chat Completions request
88
88
  * @param type - Type of request ('chat' or 'stream')
89
89
  */
90
90
  logPayload(payload: IOpenAILogData, type?: 'chat' | 'stream'): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/openai/loggers/file-payload-logger.ts","../../../src/openai/loggers/console-payload-logger.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;cAqCa,6BAA6B;mBACvB;mBACA;mBACA;mBACA;EAEL,YAAA;IACV;IACA;IACA;IACA,SAAS;;;;;EAeX;;;;;;EASM,WAAW,SAAS,gBAAgB,2BAAmC;;;;UAwCrE;;;;;;;;;;;;;;;;;;;;;;;;;;cClFG,gCAAgC;mBAC1B;mBACA;mBACA;EAEL,YAAA,UAAS;;;;EASrB;;;;;;EASM,WAAW,SAAS,gBAAgB,2BAAmC"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/openai/loggers/file-payload-logger.ts","../../../src/openai/loggers/console-payload-logger.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;cAqCa,6BAA6B;mBACvB;mBACA;mBACA;mBACA;EAEL,YAAA;IACV;IACA;IACA;IACA,SAAS;;;;;EAeX;;;;;;EASM,WAAW,SAAS,gBAAgB,2BAAmC;;;;UAyCrE;;;;;;;;;;;;;;;;;;;;;;;;;;cCnFG,gCAAgC;mBAC1B;mBACA;mBACA;EAEL,YAAA,UAAS;;;;EASrB;;;;;;EASM,WAAW,SAAS,gBAAgB,2BAAmC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/openai/loggers/sanitize-openai-log-data.ts","../../../src/openai/loggers/file-payload-logger.ts","../../../src/openai/loggers/console-payload-logger.ts"],"sourcesContent":["import type { IOpenAILogData } from '../types/api-types';\n\n/**\n * Creates a defensive deep copy of OpenAI log data.\n * SSOT utility shared by payload loggers.\n */\nexport function sanitizeOpenAILogData(payload: IOpenAILogData): IOpenAILogData {\n // Create a deep copy to avoid modifying original\n const sanitized = JSON.parse(JSON.stringify(payload)) as IOpenAILogData;\n\n // Remove or mask sensitive data if needed.\n // For now, we keep everything as OpenAI payloads don't contain API keys.\n return sanitized;\n}\n","import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { SilentLogger } from '@robota-sdk/agent-core';\n\nimport { sanitizeOpenAILogData } from './sanitize-openai-log-data';\n\nimport type { IPayloadLogger } from '../interfaces/payload-logger';\nimport type { IOpenAILogData } from '../types/api-types';\nimport type { ILogger } from '@robota-sdk/agent-core';\n\n/** Owner-only access — no group or other bits (SEC-003 / CWE-377). */\nconst OWNER_ONLY_FILE_MODE = 0o600;\nconst OWNER_ONLY_DIR_MODE = 0o700;\n\n/**\n * File-based payload logger for Node.js environments\n *\n * This logger saves API request/response payloads to JSON files on disk.\n * It's designed specifically for Node.js environments with filesystem access.\n *\n * @example\n * ```typescript\n * import { FilePayloadLogger } from '@robota-sdk/agent-provider-openai/loggers';\n *\n * const logger = new FilePayloadLogger({\n * logDir: './logs/api-payloads',\n * enabled: true,\n * includeTimestamp: true\n * });\n *\n * const provider = new OpenAIProvider({\n * client: openaiClient,\n * payloadLogger: logger\n * });\n * ```\n */\nexport class FilePayloadLogger implements IPayloadLogger {\n private readonly enabled: boolean;\n private readonly logDir: string;\n private readonly includeTimestamp: boolean;\n private readonly logger: ILogger;\n\n constructor(options: {\n logDir: string;\n enabled?: boolean;\n includeTimestamp?: boolean;\n logger?: ILogger;\n }) {\n this.enabled = options.enabled ?? true;\n this.logDir = options.logDir;\n this.includeTimestamp = options.includeTimestamp ?? true;\n this.logger = options.logger || SilentLogger;\n\n if (this.enabled) {\n this.ensureLogDirectoryExists();\n }\n }\n\n /**\n * Check if logging is enabled\n */\n isEnabled(): boolean {\n return this.enabled;\n }\n\n /**\n * Log API payload to file\n * @param payload - The API request payload\n * @param type - Type of request ('chat' or 'stream')\n */\n async logPayload(payload: IOpenAILogData, type: 'chat' | 'stream' = 'chat'): Promise<void> {\n if (!this.enabled) {\n return;\n }\n\n try {\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n const filename = this.includeTimestamp\n ? `openai-${type}-${timestamp}.json`\n : `openai-${type}-${Date.now()}.json`;\n\n const filepath = path.join(this.logDir, filename);\n\n const logData = {\n timestamp: new Date().toISOString(),\n type,\n provider: 'openai',\n payload: sanitizeOpenAILogData(payload),\n };\n\n // SEC-003: payload logs contain prompt/response content and `logDir` is\n // caller-supplied, so create them owner-only rather than under the process umask.\n await fs.promises.writeFile(filepath, JSON.stringify(logData, null, 2), {\n encoding: 'utf8',\n mode: OWNER_ONLY_FILE_MODE,\n });\n\n // Payload saved successfully (silent operation)\n } catch (error) {\n // Don't throw errors - just log them and continue\n // This ensures that API logging failures don't break the main functionality\n this.logger.error('[FilePayloadLogger] Failed to save payload log:', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n /**\n * Ensure log directory exists\n */\n private ensureLogDirectoryExists(): void {\n try {\n if (!fs.existsSync(this.logDir)) {\n fs.mkdirSync(this.logDir, { recursive: true, mode: OWNER_ONLY_DIR_MODE });\n }\n } catch (error) {\n this.logger.error('[FilePayloadLogger] Failed to create log directory:', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n // Sanitization intentionally lives in ./sanitize-openai-log-data.ts (SSOT utility).\n}\n","import { SilentLogger, type ILogger } from '@robota-sdk/agent-core';\n\nimport { sanitizeOpenAILogData } from './sanitize-openai-log-data';\n\nimport type { IPayloadLogger, IPayloadLoggerOptions } from '../interfaces/payload-logger';\nimport type { IOpenAILogData } from '../types/api-types';\n\n/**\n * Console-based payload logger for browser environments\n *\n * This logger outputs API request/response payloads to the browser console\n * using structured logging. It's designed specifically for browser environments\n * and development/debugging scenarios.\n *\n * @example\n * ```typescript\n * import { ConsolePayloadLogger } from '@robota-sdk/agent-provider-openai/loggers';\n *\n * const logger = new ConsolePayloadLogger({\n * enabled: true,\n * includeTimestamp: true\n * });\n *\n * const provider = new OpenAIProvider({\n * client: openaiClient,\n * payloadLogger: logger\n * });\n * ```\n */\nexport class ConsolePayloadLogger implements IPayloadLogger {\n private readonly enabled: boolean;\n private readonly includeTimestamp: boolean;\n private readonly logger: ILogger;\n\n constructor(options: IPayloadLoggerOptions = {}) {\n this.enabled = options.enabled ?? true;\n this.includeTimestamp = options.includeTimestamp ?? true;\n this.logger = options.logger || SilentLogger;\n }\n\n /**\n * Check if logging is enabled\n */\n isEnabled(): boolean {\n return this.enabled;\n }\n\n /**\n * Log API payload to browser console\n * @param payload - The API request payload\n * @param type - Type of request ('chat' or 'stream')\n */\n async logPayload(payload: IOpenAILogData, type: 'chat' | 'stream' = 'chat'): Promise<void> {\n if (!this.enabled) {\n return;\n }\n\n try {\n const sanitizedPayload = sanitizeOpenAILogData(payload);\n\n // Use structured console logging for better browser developer tools integration\n const title = `[OpenAI ${type.toUpperCase()}] API Payload`;\n const timeInfo = this.includeTimestamp ? ` (${sanitizedPayload.timestamp})` : '';\n\n // Group related log entries for better organization\n this.logger.group?.(`${title}${timeInfo}`);\n\n // Log different aspects with appropriate console methods\n this.logger.info('📋 Request Details:', {\n model: payload.model,\n messagesCount: payload.messagesCount,\n hasTools: payload.hasTools,\n temperature: payload.temperature,\n maxTokens: payload.maxTokens,\n });\n\n this.logger.debug('🔍 Full Payload:', { type, provider: 'openai', ...sanitizedPayload });\n\n this.logger.groupEnd?.();\n } catch (error) {\n // Don't throw errors - just log them and continue\n // This ensures that API logging failures don't break the main functionality\n this.logger.error(\n '[ConsolePayloadLogger] Failed to log payload:',\n error instanceof Error ? error.message : 'Unknown error',\n );\n }\n }\n\n /**\n * Sanitize payload to remove sensitive information\n * @param payload - Raw payload object\n * @returns Sanitized payload\n */\n // Sanitization intentionally lives in ./sanitize-openai-log-data.ts (SSOT utility).\n}\n"],"mappings":"kGAMA,SAAgB,EAAsB,EAAyC,CAM7E,OAJkB,KAAK,MAAM,KAAK,UAAU,CAAO,CAIpC,CACjB,CCwBA,IAAa,EAAb,KAAyD,CACvD,QACA,OACA,iBACA,OAEA,YAAY,EAKT,CACD,KAAK,QAAU,EAAQ,SAAW,GAClC,KAAK,OAAS,EAAQ,OACtB,KAAK,iBAAmB,EAAQ,kBAAoB,GACpD,KAAK,OAAS,EAAQ,QAAU,EAE5B,KAAK,SACP,KAAK,yBAAyB,CAElC,CAKA,WAAqB,CACnB,OAAO,KAAK,OACd,CAOA,MAAM,WAAW,EAAyB,EAA0B,OAAuB,CACpF,QAAK,QAIV,GAAI,CACF,IAAM,EAAY,IAAI,KAAK,CAAA,CAAE,YAAY,CAAC,CAAC,QAAQ,QAAS,GAAG,EACzD,EAAW,KAAK,iBAClB,UAAU,EAAK,GAAG,EAAU,OAC5B,UAAU,EAAK,GAAG,KAAK,IAAI,EAAE,OAE3B,EAAW,EAAK,KAAK,KAAK,OAAQ,CAAQ,EAE1C,EAAU,CACd,UAAW,IAAI,KAAK,CAAA,CAAE,YAAY,EAClC,OACA,SAAU,SACV,QAAS,EAAsB,CAAO,CACxC,EAIA,MAAM,EAAG,SAAS,UAAU,EAAU,KAAK,UAAU,EAAS,KAAM,CAAC,EAAG,CACtE,SAAU,OACV,KAAM,GACR,CAAC,CAGH,OAAS,EAAO,CAGd,KAAK,OAAO,MAAM,kDAAmD,CACnE,MAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAC9D,CAAC,CACH,CACF,CAKA,0BAAyC,CACvC,GAAI,CACG,EAAG,WAAW,KAAK,MAAM,GAC5B,EAAG,UAAU,KAAK,OAAQ,CAAE,UAAW,GAAM,KAAM,GAAoB,CAAC,CAE5E,OAAS,EAAO,CACd,KAAK,OAAO,MAAM,sDAAuD,CACvE,MAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAC9D,CAAC,CACH,CACF,CAEF,EC9Fa,EAAb,KAA4D,CAC1D,QACA,iBACA,OAEA,YAAY,EAAiC,CAAC,EAAG,CAC/C,KAAK,QAAU,EAAQ,SAAW,GAClC,KAAK,iBAAmB,EAAQ,kBAAoB,GACpD,KAAK,OAAS,EAAQ,QAAU,CAClC,CAKA,WAAqB,CACnB,OAAO,KAAK,OACd,CAOA,MAAM,WAAW,EAAyB,EAA0B,OAAuB,CACpF,QAAK,QAIV,GAAI,CACF,IAAM,EAAmB,EAAsB,CAAO,EAGhD,EAAQ,WAAW,EAAK,YAAY,EAAE,eACtC,EAAW,KAAK,iBAAmB,KAAK,EAAiB,UAAU,GAAK,GAG9E,KAAK,OAAO,QAAQ,GAAG,IAAQ,GAAU,EAGzC,KAAK,OAAO,KAAK,sBAAuB,CACtC,MAAO,EAAQ,MACf,cAAe,EAAQ,cACvB,SAAU,EAAQ,SAClB,YAAa,EAAQ,YACrB,UAAW,EAAQ,SACrB,CAAC,EAED,KAAK,OAAO,MAAM,mBAAoB,CAAE,OAAM,SAAU,SAAU,GAAG,CAAiB,CAAC,EAEvF,KAAK,OAAO,WAAW,CACzB,OAAS,EAAO,CAGd,KAAK,OAAO,MACV,gDACA,aAAiB,MAAQ,EAAM,QAAU,eAC3C,CACF,CACF,CAQF"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/openai/loggers/sanitize-openai-log-data.ts","../../../src/openai/loggers/file-payload-logger.ts","../../../src/openai/loggers/console-payload-logger.ts"],"sourcesContent":["import type { IOpenAILogData } from '../types/api-types';\n\n/**\n * Creates a defensive deep copy of OpenAI log data.\n * SSOT utility shared by payload loggers.\n */\nexport function sanitizeOpenAILogData(payload: IOpenAILogData): IOpenAILogData {\n // Create a deep copy to avoid modifying original\n const sanitized = JSON.parse(JSON.stringify(payload)) as IOpenAILogData;\n\n // Remove or mask sensitive data if needed.\n // For now, we keep everything as OpenAI payloads don't contain API keys.\n return sanitized;\n}\n","import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { SilentLogger } from '@robota-sdk/agent-core';\n\nimport { sanitizeOpenAILogData } from './sanitize-openai-log-data';\n\nimport type { IPayloadLogger } from '../interfaces/payload-logger';\nimport type { IOpenAILogData } from '../types/api-types';\nimport type { ILogger } from '@robota-sdk/agent-core';\n\n/** Owner-only access — no group or other bits (SEC-003 / CWE-377). */\nconst OWNER_ONLY_FILE_MODE = 0o600;\nconst OWNER_ONLY_DIR_MODE = 0o700;\n\n/**\n * File-based payload logger for Node.js environments\n *\n * This logger saves a summary of each Chat Completions request to a JSON file on disk.\n * It's designed specifically for Node.js environments with filesystem access.\n *\n * @example\n * ```typescript\n * import { FilePayloadLogger } from '@robota-sdk/agent-provider-openai/loggers';\n *\n * const logger = new FilePayloadLogger({\n * logDir: './logs/api-payloads',\n * enabled: true,\n * includeTimestamp: true\n * });\n *\n * const provider = new OpenAIProvider({\n * client: openaiClient,\n * payloadLogger: logger\n * });\n * ```\n */\nexport class FilePayloadLogger implements IPayloadLogger {\n private readonly enabled: boolean;\n private readonly logDir: string;\n private readonly includeTimestamp: boolean;\n private readonly logger: ILogger;\n\n constructor(options: {\n logDir: string;\n enabled?: boolean;\n includeTimestamp?: boolean;\n logger?: ILogger;\n }) {\n this.enabled = options.enabled ?? true;\n this.logDir = options.logDir;\n this.includeTimestamp = options.includeTimestamp ?? true;\n this.logger = options.logger || SilentLogger;\n\n if (this.enabled) {\n this.ensureLogDirectoryExists();\n }\n }\n\n /**\n * Check if logging is enabled\n */\n isEnabled(): boolean {\n return this.enabled;\n }\n\n /**\n * Log a request summary to file\n * @param payload - Summary of the outgoing Chat Completions request\n * @param type - Type of request ('chat' or 'stream')\n */\n async logPayload(payload: IOpenAILogData, type: 'chat' | 'stream' = 'chat'): Promise<void> {\n if (!this.enabled) {\n return;\n }\n\n try {\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n const filename = this.includeTimestamp\n ? `openai-${type}-${timestamp}.json`\n : `openai-${type}-${Date.now()}.json`;\n\n const filepath = path.join(this.logDir, filename);\n\n const logData = {\n timestamp: new Date().toISOString(),\n type,\n provider: 'openai',\n payload: sanitizeOpenAILogData(payload),\n };\n\n // SEC-003: payload logs record the caller's request history (a request summary, not\n // prompt/response content) and `logDir` is caller-supplied, so create them owner-only\n // rather than under the process umask.\n await fs.promises.writeFile(filepath, JSON.stringify(logData, null, 2), {\n encoding: 'utf8',\n mode: OWNER_ONLY_FILE_MODE,\n });\n\n // Payload saved successfully (silent operation)\n } catch (error) {\n // Don't throw errors - just log them and continue\n // This ensures that API logging failures don't break the main functionality\n this.logger.error('[FilePayloadLogger] Failed to save payload log:', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n /**\n * Ensure log directory exists\n */\n private ensureLogDirectoryExists(): void {\n try {\n if (!fs.existsSync(this.logDir)) {\n fs.mkdirSync(this.logDir, { recursive: true, mode: OWNER_ONLY_DIR_MODE });\n }\n } catch (error) {\n this.logger.error('[FilePayloadLogger] Failed to create log directory:', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n // Sanitization intentionally lives in ./sanitize-openai-log-data.ts (SSOT utility).\n}\n","import { SilentLogger, type ILogger } from '@robota-sdk/agent-core';\n\nimport { sanitizeOpenAILogData } from './sanitize-openai-log-data';\n\nimport type { IPayloadLogger, IPayloadLoggerOptions } from '../interfaces/payload-logger';\nimport type { IOpenAILogData } from '../types/api-types';\n\n/**\n * Console-based payload logger for browser environments\n *\n * This logger outputs a summary of each Chat Completions request to the browser console\n * using structured logging. It's designed specifically for browser environments\n * and development/debugging scenarios.\n *\n * @example\n * ```typescript\n * import { ConsolePayloadLogger } from '@robota-sdk/agent-provider-openai/loggers';\n *\n * const logger = new ConsolePayloadLogger({\n * enabled: true,\n * includeTimestamp: true\n * });\n *\n * const provider = new OpenAIProvider({\n * client: openaiClient,\n * payloadLogger: logger\n * });\n * ```\n */\nexport class ConsolePayloadLogger implements IPayloadLogger {\n private readonly enabled: boolean;\n private readonly includeTimestamp: boolean;\n private readonly logger: ILogger;\n\n constructor(options: IPayloadLoggerOptions = {}) {\n this.enabled = options.enabled ?? true;\n this.includeTimestamp = options.includeTimestamp ?? true;\n this.logger = options.logger || SilentLogger;\n }\n\n /**\n * Check if logging is enabled\n */\n isEnabled(): boolean {\n return this.enabled;\n }\n\n /**\n * Log API payload to browser console\n * @param payload - Summary of the outgoing Chat Completions request\n * @param type - Type of request ('chat' or 'stream')\n */\n async logPayload(payload: IOpenAILogData, type: 'chat' | 'stream' = 'chat'): Promise<void> {\n if (!this.enabled) {\n return;\n }\n\n try {\n const sanitizedPayload = sanitizeOpenAILogData(payload);\n\n // Use structured console logging for better browser developer tools integration\n const title = `[OpenAI ${type.toUpperCase()}] API Payload`;\n const timeInfo = this.includeTimestamp ? ` (${sanitizedPayload.timestamp})` : '';\n\n // Group related log entries for better organization\n this.logger.group?.(`${title}${timeInfo}`);\n\n // Log different aspects with appropriate console methods\n this.logger.info('📋 Request Details:', {\n model: payload.model,\n messagesCount: payload.messagesCount,\n hasTools: payload.hasTools,\n temperature: payload.temperature,\n maxTokens: payload.maxTokens,\n });\n\n this.logger.debug('🔍 Full Payload:', { type, provider: 'openai', ...sanitizedPayload });\n\n this.logger.groupEnd?.();\n } catch (error) {\n // Don't throw errors - just log them and continue\n // This ensures that API logging failures don't break the main functionality\n this.logger.error(\n '[ConsolePayloadLogger] Failed to log payload:',\n error instanceof Error ? error.message : 'Unknown error',\n );\n }\n }\n\n /**\n * Sanitize payload to remove sensitive information\n * @param payload - Raw payload object\n * @returns Sanitized payload\n */\n // Sanitization intentionally lives in ./sanitize-openai-log-data.ts (SSOT utility).\n}\n"],"mappings":"kGAMA,SAAgB,EAAsB,EAAyC,CAM7E,OAJkB,KAAK,MAAM,KAAK,UAAU,CAAO,CAIpC,CACjB,CCwBA,IAAa,EAAb,KAAyD,CACvD,QACA,OACA,iBACA,OAEA,YAAY,EAKT,CACD,KAAK,QAAU,EAAQ,SAAW,GAClC,KAAK,OAAS,EAAQ,OACtB,KAAK,iBAAmB,EAAQ,kBAAoB,GACpD,KAAK,OAAS,EAAQ,QAAU,EAE5B,KAAK,SACP,KAAK,yBAAyB,CAElC,CAKA,WAAqB,CACnB,OAAO,KAAK,OACd,CAOA,MAAM,WAAW,EAAyB,EAA0B,OAAuB,CACpF,QAAK,QAIV,GAAI,CACF,IAAM,EAAY,IAAI,KAAK,CAAA,CAAE,YAAY,CAAC,CAAC,QAAQ,QAAS,GAAG,EACzD,EAAW,KAAK,iBAClB,UAAU,EAAK,GAAG,EAAU,OAC5B,UAAU,EAAK,GAAG,KAAK,IAAI,EAAE,OAE3B,EAAW,EAAK,KAAK,KAAK,OAAQ,CAAQ,EAE1C,EAAU,CACd,UAAW,IAAI,KAAK,CAAA,CAAE,YAAY,EAClC,OACA,SAAU,SACV,QAAS,EAAsB,CAAO,CACxC,EAKA,MAAM,EAAG,SAAS,UAAU,EAAU,KAAK,UAAU,EAAS,KAAM,CAAC,EAAG,CACtE,SAAU,OACV,KAAM,GACR,CAAC,CAGH,OAAS,EAAO,CAGd,KAAK,OAAO,MAAM,kDAAmD,CACnE,MAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAC9D,CAAC,CACH,CACF,CAKA,0BAAyC,CACvC,GAAI,CACG,EAAG,WAAW,KAAK,MAAM,GAC5B,EAAG,UAAU,KAAK,OAAQ,CAAE,UAAW,GAAM,KAAM,GAAoB,CAAC,CAE5E,OAAS,EAAO,CACd,KAAK,OAAO,MAAM,sDAAuD,CACvE,MAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAC9D,CAAC,CACH,CACF,CAEF,EC/Fa,EAAb,KAA4D,CAC1D,QACA,iBACA,OAEA,YAAY,EAAiC,CAAC,EAAG,CAC/C,KAAK,QAAU,EAAQ,SAAW,GAClC,KAAK,iBAAmB,EAAQ,kBAAoB,GACpD,KAAK,OAAS,EAAQ,QAAU,CAClC,CAKA,WAAqB,CACnB,OAAO,KAAK,OACd,CAOA,MAAM,WAAW,EAAyB,EAA0B,OAAuB,CACpF,QAAK,QAIV,GAAI,CACF,IAAM,EAAmB,EAAsB,CAAO,EAGhD,EAAQ,WAAW,EAAK,YAAY,EAAE,eACtC,EAAW,KAAK,iBAAmB,KAAK,EAAiB,UAAU,GAAK,GAG9E,KAAK,OAAO,QAAQ,GAAG,IAAQ,GAAU,EAGzC,KAAK,OAAO,KAAK,sBAAuB,CACtC,MAAO,EAAQ,MACf,cAAe,EAAQ,cACvB,SAAU,EAAQ,SAClB,YAAa,EAAQ,YACrB,UAAW,EAAQ,SACrB,CAAC,EAED,KAAK,OAAO,MAAM,mBAAoB,CAAE,OAAM,SAAU,SAAU,GAAG,CAAiB,CAAC,EAEvF,KAAK,OAAO,WAAW,CACzB,OAAS,EAAO,CAGd,KAAK,OAAO,MACV,gDACA,aAAiB,MAAQ,EAAM,QAAU,eAC3C,CACF,CACF,CAQF"}
@@ -16,7 +16,9 @@ interface IOpenAILogData {
16
16
  //#endregion
17
17
  //#region src/openai/interfaces/payload-logger.d.ts
18
18
  /**
19
- * IPayloadLogger interface for logging OpenAI API payloads
19
+ * IPayloadLogger interface for logging a summary of each OpenAI Chat Completions request
20
+ * (request metadata such as model, message count and whether tools were sent — not prompt or
21
+ * response content).
20
22
  *
21
23
  * This interface provides a contract for different logging implementations:
22
24
  * - FilePayloadLogger: Node.js file-based logging
@@ -30,8 +32,8 @@ interface IPayloadLogger {
30
32
  */
31
33
  isEnabled(): boolean;
32
34
  /**
33
- * Log API payload data
34
- * @param payload - The API request/response payload data
35
+ * Log a request summary
36
+ * @param payload - Summary of the outgoing Chat Completions request
35
37
  * @param type - Type of operation ('chat' or 'stream')
36
38
  */
37
39
  logPayload(payload: IOpenAILogData, type: 'chat' | 'stream'): Promise<void>;
@@ -58,4 +60,4 @@ interface IPayloadLoggerOptions {
58
60
  }
59
61
  //#endregion
60
62
  export { IPayloadLoggerOptions as n, IOpenAILogData as r, IPayloadLogger as t };
61
- //# sourceMappingURL=payload-logger-BaW0K8yI.d.ts.map
63
+ //# sourceMappingURL=payload-logger-n89AQdUn.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payload-logger-n89AQdUn.d.cts","names":[],"sources":["../../src/openai/types/api-types.ts","../../src/openai/interfaces/payload-logger.ts"],"mappings":";;;;;;UAwGiB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;;;;UClGe;;;;;EAKf;;;;;;EAOA,WAAW,SAAS,gBAAgB,0BAA0B;;;;;UAM/C;;;;;EAKf;;;;;EAMA;;;;;EAMA,SAAS"}
@@ -16,7 +16,9 @@ interface IOpenAILogData {
16
16
  //#endregion
17
17
  //#region src/openai/interfaces/payload-logger.d.ts
18
18
  /**
19
- * IPayloadLogger interface for logging OpenAI API payloads
19
+ * IPayloadLogger interface for logging a summary of each OpenAI Chat Completions request
20
+ * (request metadata such as model, message count and whether tools were sent — not prompt or
21
+ * response content).
20
22
  *
21
23
  * This interface provides a contract for different logging implementations:
22
24
  * - FilePayloadLogger: Node.js file-based logging
@@ -30,8 +32,8 @@ interface IPayloadLogger {
30
32
  */
31
33
  isEnabled(): boolean;
32
34
  /**
33
- * Log API payload data
34
- * @param payload - The API request/response payload data
35
+ * Log a request summary
36
+ * @param payload - Summary of the outgoing Chat Completions request
35
37
  * @param type - Type of operation ('chat' or 'stream')
36
38
  */
37
39
  logPayload(payload: IOpenAILogData, type: 'chat' | 'stream'): Promise<void>;
@@ -58,4 +60,4 @@ interface IPayloadLoggerOptions {
58
60
  }
59
61
  //#endregion
60
62
  export { IPayloadLoggerOptions as n, IOpenAILogData as r, IPayloadLogger as t };
61
- //# sourceMappingURL=payload-logger-BaW0K8yI.d.cts.map
63
+ //# sourceMappingURL=payload-logger-n89AQdUn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payload-logger-n89AQdUn.d.ts","names":[],"sources":["../../src/openai/types/api-types.ts","../../src/openai/interfaces/payload-logger.ts"],"mappings":";;;;;;UAwGiB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;;;;UClGe;;;;;EAKf;;;;;;EAOA,WAAW,SAAS,gBAAgB,0BAA0B;;;;;UAM/C;;;;;EAKf;;;;;EAMA;;;;;EAMA,SAAS"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robota-sdk/agent-provider-openai",
3
- "version": "3.0.0-beta.82",
3
+ "version": "3.0.0-beta.83",
4
4
  "description": "OpenAI provider implementation for Robota SDK",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -54,11 +54,16 @@
54
54
  "default": "./dist/node/loggers/index.cjs"
55
55
  }
56
56
  }
57
- }
57
+ },
58
+ "./package.json": "./package.json"
59
+ },
60
+ "engines": {
61
+ "node": ">=22.12.0"
58
62
  },
59
63
  "files": [
60
64
  "dist",
61
- "src"
65
+ "src",
66
+ "CHANGELOG.md"
62
67
  ],
63
68
  "devDependencies": {
64
69
  "@types/node": "^22.19.21",
@@ -75,8 +80,8 @@
75
80
  },
76
81
  "dependencies": {
77
82
  "openai": "^4.104.0",
78
- "@robota-sdk/agent-core": "3.0.0-beta.82",
79
- "@robota-sdk/agent-provider-openai-compatible": "3.0.0-beta.82"
83
+ "@robota-sdk/agent-core": "3.0.0-beta.83",
84
+ "@robota-sdk/agent-provider-openai-compatible": "3.0.0-beta.83"
80
85
  },
81
86
  "volta": {
82
87
  "extends": "../../package.json"
@@ -0,0 +1,210 @@
1
+ /**
2
+ * The run's `AbortSignal` reaches the SDK request on every call site of both API surfaces
3
+ * (agent-core SPEC, Cancellation Contract). The non-streaming Chat Completions path is the one the
4
+ * forced summary takes, and it used to send no signal, so an aborted run left that request running.
5
+ */
6
+ import { createServer } from 'node:http';
7
+
8
+ import { classifyProviderFailure } from '@robota-sdk/agent-core';
9
+ import { APIUserAbortError } from 'openai';
10
+ import { describe, expect, it, vi } from 'vitest';
11
+
12
+ import { OpenAIProvider } from '../provider';
13
+
14
+ import type { IChatOptions, TUniversalMessage } from '@robota-sdk/agent-core';
15
+ import type { AddressInfo } from 'node:net';
16
+ import type OpenAI from 'openai';
17
+
18
+ const completion = {
19
+ id: 'c1',
20
+ object: 'chat.completion',
21
+ created: 0,
22
+ model: 'gpt-test',
23
+ choices: [{ index: 0, message: { role: 'assistant', content: 'hi' }, finish_reason: 'stop' }],
24
+ };
25
+ const completionChunks = [
26
+ {
27
+ id: 'c1',
28
+ object: 'chat.completion.chunk',
29
+ created: 0,
30
+ model: 'gpt-test',
31
+ choices: [{ index: 0, delta: { role: 'assistant', content: 'hi' }, finish_reason: null }],
32
+ },
33
+ {
34
+ id: 'c1',
35
+ object: 'chat.completion.chunk',
36
+ created: 0,
37
+ model: 'gpt-test',
38
+ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
39
+ },
40
+ ];
41
+ const response = {
42
+ id: 'resp_1',
43
+ object: 'response',
44
+ created_at: 0,
45
+ model: 'gpt-test',
46
+ status: 'completed',
47
+ output: [
48
+ {
49
+ type: 'message',
50
+ id: 'msg_1',
51
+ role: 'assistant',
52
+ status: 'completed',
53
+ content: [{ type: 'output_text', text: 'hi', annotations: [] }],
54
+ },
55
+ ],
56
+ usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
57
+ };
58
+ const responseEvents = [
59
+ {
60
+ type: 'response.output_text.delta',
61
+ item_id: 'msg_1',
62
+ output_index: 0,
63
+ content_index: 0,
64
+ delta: 'hi',
65
+ sequence_number: 1,
66
+ },
67
+ { type: 'response.completed', response, sequence_number: 2 },
68
+ ];
69
+
70
+ const messages: TUniversalMessage[] = [
71
+ { id: 'u1', role: 'user', content: 'hello', state: 'complete', timestamp: new Date() },
72
+ ];
73
+
74
+ async function* iterate(items: readonly object[]): AsyncGenerator<object> {
75
+ for (const item of items) yield item;
76
+ }
77
+
78
+ /** An SDK client double whose `create` answers like the SDK and records the request options. */
79
+ function fakeClient() {
80
+ const chatCreate = vi.fn(
81
+ (params: { stream?: boolean }, _requestOptions?: OpenAI.RequestOptions) =>
82
+ Promise.resolve(params.stream ? iterate(completionChunks) : completion),
83
+ );
84
+ const responsesCreate = vi.fn(
85
+ (params: { stream?: boolean }, _requestOptions?: OpenAI.RequestOptions) =>
86
+ Promise.resolve(params.stream ? iterate(responseEvents) : response),
87
+ );
88
+ const client = {
89
+ chat: { completions: { create: chatCreate } },
90
+ responses: { create: responsesCreate },
91
+ } as unknown as OpenAI;
92
+ return { client, chatCreate, responsesCreate };
93
+ }
94
+
95
+ const CALL_SITES: ReadonlyArray<
96
+ readonly [string, (provider: OpenAIProvider, options: IChatOptions) => Promise<void>]
97
+ > = [
98
+ [
99
+ 'chat() without a delta callback (non-streaming request, the forced-summary path)',
100
+ async (provider, options) => {
101
+ await provider.chat(messages, options);
102
+ },
103
+ ],
104
+ [
105
+ 'chat() with a delta callback (streaming assembly)',
106
+ async (provider, options) => {
107
+ await provider.chat(messages, { ...options, onTextDelta: () => undefined });
108
+ },
109
+ ],
110
+ [
111
+ 'chatStream()',
112
+ async (provider, options) => {
113
+ for await (const _chunk of provider.chatStream(messages, options)) {
114
+ // drain
115
+ }
116
+ },
117
+ ],
118
+ ];
119
+
120
+ describe('OpenAI run AbortSignal on the wire', () => {
121
+ for (const apiSurface of ['chat-completions', 'responses'] as const) {
122
+ for (const [callSite, run] of CALL_SITES) {
123
+ it(`${apiSurface}: ${callSite} hands the run's own signal to the SDK request`, async () => {
124
+ const { client, chatCreate, responsesCreate } = fakeClient();
125
+ const create = apiSurface === 'responses' ? responsesCreate : chatCreate;
126
+ const controller = new AbortController();
127
+
128
+ await run(new OpenAIProvider({ client, apiSurface }), {
129
+ model: 'gpt-test',
130
+ signal: controller.signal,
131
+ });
132
+
133
+ expect(create).toHaveBeenCalledTimes(1);
134
+ expect(create.mock.calls[0]?.[1]?.signal).toBe(controller.signal);
135
+ });
136
+ }
137
+ }
138
+
139
+ it('closes the HTTP request when the run aborts a non-streaming Chat Completions call', async () => {
140
+ const server = await startUnansweringServer();
141
+ let bound: ReturnType<typeof setTimeout> | undefined;
142
+ try {
143
+ const provider = new OpenAIProvider({
144
+ apiKey: 'test-key',
145
+ baseURL: server.baseURL,
146
+ apiSurface: 'chat-completions',
147
+ });
148
+ const controller = new AbortController();
149
+ const pending = provider.chat(messages, { model: 'gpt-test', signal: controller.signal });
150
+ const outcome = pending.then(
151
+ () => 'resolved' as const,
152
+ (error: unknown) => error,
153
+ );
154
+
155
+ await server.requestReceived;
156
+ controller.abort();
157
+
158
+ // The server never answers, so only the client hanging up can close the request. Without the
159
+ // signal it stays open until the SDK's own 10-minute timeout; the bound only decides failure.
160
+ const closed = await Promise.race([
161
+ server.clientHungUp.then(() => 'closed' as const),
162
+ new Promise<'still open'>((resolve) => {
163
+ bound = setTimeout(() => resolve('still open'), 2_000);
164
+ }),
165
+ ]);
166
+ expect(closed).toBe('closed');
167
+
168
+ // The SDK's own abort error, passed through rather than wrapped as a provider failure, so the
169
+ // run layer classifies it as an interruption — without needing the signal to tell it so.
170
+ const error = await outcome;
171
+ expect(error).toBeInstanceOf(APIUserAbortError);
172
+ expect(classifyProviderFailure(error)).toEqual({ switchable: false, reason: 'aborted' });
173
+ } finally {
174
+ clearTimeout(bound);
175
+ await server.close();
176
+ }
177
+ });
178
+ });
179
+
180
+ /** A local endpoint that accepts a request and never answers it, reporting when the client hangs up. */
181
+ async function startUnansweringServer(): Promise<{
182
+ baseURL: string;
183
+ requestReceived: Promise<void>;
184
+ clientHungUp: Promise<void>;
185
+ close: () => Promise<void>;
186
+ }> {
187
+ let onRequest!: () => void;
188
+ let onHangUp!: () => void;
189
+ const requestReceived = new Promise<void>((resolve) => (onRequest = resolve));
190
+ const clientHungUp = new Promise<void>((resolve) => (onHangUp = resolve));
191
+ const server = createServer((request, reply) => {
192
+ request.resume();
193
+ reply.on('close', () => {
194
+ if (!reply.writableEnded) onHangUp();
195
+ });
196
+ onRequest();
197
+ });
198
+ await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
199
+ const { port } = server.address() as AddressInfo;
200
+ return {
201
+ baseURL: `http://127.0.0.1:${port}/v1`,
202
+ requestReceived,
203
+ clientHungUp,
204
+ close: () =>
205
+ new Promise<void>((resolve) => {
206
+ server.closeAllConnections();
207
+ server.close(() => resolve());
208
+ }),
209
+ };
210
+ }
@@ -27,8 +27,9 @@ vi.mock('openai', () => {
27
27
  return { default: MockOpenAI };
28
28
  });
29
29
 
30
- interface IFakeResponsesClient {
30
+ interface IFakeOpenAIClient {
31
31
  responses: { create: ReturnType<typeof vi.fn> };
32
+ chat: { completions: { create: ReturnType<typeof vi.fn> } };
32
33
  }
33
34
 
34
35
  function createUserMessage(content: string): TUniversalMessage {
@@ -49,7 +50,7 @@ async function sendChat(
49
50
  provider: OpenAIProvider,
50
51
  tools: IToolSchema[],
51
52
  ): Promise<Record<string, unknown>> {
52
- const client = (provider as unknown as { client: IFakeResponsesClient }).client;
53
+ const client = (provider as unknown as { client: IFakeOpenAIClient }).client;
53
54
  client.responses.create.mockResolvedValue(fakeResponsesResult());
54
55
  await provider.chat([createUserMessage('hello')], { model: 'gpt-4o', tools });
55
56
  const [requestParams] = client.responses.create.mock.calls[
@@ -58,6 +59,25 @@ async function sendChat(
58
59
  return requestParams;
59
60
  }
60
61
 
62
+ /** The same request through the Chat Completions surface (the default once `baseURL` is set). */
63
+ async function sendChatCompletions(
64
+ provider: OpenAIProvider,
65
+ tools: IToolSchema[],
66
+ ): Promise<Record<string, unknown>> {
67
+ const client = (provider as unknown as { client: IFakeOpenAIClient }).client;
68
+ client.chat.completions.create.mockResolvedValue({
69
+ id: 'chatcmpl-strict-tools',
70
+ object: 'chat.completion',
71
+ created: 1,
72
+ model: 'gpt-4o',
73
+ choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
74
+ });
75
+ await provider.chat([createUserMessage('hello')], { model: 'gpt-4o', tools });
76
+ const calls = client.chat.completions.create.mock.calls;
77
+ const [requestParams] = calls[calls.length - 1] as [Record<string, unknown>];
78
+ return requestParams;
79
+ }
80
+
61
81
  const NESTED_TOOL: IToolSchema = {
62
82
  name: 'create_user',
63
83
  description: 'Creates a user',
@@ -134,3 +154,30 @@ describe('MCP-005 TC-08 — the non-strict path carries the permissive projectio
134
154
  expect(tool.parameters).toEqual(NESTED_TOOL.parameters);
135
155
  });
136
156
  });
157
+
158
+ describe('strictTools on the Chat Completions surface (#3209)', () => {
159
+ const GATEWAY = { apiKey: 'sk-test', baseURL: 'https://gateway.example/v1' };
160
+
161
+ it('declares each function strict, with the same closed schema the Responses surface sends', async () => {
162
+ const provider = new OpenAIProvider({ ...GATEWAY, strictTools: true });
163
+ const requestParams = await sendChatCompletions(provider, [NESTED_TOOL]);
164
+ const [tool] = requestParams.tools as Array<{ function: Record<string, unknown> }>;
165
+
166
+ expect(tool?.function.strict).toBe(true);
167
+ expect(tool?.function.parameters).toEqual(
168
+ closeObjectSchemas(NESTED_TOOL.parameters, {
169
+ requireAllProperties: true,
170
+ optionalAsNullable: true,
171
+ }),
172
+ );
173
+ });
174
+
175
+ it('sends no strict key when strictTools is off', async () => {
176
+ const provider = new OpenAIProvider(GATEWAY);
177
+ const requestParams = await sendChatCompletions(provider, [NESTED_TOOL]);
178
+ const [tool] = requestParams.tools as Array<{ function: Record<string, unknown> }>;
179
+
180
+ expect(tool?.function).not.toHaveProperty('strict');
181
+ expect(tool?.function.parameters).toEqual(NESTED_TOOL.parameters);
182
+ });
183
+ });
@@ -113,16 +113,23 @@ interface ISentTool {
113
113
  name: string;
114
114
  description: string;
115
115
  parameters: unknown;
116
+ /** Whether the tool was declared strict on the wire (absent counts as not strict). */
117
+ strict: boolean;
116
118
  }
117
119
 
118
120
  function sentToolsFromResponses(client: IFakeClient): ISentTool[] {
119
121
  const [requestParams] = client.responses.create.mock.calls[
120
122
  client.responses.create.mock.calls.length - 1
121
- ] as [{ tools?: Array<{ name: string; description: string; parameters: unknown }> }];
123
+ ] as [
124
+ {
125
+ tools?: Array<{ name: string; description: string; parameters: unknown; strict?: boolean }>;
126
+ },
127
+ ];
122
128
  return (requestParams.tools ?? []).map((tool) => ({
123
129
  name: tool.name,
124
130
  description: tool.description,
125
131
  parameters: tool.parameters,
132
+ strict: tool.strict === true,
126
133
  }));
127
134
  }
128
135
 
@@ -131,13 +138,16 @@ function sentToolsFromChatCompletions(client: IFakeClient): ISentTool[] {
131
138
  client.chat.completions.create.mock.calls.length - 1
132
139
  ] as [
133
140
  {
134
- tools?: Array<{ function: { name: string; description: string; parameters: unknown } }>;
141
+ tools?: Array<{
142
+ function: { name: string; description: string; parameters: unknown; strict?: boolean };
143
+ }>;
135
144
  },
136
145
  ];
137
146
  return (requestParams.tools ?? []).map((tool) => ({
138
147
  name: tool.function.name,
139
148
  description: tool.function.description,
140
149
  parameters: tool.function.parameters,
150
+ strict: tool.function.strict === true,
141
151
  }));
142
152
  }
143
153
 
@@ -173,6 +183,7 @@ describe.each([
173
183
 
174
184
  const sent = sentTools(client);
175
185
  expect(sent).toHaveLength(3);
186
+ expect(sent.every((tool) => !tool.strict)).toBe(true);
176
187
  for (const [index, tool] of tools.entries()) {
177
188
  const projection = projectToolSchema(tool, PERMISSIVE_PROFILE);
178
189
  expect(sent[index]?.parameters).toEqual(projection.tool.parameters);
@@ -242,6 +253,7 @@ describe.each([
242
253
 
243
254
  const sent = sentTools(client);
244
255
  expect(sent.map((tool) => tool.name)).toEqual(['good_tool_one', 'good_tool_two']);
256
+ expect(sent.every((tool) => !tool.strict)).toBe(true);
245
257
  });
246
258
  },
247
259
  );
@@ -274,6 +286,7 @@ describe.each([
274
286
  const projection = projectToolSchema(tool, STRICT_PROFILE);
275
287
  expect(sent[index]?.parameters).toEqual(projection.tool.parameters);
276
288
  expect(sent[index]?.description).toEqual(projection.tool.description);
289
+ expect(sent[index]?.strict).toBe(true);
277
290
  }
278
291
  });
279
292
 
@@ -297,6 +310,7 @@ describe.each([
297
310
  const sent = sentTools(client);
298
311
  const projection = projectToolSchema(fixtures.subsetOnly, STRICT_PROFILE);
299
312
  expect(sent[0]?.parameters).toEqual(projection.tool.parameters);
313
+ expect(sent[0]?.strict).toBe(true);
300
314
  });
301
315
  },
302
316
  );
@@ -56,7 +56,7 @@ export async function chatWithOpenAIChatCompletions(
56
56
  payloadKind: 'request',
57
57
  payload: requestParams,
58
58
  });
59
- const requestOptions = openAIRequestOptions(undefined, input.requestHeaders);
59
+ const requestOptions = openAIRequestOptions(input.chatOptions?.signal, input.requestHeaders);
60
60
  const response = requestOptions
61
61
  ? await client.chat.completions.create(requestParams, requestOptions)
62
62
  : await client.chat.completions.create(requestParams);
@@ -151,7 +151,7 @@ function buildChatRequestParams(
151
151
  }),
152
152
  ...(input.chatOptions?.maxTokens !== undefined && { max_tokens: input.chatOptions.maxTokens }),
153
153
  ...(input.chatOptions?.tools && {
154
- tools: convertToOpenAITools(input.chatOptions.tools),
154
+ tools: convertToOpenAITools(input.chatOptions.tools, input.providerOptions.strictTools),
155
155
  tool_choice: toOpenAICompatibleToolChoice(input.chatOptions.toolChoice),
156
156
  }),
157
157
  ...(responseFormat !== undefined && { response_format: responseFormat }),
@@ -2,7 +2,9 @@ import type { IOpenAILogData } from '../types/api-types';
2
2
  import type { ILogger } from '@robota-sdk/agent-core';
3
3
 
4
4
  /**
5
- * IPayloadLogger interface for logging OpenAI API payloads
5
+ * IPayloadLogger interface for logging a summary of each OpenAI Chat Completions request
6
+ * (request metadata such as model, message count and whether tools were sent — not prompt or
7
+ * response content).
6
8
  *
7
9
  * This interface provides a contract for different logging implementations:
8
10
  * - FilePayloadLogger: Node.js file-based logging
@@ -17,8 +19,8 @@ export interface IPayloadLogger {
17
19
  isEnabled(): boolean;
18
20
 
19
21
  /**
20
- * Log API payload data
21
- * @param payload - The API request/response payload data
22
+ * Log a request summary
23
+ * @param payload - Summary of the outgoing Chat Completions request
22
24
  * @param type - Type of operation ('chat' or 'stream')
23
25
  */
24
26
  logPayload(payload: IOpenAILogData, type: 'chat' | 'stream'): Promise<void>;