@x12i/ai-gateway 7.9.1

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 (179) hide show
  1. package/README.md +4259 -0
  2. package/config.defaults.json +31 -0
  3. package/dist/activity-manager.d.ts +206 -0
  4. package/dist/activity-manager.js +1051 -0
  5. package/dist/config/activity-tracking-config.d.ts +11 -0
  6. package/dist/config/activity-tracking-config.js +15 -0
  7. package/dist/config.defaults.json +31 -0
  8. package/dist/content-normalizer/content-normalizer.d.ts +46 -0
  9. package/dist/content-normalizer/content-normalizer.js +393 -0
  10. package/dist/content-normalizer/index.d.ts +7 -0
  11. package/dist/content-normalizer/index.js +6 -0
  12. package/dist/content-normalizer/types.d.ts +33 -0
  13. package/dist/content-normalizer/types.js +4 -0
  14. package/dist/defaults/instructions-blocks.json +61 -0
  15. package/dist/defaults/model-config.json +16 -0
  16. package/dist/defaults/template-rendering.json +6 -0
  17. package/dist/flex-md-loader.d.ts +109 -0
  18. package/dist/flex-md-loader.js +940 -0
  19. package/dist/gateway-config.d.ts +49 -0
  20. package/dist/gateway-config.js +292 -0
  21. package/dist/gateway-conversion.d.ts +29 -0
  22. package/dist/gateway-conversion.js +174 -0
  23. package/dist/gateway-instructions.d.ts +30 -0
  24. package/dist/gateway-instructions.js +62 -0
  25. package/dist/gateway-memory.d.ts +51 -0
  26. package/dist/gateway-memory.js +207 -0
  27. package/dist/gateway-messages.d.ts +23 -0
  28. package/dist/gateway-messages.js +83 -0
  29. package/dist/gateway-meta.d.ts +25 -0
  30. package/dist/gateway-meta.js +87 -0
  31. package/dist/gateway-provider-auto-register.d.ts +17 -0
  32. package/dist/gateway-provider-auto-register.js +159 -0
  33. package/dist/gateway-provider.d.ts +54 -0
  34. package/dist/gateway-provider.js +202 -0
  35. package/dist/gateway-rate-limiter-constants.d.ts +16 -0
  36. package/dist/gateway-rate-limiter-constants.js +16 -0
  37. package/dist/gateway-rate-limiter.d.ts +56 -0
  38. package/dist/gateway-rate-limiter.js +107 -0
  39. package/dist/gateway-retry.d.ts +49 -0
  40. package/dist/gateway-retry.js +204 -0
  41. package/dist/gateway-utils.d.ts +21 -0
  42. package/dist/gateway-utils.js +181 -0
  43. package/dist/gateway-validation.d.ts +13 -0
  44. package/dist/gateway-validation.js +50 -0
  45. package/dist/gateway.d.ts +39 -0
  46. package/dist/gateway.js +430 -0
  47. package/dist/index.d.ts +36 -0
  48. package/dist/index.js +55 -0
  49. package/dist/instruction-errors.d.ts +16 -0
  50. package/dist/instruction-errors.js +29 -0
  51. package/dist/instruction-optimizer.d.ts +113 -0
  52. package/dist/instruction-optimizer.js +293 -0
  53. package/dist/instructions-parser.d.ts +31 -0
  54. package/dist/instructions-parser.js +56 -0
  55. package/dist/logger-factory.d.ts +17 -0
  56. package/dist/logger-factory.js +42 -0
  57. package/dist/message-builder.d.ts +41 -0
  58. package/dist/message-builder.js +522 -0
  59. package/dist/object-types-library-integration.d.ts +22 -0
  60. package/dist/object-types-library-integration.js +27 -0
  61. package/dist/object-types-library.d.ts +351 -0
  62. package/dist/object-types-library.js +210 -0
  63. package/dist/output-auditor.d.ts +44 -0
  64. package/dist/output-auditor.js +49 -0
  65. package/dist/request-report-generator.d.ts +60 -0
  66. package/dist/request-report-generator.js +169 -0
  67. package/dist/response-analyzer/format-type-detector.d.ts +35 -0
  68. package/dist/response-analyzer/format-type-detector.js +115 -0
  69. package/dist/response-analyzer/index.d.ts +9 -0
  70. package/dist/response-analyzer/index.js +8 -0
  71. package/dist/response-analyzer/object-type-detector.d.ts +42 -0
  72. package/dist/response-analyzer/object-type-detector.js +95 -0
  73. package/dist/response-analyzer/response-analyzer.d.ts +38 -0
  74. package/dist/response-analyzer/response-analyzer.js +97 -0
  75. package/dist/response-analyzer/types.d.ts +97 -0
  76. package/dist/response-analyzer/types.js +4 -0
  77. package/dist/response-fallback-fixer.d.ts +11 -0
  78. package/dist/response-fallback-fixer.js +123 -0
  79. package/dist/runtime-objects.d.ts +52 -0
  80. package/dist/runtime-objects.js +46 -0
  81. package/dist/template-parser.d.ts +58 -0
  82. package/dist/template-parser.js +99 -0
  83. package/dist/template-render-merge.d.ts +9 -0
  84. package/dist/template-render-merge.js +40 -0
  85. package/dist/troubleshooting-helper.d.ts +123 -0
  86. package/dist/troubleshooting-helper.js +596 -0
  87. package/dist/types.d.ts +1173 -0
  88. package/dist/types.js +6 -0
  89. package/dist/usage-tracker.d.ts +78 -0
  90. package/dist/usage-tracker.js +79 -0
  91. package/dist-cjs/activity-manager.cjs +1056 -0
  92. package/dist-cjs/activity-manager.d.ts +206 -0
  93. package/dist-cjs/config/activity-tracking-config.cjs +18 -0
  94. package/dist-cjs/config/activity-tracking-config.d.ts +11 -0
  95. package/dist-cjs/config.defaults.json +31 -0
  96. package/dist-cjs/content-normalizer/content-normalizer.cjs +398 -0
  97. package/dist-cjs/content-normalizer/content-normalizer.d.ts +46 -0
  98. package/dist-cjs/content-normalizer/index.cjs +12 -0
  99. package/dist-cjs/content-normalizer/index.d.ts +7 -0
  100. package/dist-cjs/content-normalizer/types.cjs +5 -0
  101. package/dist-cjs/content-normalizer/types.d.ts +33 -0
  102. package/dist-cjs/defaults/instructions-blocks.json +61 -0
  103. package/dist-cjs/defaults/model-config.json +16 -0
  104. package/dist-cjs/defaults/template-rendering.json +6 -0
  105. package/dist-cjs/flex-md-loader.cjs +986 -0
  106. package/dist-cjs/flex-md-loader.d.ts +109 -0
  107. package/dist-cjs/gateway-config.cjs +331 -0
  108. package/dist-cjs/gateway-config.d.ts +49 -0
  109. package/dist-cjs/gateway-conversion.cjs +212 -0
  110. package/dist-cjs/gateway-conversion.d.ts +29 -0
  111. package/dist-cjs/gateway-instructions.cjs +67 -0
  112. package/dist-cjs/gateway-instructions.d.ts +30 -0
  113. package/dist-cjs/gateway-memory.cjs +211 -0
  114. package/dist-cjs/gateway-memory.d.ts +51 -0
  115. package/dist-cjs/gateway-messages.cjs +86 -0
  116. package/dist-cjs/gateway-messages.d.ts +23 -0
  117. package/dist-cjs/gateway-meta.cjs +90 -0
  118. package/dist-cjs/gateway-meta.d.ts +25 -0
  119. package/dist-cjs/gateway-provider-auto-register.cjs +195 -0
  120. package/dist-cjs/gateway-provider-auto-register.d.ts +17 -0
  121. package/dist-cjs/gateway-provider.cjs +214 -0
  122. package/dist-cjs/gateway-provider.d.ts +54 -0
  123. package/dist-cjs/gateway-rate-limiter-constants.cjs +19 -0
  124. package/dist-cjs/gateway-rate-limiter-constants.d.ts +16 -0
  125. package/dist-cjs/gateway-rate-limiter.cjs +111 -0
  126. package/dist-cjs/gateway-rate-limiter.d.ts +56 -0
  127. package/dist-cjs/gateway-retry.cjs +212 -0
  128. package/dist-cjs/gateway-retry.d.ts +49 -0
  129. package/dist-cjs/gateway-utils.cjs +219 -0
  130. package/dist-cjs/gateway-utils.d.ts +21 -0
  131. package/dist-cjs/gateway-validation.cjs +54 -0
  132. package/dist-cjs/gateway-validation.d.ts +13 -0
  133. package/dist-cjs/gateway.cjs +434 -0
  134. package/dist-cjs/gateway.d.ts +39 -0
  135. package/dist-cjs/index.cjs +108 -0
  136. package/dist-cjs/index.d.ts +36 -0
  137. package/dist-cjs/instruction-errors.cjs +34 -0
  138. package/dist-cjs/instruction-errors.d.ts +16 -0
  139. package/dist-cjs/instruction-optimizer.cjs +299 -0
  140. package/dist-cjs/instruction-optimizer.d.ts +113 -0
  141. package/dist-cjs/instructions-parser.cjs +61 -0
  142. package/dist-cjs/instructions-parser.d.ts +31 -0
  143. package/dist-cjs/logger-factory.cjs +45 -0
  144. package/dist-cjs/logger-factory.d.ts +17 -0
  145. package/dist-cjs/message-builder.cjs +558 -0
  146. package/dist-cjs/message-builder.d.ts +41 -0
  147. package/dist-cjs/object-types-library-integration.cjs +32 -0
  148. package/dist-cjs/object-types-library-integration.d.ts +22 -0
  149. package/dist-cjs/object-types-library.cjs +215 -0
  150. package/dist-cjs/object-types-library.d.ts +351 -0
  151. package/dist-cjs/output-auditor.cjs +52 -0
  152. package/dist-cjs/output-auditor.d.ts +44 -0
  153. package/dist-cjs/request-report-generator.cjs +172 -0
  154. package/dist-cjs/request-report-generator.d.ts +60 -0
  155. package/dist-cjs/response-analyzer/format-type-detector.cjs +119 -0
  156. package/dist-cjs/response-analyzer/format-type-detector.d.ts +35 -0
  157. package/dist-cjs/response-analyzer/index.cjs +14 -0
  158. package/dist-cjs/response-analyzer/index.d.ts +9 -0
  159. package/dist-cjs/response-analyzer/object-type-detector.cjs +99 -0
  160. package/dist-cjs/response-analyzer/object-type-detector.d.ts +42 -0
  161. package/dist-cjs/response-analyzer/response-analyzer.cjs +101 -0
  162. package/dist-cjs/response-analyzer/response-analyzer.d.ts +38 -0
  163. package/dist-cjs/response-analyzer/types.cjs +5 -0
  164. package/dist-cjs/response-analyzer/types.d.ts +97 -0
  165. package/dist-cjs/response-fallback-fixer.cjs +126 -0
  166. package/dist-cjs/response-fallback-fixer.d.ts +11 -0
  167. package/dist-cjs/runtime-objects.cjs +52 -0
  168. package/dist-cjs/runtime-objects.d.ts +52 -0
  169. package/dist-cjs/template-parser.cjs +136 -0
  170. package/dist-cjs/template-parser.d.ts +58 -0
  171. package/dist-cjs/template-render-merge.cjs +43 -0
  172. package/dist-cjs/template-render-merge.d.ts +9 -0
  173. package/dist-cjs/troubleshooting-helper.cjs +611 -0
  174. package/dist-cjs/troubleshooting-helper.d.ts +123 -0
  175. package/dist-cjs/types.cjs +7 -0
  176. package/dist-cjs/types.d.ts +1173 -0
  177. package/dist-cjs/usage-tracker.cjs +83 -0
  178. package/dist-cjs/usage-tracker.d.ts +78 -0
  179. package/package.json +91 -0
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Activity Manager
3
+ *
4
+ * Manages activity tracking for LLM requests.
5
+ * Wraps the ActivityTracker and provides convenience methods.
6
+ */
7
+ import { Activix } from '@x12i/activix';
8
+ import type { Logxer } from '@x12i/logxer';
9
+ import type { ActivityIdentity, ChatRequest, AIRequest, FailureType, LLMResponseFailureSubtype, ResponseParsingFailureSubtype } from './types.js';
10
+ type Request = ChatRequest | AIRequest;
11
+ /**
12
+ * Normalizes and attaches the gateway `identity` envelope on the request (Activix + router correlation).
13
+ * Call at the API boundary before the router when activity tracking is off, and used internally when it is on.
14
+ *
15
+ * **Execution context:** treats `request.identity` as received from upstream. Root fields (`sessionId`,
16
+ * `instance`, and any keys already set on that object) are not replaced by gateway defaults; the gateway
17
+ * only fills missing required pieces and adds local context (e.g. graph linkage) where absent.
18
+ */
19
+ export declare function ensureGatewayRequestIdentity(request: ChatRequest | AIRequest, extras?: Partial<ActivityIdentity>): ActivityIdentity;
20
+ type ActivityMetadata = Record<string, any> & {
21
+ activityId?: string;
22
+ recordId?: string;
23
+ aiRequestId: string;
24
+ runContext?: ActivityIdentity;
25
+ status?: string;
26
+ activityType?: string;
27
+ };
28
+ export interface ActivityManagerConfig {
29
+ enableActivityTracking: boolean;
30
+ customTracker?: Activix;
31
+ logger: Logxer;
32
+ }
33
+ /**
34
+ * Manages activity tracking lifecycle
35
+ */
36
+ export declare class ActivityManager {
37
+ private activix?;
38
+ private initPromise?;
39
+ private mainCollectionName?;
40
+ private skillExecutionsCollectionName;
41
+ private badRequestsCollectionName?;
42
+ private logger;
43
+ constructor(config: ActivityManagerConfig);
44
+ /**
45
+ * Gets upstream-provided AI request ID
46
+ *
47
+ * @param request - Enhanced LLM request
48
+ * @returns AI request ID string
49
+ */
50
+ generateJobId(request: Request): string;
51
+ /**
52
+ * Starts activity tracking for a request
53
+ *
54
+ * IMPORTANT: This method ONLY sends REQUEST data (no response).
55
+ * The same record will be updated later via logSuccess() or logFailure()
56
+ * with response/timing data.
57
+ *
58
+ * KEY CONCEPT: jobId is NOT the unique identifier!
59
+ * - jobId = entire job (can have 100+ activities sharing the same jobId)
60
+ * - taskId = task within a job (can have multiple activities)
61
+ * - Each activity gets its own unique identifier (_id/recordId) from ActivityTracker
62
+ * - The returned ActivityMetadata contains this unique identifier for later updates
63
+ *
64
+ * This ensures:
65
+ * - On start: Only request data is stored → creates NEW record with unique _id
66
+ * - On finish: Only response data is added → updates SAME record by _id/recordId
67
+ * - No duplication: Request data sent once, response data sent once
68
+ * - Multiple activities per job: Each activity is a separate record, grouped by jobId
69
+ *
70
+ * @param request - Enhanced LLM request
71
+ * @param startTime - Request start timestamp
72
+ * @returns Activity metadata (contains unique _id/recordId) or undefined if tracking is disabled
73
+ */
74
+ startActivity(request: Request, startTime: number): Promise<ActivityMetadata | undefined>;
75
+ /**
76
+ * Starts skill execution activity tracking
77
+ *
78
+ * Similar to startActivity() but creates a skill-execution activity record instead of gateway-invocation.
79
+ * Skill executions are tracked separately and stored in the 'skill-executions' collection.
80
+ *
81
+ * @param request - Enhanced LLM request (must be a skill execution)
82
+ * @param instructionMetadata - Optional audit fields when caller tracks instruction catalog info
83
+ * @param startTime - Request start timestamp
84
+ * @returns Activity metadata (contains unique _id/recordId) or undefined if tracking is disabled
85
+ */
86
+ startSkillExecution(request: Request, instructionMetadata: {
87
+ key?: string;
88
+ version?: string;
89
+ rawContent?: string;
90
+ } | undefined, startTime: number): Promise<ActivityMetadata | undefined>;
91
+ /**
92
+ * Logs successful activity completion
93
+ *
94
+ * IMPORTANT: This method ONLY sends RESPONSE/TIMING data (no request data).
95
+ * It updates the SAME record created by startActivity() using the unique record identifier.
96
+ *
97
+ * KEY CONCEPT: jobId is NOT the unique identifier!
98
+ * - jobId = entire job (can have 100+ activities sharing the same jobId)
99
+ * - taskId = task within a job (can have multiple activities)
100
+ * - Each activity has its own unique identifier (_id/recordId) returned by startActivity()
101
+ *
102
+ * This ensures:
103
+ * - On start: Only request data is stored (via startActivity) → creates new record with unique _id
104
+ * - On finish: Only response data is added (via logSuccess) → updates same record by _id/recordId
105
+ * - No duplication: Request data sent once, response data sent once
106
+ * - Same record: Updated by unique identifier (_id/recordId), NOT by jobId
107
+ *
108
+ * Structure sent to ActivityTracker:
109
+ * - response: { content, metadata } (response object)
110
+ * - endTime, duration (timing completion)
111
+ * - cost (cost calculation)
112
+ * - status: 'success' (updated by ActivityTracker)
113
+ * - NO request data (already stored in startActivity)
114
+ * - NO config data (already stored in startActivity)
115
+ *
116
+ * @param activity - Activity metadata from startActivity() (contains unique _id/recordId for record lookup)
117
+ * @param details - Success details (cost, response, timing)
118
+ */
119
+ logSuccess(activity: ActivityMetadata | undefined, details: {
120
+ cost?: number;
121
+ response: any;
122
+ endTime: number;
123
+ duration: number;
124
+ }): Promise<void>;
125
+ /**
126
+ * Logs failed activity
127
+ *
128
+ * IMPORTANT: This method ONLY sends ERROR/TIMING data (no request/response data).
129
+ * It updates the SAME record created by startActivity() using the unique record identifier.
130
+ *
131
+ * KEY CONCEPT: jobId is NOT the unique identifier!
132
+ * - jobId = entire job (can have 100+ activities sharing the same jobId)
133
+ * - taskId = task within a job (can have multiple activities)
134
+ * - Each activity has its own unique identifier (_id/recordId) returned by startActivity()
135
+ *
136
+ * Structure sent to ActivityTracker:
137
+ * - error (error message)
138
+ * - endTime, duration (timing completion)
139
+ * - status: 'failed' (updated by ActivityTracker)
140
+ * - NO request data (already stored in startActivity)
141
+ * - NO response data (request failed)
142
+ *
143
+ * @param activity - Activity metadata from startActivity() (contains unique _id/recordId for record lookup)
144
+ * @param error - Error that occurred
145
+ * @param details - Failure details (timing, error message)
146
+ */
147
+ logFailure(activity: ActivityMetadata | undefined, error: Error, details: {
148
+ endTime: number;
149
+ duration: number;
150
+ error: string;
151
+ failureType?: FailureType;
152
+ failureSubtype?: LLMResponseFailureSubtype | ResponseParsingFailureSubtype;
153
+ response?: any;
154
+ }): Promise<void>;
155
+ /**
156
+ * Logs a bad request (error that occurred before startActivity)
157
+ *
158
+ * This method creates a new activity record specifically for tracking validation
159
+ * errors, format extraction failures, and other errors that occur before the
160
+ * normal activity lifecycle begins.
161
+ *
162
+ * Uses native ActivityTracker support for bad requests collection via the
163
+ * `collectionName` option in `startActivity()` and `logFailure()` methods.
164
+ * The bad requests collection is configured via `badRequestsCollectionName` in
165
+ * the ActivityTracker constructor (defaults to 'ai-bad-requests').
166
+ *
167
+ * Bad requests are automatically routed to the separate bad requests collection
168
+ * and can be filtered by `failureType` field:
169
+ * - 'validation-failure': Request validation errors
170
+ * - 'format-extraction-failure': Format extraction errors
171
+ * - 'configuration-failure': Configuration errors
172
+ *
173
+ * @param request - The request that failed
174
+ * @param error - The error that occurred
175
+ * @param details - Additional error details
176
+ * @param startTime - When the request started
177
+ */
178
+ logBadRequest(request: Request, error: Error, details: {
179
+ endTime: number;
180
+ duration: number;
181
+ error: string;
182
+ errorType?: string;
183
+ diagnosticInfo?: Record<string, any>;
184
+ failureType?: 'validation-failure' | 'format-extraction-failure' | 'configuration-failure';
185
+ }, startTime: number): Promise<void>;
186
+ /**
187
+ * Gets the underlying activity tracker instance
188
+ *
189
+ * @returns Activix instance or undefined if not enabled
190
+ */
191
+ getTracker(): Activix | undefined;
192
+ /**
193
+ * Get status of activity tracker
194
+ */
195
+ getStatus(): {
196
+ activityTracking: {
197
+ enabled: boolean;
198
+ tracker?: any;
199
+ };
200
+ };
201
+ /**
202
+ * Shutdown tracker
203
+ */
204
+ shutdown(): Promise<void>;
205
+ }
206
+ export {};
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ /**
3
+ * Centralized activity tracking configuration.
4
+ * Single source of truth for package-level collection names.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.resolveActivityTrackingConfig = resolveActivityTrackingConfig;
8
+ const ACTIVITY_COLLECTION_NAME = 'ai-activities';
9
+ const BAD_REQUESTS_COLLECTION_NAME = 'bad-requests';
10
+ function resolveActivityTrackingConfig() {
11
+ // Collection names are intentionally hardcoded at package level.
12
+ return {
13
+ mongoUri: '',
14
+ databaseName: '',
15
+ collectionName: ACTIVITY_COLLECTION_NAME,
16
+ badRequestsCollectionName: BAD_REQUESTS_COLLECTION_NAME
17
+ };
18
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Centralized activity tracking configuration.
3
+ * Single source of truth for package-level collection names.
4
+ */
5
+ export interface ActivityTrackingConfig {
6
+ mongoUri: string;
7
+ databaseName: string;
8
+ collectionName: string;
9
+ badRequestsCollectionName: string;
10
+ }
11
+ export declare function resolveActivityTrackingConfig(): ActivityTrackingConfig;
@@ -0,0 +1,31 @@
1
+ {
2
+ "contentRegistry": {
3
+ "mode": "dev",
4
+ "localRoot": ".metadata",
5
+ "github": {
6
+ "token": null,
7
+ "repo": null
8
+ },
9
+ "s3": {
10
+ "bucket": null,
11
+ "region": null
12
+ },
13
+ "redis": {
14
+ "host": null,
15
+ "port": null
16
+ },
17
+ "cache": {
18
+ "ttl": 3600000
19
+ }
20
+ },
21
+ "logging": {
22
+ "level": "info",
23
+ "enabled": true
24
+ },
25
+ "templateRendering": {
26
+ "subPathSearch": {
27
+ "enabled": false,
28
+ "roots": ["execution", "input", "inputs"]
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,398 @@
1
+ "use strict";
2
+ /**
3
+ * Content Normalizer
4
+ *
5
+ * Extracts and normalizes content from LLM provider responses.
6
+ * Fixes the "[object Object]" bug by properly handling objects.
7
+ * Preserves both rawText and parsedContent for downstream consumers.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.normalizeContent = normalizeContent;
11
+ exports.isEmptyContent = isEmptyContent;
12
+ exports.getResponseDiagnostics = getResponseDiagnostics;
13
+ /**
14
+ * Extracts content from router response, checking multiple possible locations
15
+ *
16
+ * @param response - Router response object
17
+ * @returns Extracted content value (any type) or null
18
+ */
19
+ function extractContentValue(response) {
20
+ // Check multiple possible locations
21
+ // CRITICAL: Check outputText FIRST (router's standard field) before other fields
22
+ return response.outputText ?? // Router's standard output field (check first)
23
+ response.content ??
24
+ response.rawText ??
25
+ response.output ??
26
+ response.choices?.[0]?.message?.content ??
27
+ response.choices?.[0]?.text ??
28
+ response.message?.content ??
29
+ response.text ??
30
+ response.result ??
31
+ null;
32
+ }
33
+ /**
34
+ * Extracts rawText from router response
35
+ *
36
+ * @param response - Router response object
37
+ * @param contentValue - The extracted content value (to avoid re-extraction)
38
+ * @returns Raw text string or undefined
39
+ */
40
+ function extractRawText(response, contentValue) {
41
+ // Try to get rawText from router response (preferred - original from provider)
42
+ // Check multiple possible locations where router might store rawText
43
+ const rawText = response.rawText || // Direct rawText field (preferred)
44
+ response.outputText || // Router's standard output field (if rawText not available)
45
+ response.raw || // Some providers use 'raw'
46
+ response.rawContent || // Some providers use 'rawContent'
47
+ response.originalText || // Some providers use 'originalText'
48
+ response.originalContent || // Some providers use 'originalContent'
49
+ response.choices?.[0]?.message?.rawText || // OpenAI-style nested rawText
50
+ response.choices?.[0]?.rawText || // OpenAI-style direct rawText
51
+ response.message?.rawText || // Message-level rawText
52
+ response.output?.rawText; // Output-level rawText
53
+ if (typeof rawText === 'string' && rawText.trim().length > 0) {
54
+ return rawText;
55
+ }
56
+ // If no rawText found, but content is a string, use it as rawText
57
+ // This ensures we always have something to work with
58
+ // If content is already a string, check if it's a JSON string with wrapped prose
59
+ if (typeof contentValue === 'string' && contentValue.trim().length > 0) {
60
+ // Check if it's a JSON string that might contain wrapped prose
61
+ const trimmed = contentValue.trim();
62
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
63
+ (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
64
+ try {
65
+ const parsed = JSON.parse(trimmed);
66
+ // If it's a wrapped prose object (has _wrapped and text fields), extract the text
67
+ if (parsed && typeof parsed === 'object' && parsed._wrapped === true && typeof parsed.text === 'string') {
68
+ return parsed.text;
69
+ }
70
+ // Otherwise, return the original string (it's a valid JSON string)
71
+ return contentValue;
72
+ }
73
+ catch {
74
+ // Not valid JSON, return as-is
75
+ return contentValue;
76
+ }
77
+ }
78
+ // Not JSON, return as-is
79
+ return contentValue;
80
+ }
81
+ // If content is an object/array, check for wrapped prose first
82
+ if (contentValue != null && typeof contentValue === 'object') {
83
+ // Check if it's a wrapped prose object (has _wrapped and text fields)
84
+ if (contentValue._wrapped === true && typeof contentValue.text === 'string') {
85
+ return contentValue.text;
86
+ }
87
+ // Otherwise, stringify it to get the JSON string
88
+ // This handles the case where router doesn't preserve rawText but returns parsed object
89
+ try {
90
+ return JSON.stringify(contentValue);
91
+ }
92
+ catch {
93
+ // If stringify fails, return undefined
94
+ return undefined;
95
+ }
96
+ }
97
+ return undefined;
98
+ }
99
+ /**
100
+ * Attempts to parse content as JSON
101
+ * Always returns a structure - forces JSON structure even if parsing fails
102
+ *
103
+ * @param value - Value to parse
104
+ * @param forceStructure - If true, always return an object/array structure (default: true)
105
+ * @returns Parsed object/array or forced structure
106
+ */
107
+ function tryParseJSON(value, forceStructure = true) {
108
+ if (value == null) {
109
+ // Force structure: return empty object instead of undefined
110
+ return forceStructure ? {} : undefined;
111
+ }
112
+ // If already an object or array, return it
113
+ if (typeof value === 'object' && !Array.isArray(value)) {
114
+ return value;
115
+ }
116
+ if (Array.isArray(value)) {
117
+ return value;
118
+ }
119
+ // If it's a string, try to parse it
120
+ if (typeof value === 'string') {
121
+ const trimmed = value.trim();
122
+ // Try to parse as JSON
123
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
124
+ (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
125
+ try {
126
+ return JSON.parse(trimmed);
127
+ }
128
+ catch {
129
+ // Not valid JSON - force structure if requested
130
+ if (forceStructure) {
131
+ // Wrap in object with the text as a value
132
+ return trimmed.startsWith('[') ? [] : { value: trimmed, _parseError: true };
133
+ }
134
+ return undefined;
135
+ }
136
+ }
137
+ // If forceStructure and it's not JSON-like, wrap it
138
+ if (forceStructure && trimmed.length > 0) {
139
+ return { text: trimmed, _wrapped: true };
140
+ }
141
+ }
142
+ // For other types (number, boolean, etc.), wrap in object if forceStructure
143
+ if (forceStructure && value != null) {
144
+ return { value, _wrapped: true };
145
+ }
146
+ return undefined;
147
+ }
148
+ /**
149
+ * Determines content type
150
+ *
151
+ * @param value - Content value
152
+ * @param parsedContent - Parsed content (if available)
153
+ * @returns Content type classification
154
+ */
155
+ function determineContentType(value, parsedContent) {
156
+ if (value == null || value === '') {
157
+ return 'null';
158
+ }
159
+ // Use parsedContent if available (more accurate)
160
+ if (parsedContent != null) {
161
+ if (Array.isArray(parsedContent)) {
162
+ return 'array';
163
+ }
164
+ if (typeof parsedContent === 'object') {
165
+ return 'object';
166
+ }
167
+ }
168
+ // Fall back to checking value directly
169
+ if (Array.isArray(value)) {
170
+ return 'array';
171
+ }
172
+ if (typeof value === 'object') {
173
+ return 'object';
174
+ }
175
+ return 'string';
176
+ }
177
+ /**
178
+ * Normalizes content from router response
179
+ *
180
+ * This function:
181
+ * 1. Extracts content from multiple possible locations
182
+ * 2. Preserves rawText if available
183
+ * 3. Attempts to parse flex-md content to JSON
184
+ * 4. Properly stringifies objects (fixes "[object Object]" bug)
185
+ * 5. Determines content type
186
+ *
187
+ * @param response - Router response object
188
+ * @returns Normalized content with metadata
189
+ */
190
+ function normalizeContent(response) {
191
+ // Extract content value from response
192
+ const contentValue = extractContentValue(response);
193
+ // CRITICAL: Check if router already converted object to "[object Object]" string
194
+ // This happens when router uses String() on objects before returning
195
+ if (typeof contentValue === 'string' && contentValue === '[object Object]') {
196
+ // Router already broke it - try to recover from other fields
197
+ console.error('[content-normalizer] Router returned "[object Object]" string - attempting recovery', {
198
+ responseKeys: Object.keys(response),
199
+ hasOutput: !!response.output,
200
+ hasRawText: !!response.rawText,
201
+ hasChoices: !!response.choices
202
+ });
203
+ // Try to get the original object from other fields
204
+ const output = response.output;
205
+ const choicesContent = response.choices?.[0]?.message?.content;
206
+ const messageContent = response.message?.content;
207
+ // Prefer output (parsed object from provider)
208
+ if (output && typeof output === 'object' && output !== null) {
209
+ const recovered = JSON.stringify(output);
210
+ return {
211
+ content: recovered,
212
+ rawText: recovered,
213
+ parsedContent: output,
214
+ contentType: Array.isArray(output) ? 'array' : 'object'
215
+ };
216
+ }
217
+ // Try choices content (might be object)
218
+ if (choicesContent && typeof choicesContent === 'object' && choicesContent !== null) {
219
+ const recovered = JSON.stringify(choicesContent);
220
+ return {
221
+ content: recovered,
222
+ rawText: recovered,
223
+ parsedContent: choicesContent,
224
+ contentType: Array.isArray(choicesContent) ? 'array' : 'object'
225
+ };
226
+ }
227
+ // Try message content (might be object)
228
+ if (messageContent && typeof messageContent === 'object' && messageContent !== null) {
229
+ const recovered = JSON.stringify(messageContent);
230
+ return {
231
+ content: recovered,
232
+ rawText: recovered,
233
+ parsedContent: messageContent,
234
+ contentType: Array.isArray(messageContent) ? 'array' : 'object'
235
+ };
236
+ }
237
+ // If we can't recover, log error and return empty
238
+ console.error('[content-normalizer] Cannot recover from "[object Object]" - no valid object found in response');
239
+ return {
240
+ content: '',
241
+ rawText: undefined,
242
+ parsedContent: undefined,
243
+ contentType: 'null'
244
+ };
245
+ }
246
+ // Extract rawText (original text from provider)
247
+ // Pass contentValue to avoid re-extraction and handle object case
248
+ const rawText = extractRawText(response, contentValue);
249
+ // Always parse flex-md to JSON
250
+ let parsedContent;
251
+ let contentType;
252
+ // Try to parse flex-md content to JSON
253
+ parsedContent = tryParseJSON(contentValue, true);
254
+ contentType = determineContentType(contentValue, parsedContent);
255
+ // Ensure parsedContent matches contentType - force structure consistency
256
+ // JSON is always JSON (object/array), text is always text (string), etc.
257
+ let finalParsedContent = parsedContent;
258
+ if (contentType === 'object') {
259
+ // Force object structure - always return an object
260
+ if (!finalParsedContent || typeof finalParsedContent !== 'object' || Array.isArray(finalParsedContent)) {
261
+ if (typeof contentValue === 'string' && contentValue.trim().length > 0) {
262
+ // Wrap text in object
263
+ finalParsedContent = { text: contentValue, _wrapped: true };
264
+ }
265
+ else if (Array.isArray(contentValue)) {
266
+ // Wrap array in object
267
+ finalParsedContent = { array: contentValue, _wrapped: true };
268
+ }
269
+ else {
270
+ // Empty object
271
+ finalParsedContent = {};
272
+ }
273
+ }
274
+ }
275
+ else if (contentType === 'array') {
276
+ // Force array structure - always return an array
277
+ if (!Array.isArray(finalParsedContent)) {
278
+ if (finalParsedContent && typeof finalParsedContent === 'object') {
279
+ // Wrap object in array
280
+ finalParsedContent = [finalParsedContent];
281
+ }
282
+ else if (typeof contentValue === 'string' && contentValue.trim().length > 0) {
283
+ // Wrap text in array
284
+ finalParsedContent = [contentValue];
285
+ }
286
+ else {
287
+ // Empty array
288
+ finalParsedContent = [];
289
+ }
290
+ }
291
+ }
292
+ else if (contentType === 'string') {
293
+ // For string type, parsedContent can be undefined (it's just text)
294
+ // But if we have an object, we should keep it for structure preservation
295
+ // Don't force anything - string content doesn't need parsedContent
296
+ finalParsedContent = parsedContent; // Keep as-is (may be undefined, which is fine for strings)
297
+ }
298
+ else {
299
+ // null type - no parsedContent
300
+ finalParsedContent = undefined;
301
+ }
302
+ // Normalize content to string (for backward compatibility)
303
+ // CRITICAL: This must NEVER return "[object Object]" - always stringify objects properly
304
+ let normalizedString;
305
+ if (contentValue == null || contentValue === '') {
306
+ normalizedString = '';
307
+ }
308
+ else if (typeof contentValue === 'string') {
309
+ normalizedString = contentValue;
310
+ }
311
+ else if (typeof contentValue === 'object') {
312
+ // Fix "[object Object]" bug: properly stringify objects
313
+ // This is the critical fix - never use String() on objects
314
+ try {
315
+ normalizedString = JSON.stringify(contentValue);
316
+ }
317
+ catch (error) {
318
+ // If JSON.stringify fails (circular reference, etc.), try to recover
319
+ // Use rawText if available, otherwise fallback (but log error)
320
+ if (rawText && typeof rawText === 'string') {
321
+ normalizedString = rawText;
322
+ }
323
+ else {
324
+ // Last resort: use String() but this should never happen
325
+ normalizedString = String(contentValue);
326
+ // This will be "[object Object]" but we've exhausted all options
327
+ console.error('[content-normalizer] Failed to stringify object and no rawText available', {
328
+ error,
329
+ contentValueType: typeof contentValue,
330
+ isArray: Array.isArray(contentValue)
331
+ });
332
+ }
333
+ }
334
+ }
335
+ else {
336
+ normalizedString = String(contentValue);
337
+ }
338
+ // Final safety check: if normalizedString is "[object Object]", try to recover
339
+ if (normalizedString === '[object Object]') {
340
+ // This should never happen, but if it does, try to recover
341
+ if (rawText && typeof rawText === 'string' && rawText !== '[object Object]') {
342
+ normalizedString = rawText;
343
+ }
344
+ else if (parsedContent) {
345
+ try {
346
+ normalizedString = JSON.stringify(parsedContent);
347
+ }
348
+ catch {
349
+ // If this also fails, we're stuck with "[object Object]"
350
+ console.error('[content-normalizer] Cannot recover from "[object Object]" - all recovery attempts failed');
351
+ }
352
+ }
353
+ }
354
+ // CRITICAL: Ensure rawText is always set
355
+ // Priority: extracted rawText > normalizedString > contentValue (if string) > empty string
356
+ let finalRawText = rawText;
357
+ if (!finalRawText || typeof finalRawText !== 'string' || finalRawText.trim().length === 0) {
358
+ // Fallback to normalizedString (which is always a string)
359
+ finalRawText = normalizedString;
360
+ }
361
+ // If we still don't have rawText and contentValue is a string, use it
362
+ if ((!finalRawText || finalRawText.trim().length === 0) && typeof contentValue === 'string' && contentValue.trim().length > 0) {
363
+ finalRawText = contentValue;
364
+ }
365
+ return {
366
+ content: normalizedString,
367
+ rawText: finalRawText, // Always set (never undefined)
368
+ parsedContent: finalParsedContent, // Use final parsed content with forced structure
369
+ contentType
370
+ };
371
+ }
372
+ /**
373
+ * Checks if content is empty
374
+ *
375
+ * @param normalized - Normalized content result
376
+ * @returns True if content is empty
377
+ */
378
+ function isEmptyContent(normalized) {
379
+ return !normalized.content || normalized.content.trim().length === 0;
380
+ }
381
+ /**
382
+ * Gets diagnostic information about response structure
383
+ * Useful for debugging when content extraction fails
384
+ *
385
+ * @param response - Router response object
386
+ * @returns Diagnostic information
387
+ */
388
+ function getResponseDiagnostics(response) {
389
+ return {
390
+ responseKeys: Object.keys(response),
391
+ hasMessage: !!(response?.message),
392
+ hasChoices: !!(response?.choices),
393
+ hasOutput: !!(response?.output),
394
+ hasRawText: !!(response?.rawText),
395
+ originalContent: response?.content,
396
+ originalContentType: typeof response?.content
397
+ };
398
+ }