@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.
- package/CHANGELOG.md +207 -0
- package/README.md +110 -0
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.cts +8 -3
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +8 -3
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -1
- package/dist/node/index.js.map +1 -1
- package/dist/node/loggers/index.d.cts +6 -6
- package/dist/node/loggers/index.d.cts.map +1 -1
- package/dist/node/loggers/index.d.ts +6 -6
- package/dist/node/loggers/index.d.ts.map +1 -1
- package/dist/node/loggers/index.js.map +1 -1
- package/dist/node/{payload-logger-BaW0K8yI.d.ts → payload-logger-n89AQdUn.d.cts} +6 -4
- package/dist/node/payload-logger-n89AQdUn.d.cts.map +1 -0
- package/dist/node/{payload-logger-BaW0K8yI.d.cts → payload-logger-n89AQdUn.d.ts} +6 -4
- package/dist/node/payload-logger-n89AQdUn.d.ts.map +1 -0
- package/package.json +10 -5
- package/src/openai/__tests__/abort-signal-wire.test.ts +210 -0
- package/src/openai/__tests__/strict-tools-closure.test.ts +49 -2
- package/src/openai/__tests__/tool-schema-projection.test.ts +16 -2
- package/src/openai/chat-completions-chat.ts +2 -2
- package/src/openai/interfaces/payload-logger.ts +5 -3
- package/src/openai/loggers/console-payload-logger.ts +2 -2
- package/src/openai/loggers/file-payload-logger.ts +6 -5
- package/src/openai/message-converter.ts +7 -3
- package/src/openai/responses-parser.test.ts +87 -0
- package/src/openai/responses-parser.ts +15 -9
- package/src/openai/responses-types.ts +2 -0
- package/src/openai/types.ts +7 -2
- package/dist/node/payload-logger-BaW0K8yI.d.cts.map +0 -1
- 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-
|
|
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
|
|
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
|
|
43
|
-
* @param 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
|
|
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 -
|
|
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;;;;
|
|
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-
|
|
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
|
|
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
|
|
43
|
-
* @param 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
|
|
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 -
|
|
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;;;;
|
|
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
|
|
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
|
|
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
|
|
34
|
-
* @param payload -
|
|
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-
|
|
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
|
|
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
|
|
34
|
-
* @param payload -
|
|
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-
|
|
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.
|
|
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.
|
|
79
|
-
"@robota-sdk/agent-provider-openai-compatible": "3.0.0-beta.
|
|
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
|
|
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:
|
|
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 [
|
|
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<{
|
|
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(
|
|
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
|
|
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
|
|
21
|
-
* @param payload -
|
|
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>;
|