@nexus-cortex/core 4.124.59 → 4.124.61

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.
@@ -2122,118 +2122,16 @@ export class APIClient {
2122
2122
  // Capture this context before entering generator
2123
2123
  const self = this;
2124
2124
  // Create async generator that both yields chunks AND accumulates final message
2125
- const chunks = async function* () {
2126
- if (process.env.DEBUG === 'true' || process.env.DEBUG_THINKING === 'true') {
2127
- console.log(`[DEBUG APIClient] OpenAI request:`, JSON.stringify(openaiRequest, null, 2));
2128
- }
2129
- const stream = await client.chat.completions.create(openaiRequest);
2130
- let chunkCount = 0;
2131
- for await (const chunk of stream) {
2132
- if (!firstChunk)
2133
- firstChunk = chunk;
2134
- chunkCount++;
2135
- if (captureStreamUsage && chunk?.usage)
2136
- streamUsage = chunk.usage;
2137
- const delta = chunk.choices?.[0]?.delta;
2138
- // Debug: Log first few chunks to see what fields are present
2139
- if ((process.env.DEBUG === 'true' || process.env.DEBUG_THINKING === 'true') && chunkCount <= 3) {
2140
- console.log(`[DEBUG APIClient] Chunk ${chunkCount} delta keys:`, delta ? Object.keys(delta) : 'null');
2141
- if (delta?.reasoning_content) {
2142
- console.log(`[DEBUG APIClient] Found reasoning_content in chunk ${chunkCount}`);
2143
- }
2144
- }
2145
- // Extract reasoning content (for models with native interleaved thinking)
2146
- // XAI: chunk.choices[0].delta.reasoning_content
2147
- // DeepSeek: chunk.choices[0].delta.reasoning_content
2148
- // GPT-5: chunk.choices[0].delta.reasoning_content (when reasoning_effort != 'none')
2149
- if (delta?.reasoning_content) {
2150
- fullReasoningContent += delta.reasoning_content;
2151
- yield {
2152
- type: 'content_block_delta',
2153
- delta: delta.reasoning_content,
2154
- data: { ...chunk, reasoning: true }
2155
- };
2156
- }
2157
- // Extract regular text content
2158
- if (delta?.content) {
2159
- fullContent += delta.content;
2160
- yield {
2161
- type: 'text_delta',
2162
- delta: delta.content,
2163
- data: chunk
2164
- };
2165
- }
2166
- // Phase 2.8: Handle streaming tool_calls
2167
- if (delta?.tool_calls && Array.isArray(delta.tool_calls)) {
2168
- for (const toolCallDelta of delta.tool_calls) {
2169
- const index = toolCallDelta.index;
2170
- // Initialize tool call accumulator if needed
2171
- if (!accumulatedToolCalls.has(index)) {
2172
- accumulatedToolCalls.set(index, { arguments: '' });
2173
- }
2174
- const accumulated = accumulatedToolCalls.get(index);
2175
- // Accumulate tool call properties
2176
- if (toolCallDelta.id) {
2177
- accumulated.id = toolCallDelta.id;
2178
- }
2179
- if (toolCallDelta.function?.name) {
2180
- accumulated.name = toolCallDelta.function.name;
2181
- }
2182
- if (toolCallDelta.function?.arguments) {
2183
- accumulated.arguments += toolCallDelta.function.arguments;
2184
- }
2185
- }
2186
- }
2187
- // Check if finish_reason indicates tool_calls completion (or length-truncated tool call)
2188
- const finishReason = chunk.choices?.[0]?.finish_reason;
2189
- if (finishReason)
2190
- lastFinishReason = String(finishReason);
2191
- if ((finishReason === 'tool_calls' || finishReason === 'length') && accumulatedToolCalls.size > 0) {
2192
- // Emit tool_use_complete events for each accumulated tool call
2193
- for (const [_, toolCall] of accumulatedToolCalls) {
2194
- if (toolCall.id && toolCall.name) {
2195
- try {
2196
- let input;
2197
- try {
2198
- input = JSON.parse(toolCall.arguments);
2199
- }
2200
- catch {
2201
- input = self.repairTruncatedJSON(toolCall.arguments);
2202
- }
2203
- const decodedInput = self.decodeHtmlEntitiesInObject(input);
2204
- yield {
2205
- type: 'tool_use_complete',
2206
- toolUse: {
2207
- id: toolCall.id,
2208
- name: toolCall.name,
2209
- input: decodedInput
2210
- },
2211
- data: chunk
2212
- };
2213
- }
2214
- catch (error) {
2215
- console.error(`[APIClient] Failed to parse tool arguments (${toolCall.arguments.length} chars): ${toolCall.arguments.slice(0, 200)}...`);
2216
- }
2217
- }
2218
- }
2219
- }
2220
- }
2221
- // Stream complete - log summary
2222
- if (process.env.DEBUG === 'true' || process.env.DEBUG_THINKING === 'true') {
2223
- console.log(`[DEBUG APIClient] Stream complete - total chunks: ${chunkCount}, reasoning content length: ${fullReasoningContent.length}, content length: ${fullContent.length}`);
2224
- }
2225
- // Stream complete - resolve final message in OpenAI Chat Completions format
2226
- // Include accumulated tool_calls in the final message
2227
- const finalToolCalls = Array.from(accumulatedToolCalls.values())
2228
- .filter(tc => tc.id && tc.name)
2229
- .map(tc => ({
2230
- id: tc.id,
2231
- type: 'function',
2232
- function: {
2233
- name: tc.name,
2234
- arguments: tc.arguments
2235
- }
2236
- }));
2125
+ // R238 (2026-10-06): `finalMessage` used to resolve ONLY at the natural end of the stream — a consumer that broke out of `chunks`
2126
+ // (abort, tail-loop guard, ESC) or a stream that threw left the promise pending forever. Any early exit now settles it with the
2127
+ // PARTIAL message (finish_reason 'aborted', content/reasoning/tool_calls accumulated so far, x_partial: true, an estimated output-token
2128
+ // count for accounting since the provider's usage chunk never arrives). The natural-end path is byte-identical.
2129
+ let streamCompletedNaturally = false;
2130
+ let streamError = undefined;
2131
+ const resolvePartial = () => {
2132
+ const partialToolCalls = Array.from(accumulatedToolCalls.values())
2133
+ .filter((tc) => tc.id && tc.name)
2134
+ .map((tc) => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: tc.arguments } }));
2237
2135
  finalMessageResolve({
2238
2136
  id: firstChunk?.id || 'chatcmpl-' + Date.now(),
2239
2137
  object: 'chat.completion',
@@ -2245,14 +2143,158 @@ export class APIClient {
2245
2143
  role: 'assistant',
2246
2144
  content: fullContent || null,
2247
2145
  ...(fullReasoningContent && { reasoning_content: fullReasoningContent }),
2248
- ...(finalToolCalls.length > 0 && { tool_calls: finalToolCalls })
2146
+ ...(partialToolCalls.length > 0 && { tool_calls: partialToolCalls })
2249
2147
  },
2250
- // R153: tool calls keep 'tool_calls' (downstream contract); otherwise report what the provider said
2251
- // ('length' = output cap hit) instead of a synthetic 'stop'.
2252
- finish_reason: finalToolCalls.length > 0 ? 'tool_calls' : (lastFinishReason ?? 'stop')
2148
+ finish_reason: 'aborted'
2253
2149
  }],
2254
- usage: streamFinalUsage(streamUsage)
2150
+ usage: streamFinalUsage(streamUsage),
2151
+ x_partial: true,
2152
+ x_estimated_output_tokens: Math.round((fullContent.length + fullReasoningContent.length) / 4),
2153
+ ...(streamError !== undefined ? { x_error: String(streamError?.message ?? streamError).slice(0, 300) } : {}),
2255
2154
  });
2155
+ };
2156
+ const chunks = async function* () {
2157
+ try {
2158
+ if (process.env.DEBUG === 'true' || process.env.DEBUG_THINKING === 'true') {
2159
+ console.log(`[DEBUG APIClient] OpenAI request:`, JSON.stringify(openaiRequest, null, 2));
2160
+ }
2161
+ const stream = await client.chat.completions.create(openaiRequest);
2162
+ let chunkCount = 0;
2163
+ for await (const chunk of stream) {
2164
+ if (!firstChunk)
2165
+ firstChunk = chunk;
2166
+ chunkCount++;
2167
+ if (captureStreamUsage && chunk?.usage)
2168
+ streamUsage = chunk.usage;
2169
+ const delta = chunk.choices?.[0]?.delta;
2170
+ // Debug: Log first few chunks to see what fields are present
2171
+ if ((process.env.DEBUG === 'true' || process.env.DEBUG_THINKING === 'true') && chunkCount <= 3) {
2172
+ console.log(`[DEBUG APIClient] Chunk ${chunkCount} delta keys:`, delta ? Object.keys(delta) : 'null');
2173
+ if (delta?.reasoning_content) {
2174
+ console.log(`[DEBUG APIClient] Found reasoning_content in chunk ${chunkCount}`);
2175
+ }
2176
+ }
2177
+ // Extract reasoning content (for models with native interleaved thinking)
2178
+ // XAI: chunk.choices[0].delta.reasoning_content
2179
+ // DeepSeek: chunk.choices[0].delta.reasoning_content
2180
+ // GPT-5: chunk.choices[0].delta.reasoning_content (when reasoning_effort != 'none')
2181
+ if (delta?.reasoning_content) {
2182
+ fullReasoningContent += delta.reasoning_content;
2183
+ yield {
2184
+ type: 'content_block_delta',
2185
+ delta: delta.reasoning_content,
2186
+ data: { ...chunk, reasoning: true }
2187
+ };
2188
+ }
2189
+ // Extract regular text content
2190
+ if (delta?.content) {
2191
+ fullContent += delta.content;
2192
+ yield {
2193
+ type: 'text_delta',
2194
+ delta: delta.content,
2195
+ data: chunk
2196
+ };
2197
+ }
2198
+ // Phase 2.8: Handle streaming tool_calls
2199
+ if (delta?.tool_calls && Array.isArray(delta.tool_calls)) {
2200
+ for (const toolCallDelta of delta.tool_calls) {
2201
+ const index = toolCallDelta.index;
2202
+ // Initialize tool call accumulator if needed
2203
+ if (!accumulatedToolCalls.has(index)) {
2204
+ accumulatedToolCalls.set(index, { arguments: '' });
2205
+ }
2206
+ const accumulated = accumulatedToolCalls.get(index);
2207
+ // Accumulate tool call properties
2208
+ if (toolCallDelta.id) {
2209
+ accumulated.id = toolCallDelta.id;
2210
+ }
2211
+ if (toolCallDelta.function?.name) {
2212
+ accumulated.name = toolCallDelta.function.name;
2213
+ }
2214
+ if (toolCallDelta.function?.arguments) {
2215
+ accumulated.arguments += toolCallDelta.function.arguments;
2216
+ }
2217
+ }
2218
+ }
2219
+ // Check if finish_reason indicates tool_calls completion (or length-truncated tool call)
2220
+ const finishReason = chunk.choices?.[0]?.finish_reason;
2221
+ if (finishReason)
2222
+ lastFinishReason = String(finishReason);
2223
+ if ((finishReason === 'tool_calls' || finishReason === 'length') && accumulatedToolCalls.size > 0) {
2224
+ // Emit tool_use_complete events for each accumulated tool call
2225
+ for (const [_, toolCall] of accumulatedToolCalls) {
2226
+ if (toolCall.id && toolCall.name) {
2227
+ try {
2228
+ let input;
2229
+ try {
2230
+ input = JSON.parse(toolCall.arguments);
2231
+ }
2232
+ catch {
2233
+ input = self.repairTruncatedJSON(toolCall.arguments);
2234
+ }
2235
+ const decodedInput = self.decodeHtmlEntitiesInObject(input);
2236
+ yield {
2237
+ type: 'tool_use_complete',
2238
+ toolUse: {
2239
+ id: toolCall.id,
2240
+ name: toolCall.name,
2241
+ input: decodedInput
2242
+ },
2243
+ data: chunk
2244
+ };
2245
+ }
2246
+ catch (error) {
2247
+ console.error(`[APIClient] Failed to parse tool arguments (${toolCall.arguments.length} chars): ${toolCall.arguments.slice(0, 200)}...`);
2248
+ }
2249
+ }
2250
+ }
2251
+ }
2252
+ }
2253
+ // Stream complete - log summary
2254
+ if (process.env.DEBUG === 'true' || process.env.DEBUG_THINKING === 'true') {
2255
+ console.log(`[DEBUG APIClient] Stream complete - total chunks: ${chunkCount}, reasoning content length: ${fullReasoningContent.length}, content length: ${fullContent.length}`);
2256
+ }
2257
+ // Stream complete - resolve final message in OpenAI Chat Completions format
2258
+ // Include accumulated tool_calls in the final message
2259
+ const finalToolCalls = Array.from(accumulatedToolCalls.values())
2260
+ .filter(tc => tc.id && tc.name)
2261
+ .map(tc => ({
2262
+ id: tc.id,
2263
+ type: 'function',
2264
+ function: {
2265
+ name: tc.name,
2266
+ arguments: tc.arguments
2267
+ }
2268
+ }));
2269
+ finalMessageResolve({
2270
+ id: firstChunk?.id || 'chatcmpl-' + Date.now(),
2271
+ object: 'chat.completion',
2272
+ created: firstChunk?.created || Math.floor(Date.now() / 1000),
2273
+ model: firstChunk?.model || request.modelId,
2274
+ choices: [{
2275
+ index: 0,
2276
+ message: {
2277
+ role: 'assistant',
2278
+ content: fullContent || null,
2279
+ ...(fullReasoningContent && { reasoning_content: fullReasoningContent }),
2280
+ ...(finalToolCalls.length > 0 && { tool_calls: finalToolCalls })
2281
+ },
2282
+ // R153: tool calls keep 'tool_calls' (downstream contract); otherwise report what the provider said
2283
+ // ('length' = output cap hit) instead of a synthetic 'stop'.
2284
+ finish_reason: finalToolCalls.length > 0 ? 'tool_calls' : (lastFinishReason ?? 'stop')
2285
+ }],
2286
+ usage: streamFinalUsage(streamUsage)
2287
+ });
2288
+ streamCompletedNaturally = true;
2289
+ }
2290
+ catch (err) {
2291
+ streamError = err;
2292
+ throw err; // the chunk consumer still sees the error, as before
2293
+ }
2294
+ finally {
2295
+ if (!streamCompletedNaturally)
2296
+ resolvePartial(); // R238: abort / early return / throw → settle with the partial message
2297
+ }
2256
2298
  }();
2257
2299
  return {
2258
2300
  chunks,