@vybestack/llxprt-code-core 0.7.0-nightly.251218.472c013d2 → 0.7.0-nightly.251219.3619c584b

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.
@@ -25,6 +25,7 @@ import * as net from 'net';
25
25
  import { isKimiModel, isMistralModel, getToolIdStrategy, } from '../../tools/ToolIdStrategy.js';
26
26
  import { BaseProvider, } from '../BaseProvider.js';
27
27
  import { DebugLogger } from '../../debug/index.js';
28
+ import { flushRuntimeAuthScope, } from '../../auth/precedence.js';
28
29
  import { ToolFormatter } from '../../tools/ToolFormatter.js';
29
30
  import { convertToolsToOpenAI } from './schemaConverter.js';
30
31
  import { GemmaToolCallParser } from '../../parsers/TextToolCallParser.js';
@@ -36,15 +37,12 @@ import { resolveRuntimeAuthToken } from '../utils/authToken.js';
36
37
  import { filterOpenAIRequestParams } from './openaiRequestParams.js';
37
38
  import { ensureJsonSafe } from '../../utils/unicodeUtils.js';
38
39
  import { ToolCallPipeline } from './ToolCallPipeline.js';
39
- import { buildToolResponsePayload, EMPTY_TOOL_RESULT_PLACEHOLDER, } from '../utils/toolResponsePayload.js';
40
+ import { buildToolResponsePayload } from '../utils/toolResponsePayload.js';
40
41
  import { isLocalEndpoint } from '../utils/localEndpoint.js';
41
42
  import { filterThinkingForContext, thinkingToReasoningField, extractThinkingBlocks, } from '../reasoning/reasoningUtils.js';
42
43
  import { shouldDumpSDKContext, dumpSDKContext, } from '../utils/dumpSDKContext.js';
43
44
  import { extractCacheMetrics } from '../utils/cacheMetricsExtractor.js';
44
- const MAX_TOOL_RESPONSE_CHARS = 1024;
45
- const MAX_TOOL_RESPONSE_RETRY_CHARS = 512;
46
45
  const TOOL_ARGS_PREVIEW_LENGTH = 500;
47
- const TEXTUAL_TOOL_REPLAY_MODELS = new Set(['openrouter/polaris-alpha']);
48
46
  export class OpenAIProvider extends BaseProvider {
49
47
  textToolParser = new GemmaToolCallParser();
50
48
  toolCallPipeline = new ToolCallPipeline();
@@ -52,6 +50,38 @@ export class OpenAIProvider extends BaseProvider {
52
50
  getLogger() {
53
51
  return new DebugLogger('llxprt:provider:openai');
54
52
  }
53
+ async handleBucketFailoverOnPersistent429(options, logger) {
54
+ const failoverHandler = options.runtime?.config?.getBucketFailoverHandler();
55
+ if (!failoverHandler || !failoverHandler.isEnabled()) {
56
+ return { result: null };
57
+ }
58
+ logger.debug(() => 'Attempting bucket failover on persistent 429');
59
+ const success = await failoverHandler.tryFailover();
60
+ if (!success) {
61
+ logger.debug(() => 'Bucket failover failed - no more buckets available');
62
+ return { result: false };
63
+ }
64
+ const previousAuthToken = options.resolved.authToken;
65
+ try {
66
+ // Clear runtime-scoped auth cache so subsequent auth resolution can pick up the new bucket.
67
+ if (typeof options.runtime?.runtimeId === 'string') {
68
+ flushRuntimeAuthScope(options.runtime.runtimeId);
69
+ }
70
+ // Force re-resolution of the auth token after bucket failover.
71
+ options.resolved.authToken = '';
72
+ const refreshedAuthToken = await this.getAuthTokenForPrompt();
73
+ options.resolved.authToken = refreshedAuthToken;
74
+ // Rebuild client with fresh credentials from new bucket
75
+ const client = await this.getClient(options);
76
+ logger.debug(() => `Bucket failover successful, new bucket: ${failoverHandler.getCurrentBucket()}`);
77
+ return { result: true, client };
78
+ }
79
+ catch (error) {
80
+ options.resolved.authToken = previousAuthToken;
81
+ logger.debug(() => `Bucket failover auth refresh failed: ${error instanceof Error ? error.message : String(error)}`);
82
+ return { result: false };
83
+ }
84
+ }
55
85
  /**
56
86
  * @plan:PLAN-20251023-STATELESS-HARDENING.P08
57
87
  * @requirement:REQ-SP4-003
@@ -827,39 +857,6 @@ export class OpenAIProvider extends BaseProvider {
827
857
  }
828
858
  return JSON.stringify({ value: parameters });
829
859
  }
830
- determineToolReplayMode(model) {
831
- if (!model) {
832
- return 'native';
833
- }
834
- const normalized = model.toLowerCase();
835
- if (TEXTUAL_TOOL_REPLAY_MODELS.has(normalized)) {
836
- return 'textual';
837
- }
838
- return 'native';
839
- }
840
- describeToolCallForText(block) {
841
- const normalizedArgs = this.normalizeToolCallArguments(block.parameters);
842
- const preview = normalizedArgs.length > MAX_TOOL_RESPONSE_CHARS
843
- ? `${normalizedArgs.slice(0, MAX_TOOL_RESPONSE_CHARS)}… [truncated ${normalizedArgs.length - MAX_TOOL_RESPONSE_CHARS} chars]`
844
- : normalizedArgs;
845
- const callId = block.id ? ` ${this.normalizeToOpenAIToolId(block.id)}` : '';
846
- return `[TOOL CALL${callId ? ` ${callId}` : ''}] ${block.name ?? 'unknown_tool'} args=${preview}`;
847
- }
848
- describeToolResponseForText(block, config) {
849
- const payload = buildToolResponsePayload(block, config);
850
- const header = `[TOOL RESULT] ${payload.toolName ?? block.toolName ?? 'unknown_tool'} (${payload.status ?? 'unknown'})`;
851
- const bodyParts = [];
852
- if (payload.error) {
853
- bodyParts.push(`error: ${payload.error}`);
854
- }
855
- if (payload.result && payload.result !== EMPTY_TOOL_RESULT_PLACEHOLDER) {
856
- bodyParts.push(payload.result);
857
- }
858
- if (payload.limitMessage) {
859
- bodyParts.push(payload.limitMessage);
860
- }
861
- return bodyParts.length > 0 ? `${header}\n${bodyParts.join('\n')}` : header;
862
- }
863
860
  buildToolResponseContent(block, config) {
864
861
  const payload = buildToolResponsePayload(block, config);
865
862
  return ensureJsonSafe(JSON.stringify(payload));
@@ -911,107 +908,6 @@ export class OpenAIProvider extends BaseProvider {
911
908
  });
912
909
  return modified;
913
910
  }
914
- /**
915
- * Convert IContent array to OpenAI ChatCompletionMessageParam array
916
- */
917
- convertToOpenAIMessages(contents, mode = 'native', config) {
918
- const messages = [];
919
- for (const content of contents) {
920
- if (content.speaker === 'human') {
921
- // Convert human messages to user messages
922
- const textBlocks = content.blocks.filter((b) => b.type === 'text');
923
- const text = textBlocks.map((b) => b.text).join('\n');
924
- if (text) {
925
- messages.push({
926
- role: 'user',
927
- content: text,
928
- });
929
- }
930
- }
931
- else if (content.speaker === 'ai') {
932
- // Convert AI messages
933
- const textBlocks = content.blocks.filter((b) => b.type === 'text');
934
- const text = textBlocks.map((b) => b.text).join('\n');
935
- const toolCalls = content.blocks.filter((b) => b.type === 'tool_call');
936
- if (toolCalls.length > 0) {
937
- if (mode === 'textual') {
938
- const segments = [];
939
- if (text) {
940
- segments.push(text);
941
- }
942
- for (const tc of toolCalls) {
943
- segments.push(this.describeToolCallForText(tc));
944
- }
945
- const combined = segments.join('\n\n').trim();
946
- if (combined) {
947
- messages.push({
948
- role: 'assistant',
949
- content: combined,
950
- });
951
- }
952
- }
953
- else {
954
- // Assistant message with tool calls
955
- // CRITICAL for Mistral API compatibility (#760):
956
- // When tool_calls are present, we must NOT include a content property at all
957
- // (not even null). Mistral's OpenAI-compatible API requires this.
958
- // See: https://docs.mistral.ai/capabilities/function_calling
959
- messages.push({
960
- role: 'assistant',
961
- tool_calls: toolCalls.map((tc) => ({
962
- id: this.normalizeToOpenAIToolId(tc.id),
963
- type: 'function',
964
- function: {
965
- name: tc.name,
966
- arguments: this.normalizeToolCallArguments(tc.parameters),
967
- },
968
- })),
969
- });
970
- }
971
- }
972
- else if (textBlocks.length > 0) {
973
- // Plain assistant message
974
- messages.push({
975
- role: 'assistant',
976
- content: text,
977
- });
978
- }
979
- }
980
- else if (content.speaker === 'tool') {
981
- // Convert tool responses
982
- const toolResponses = content.blocks.filter((b) => b.type === 'tool_response');
983
- if (mode === 'textual') {
984
- const segments = toolResponses
985
- .map((tr) => this.describeToolResponseForText(tr, config))
986
- .filter(Boolean);
987
- if (segments.length > 0) {
988
- messages.push({
989
- role: 'user',
990
- content: segments.join('\n\n'),
991
- });
992
- }
993
- }
994
- else {
995
- for (const tr of toolResponses) {
996
- // CRITICAL for Mistral API compatibility (#760):
997
- // Tool messages must include a name field matching the function name.
998
- // See: https://docs.mistral.ai/capabilities/function_calling
999
- // Note: The OpenAI SDK types don't include name, but Mistral requires it.
1000
- // We use a type assertion to add this required field.
1001
- messages.push({
1002
- role: 'tool',
1003
- content: this.buildToolResponseContent(tr, config),
1004
- tool_call_id: this.normalizeToOpenAIToolId(tr.callId),
1005
- name: tr.toolName,
1006
- });
1007
- }
1008
- }
1009
- }
1010
- }
1011
- // Validate tool message sequence to prevent API errors
1012
- // This ensures each tool message has a corresponding tool_calls in previous message
1013
- return this.validateToolMessageSequence(messages);
1014
- }
1015
911
  /**
1016
912
  * Build messages with optional reasoning_content based on settings.
1017
913
  *
@@ -1266,7 +1162,6 @@ export class OpenAIProvider extends BaseProvider {
1266
1162
  async *generateLegacyChatCompletionImpl(options, toolFormatter, client, logger) {
1267
1163
  const { contents, tools, metadata } = options;
1268
1164
  const model = options.resolved.model || this.getDefaultModel();
1269
- const toolReplayMode = this.determineToolReplayMode(model);
1270
1165
  const abortSignal = metadata?.abortSignal;
1271
1166
  const ephemeralSettings = options.invocation?.ephemerals ?? {};
1272
1167
  if (logger.enabled) {
@@ -1294,12 +1189,7 @@ export class OpenAIProvider extends BaseProvider {
1294
1189
  // Convert IContent to OpenAI messages format
1295
1190
  // Use buildMessagesWithReasoning for reasoning-aware message building
1296
1191
  // Pass detectedFormat so that Kimi K2 tool IDs are generated correctly
1297
- const messages = toolReplayMode === 'native'
1298
- ? this.buildMessagesWithReasoning(contents, options, detectedFormat)
1299
- : this.convertToOpenAIMessages(contents, toolReplayMode, options.config ?? options.runtime?.config ?? this.globalConfig);
1300
- if (logger.enabled && toolReplayMode !== 'native') {
1301
- logger.debug(() => `[OpenAIProvider] Using textual tool replay mode for model '${model}'`);
1302
- }
1192
+ const messages = this.buildMessagesWithReasoning(contents, options, detectedFormat);
1303
1193
  // Convert Gemini format tools to OpenAI format using the schema converter
1304
1194
  // This ensures required fields are always present in tool schemas
1305
1195
  let formattedTools = convertToolsToOpenAI(tools);
@@ -1494,22 +1384,11 @@ export class OpenAIProvider extends BaseProvider {
1494
1384
  // Bucket failover callback for 429 errors
1495
1385
  // @plan PLAN-20251213issue686 Bucket failover integration for OpenAIProvider
1496
1386
  const onPersistent429Callback = async () => {
1497
- // Try to get the bucket failover handler from runtime context config
1498
- const failoverHandler = options.runtime?.config?.getBucketFailoverHandler();
1499
- if (failoverHandler && failoverHandler.isEnabled()) {
1500
- logger.debug(() => 'Attempting bucket failover on persistent 429');
1501
- const success = await failoverHandler.tryFailover();
1502
- if (success) {
1503
- // Rebuild client with fresh credentials from new bucket
1504
- failoverClient = await this.getClient(options);
1505
- logger.debug(() => `Bucket failover successful, new bucket: ${failoverHandler.getCurrentBucket()}`);
1506
- return true; // Signal retry with new bucket
1507
- }
1508
- logger.debug(() => 'Bucket failover failed - no more buckets available');
1509
- return false; // No more buckets, stop retrying
1510
- }
1511
- // No bucket failover configured
1512
- return null;
1387
+ const { result, client } = await this.handleBucketFailoverOnPersistent429(options, logger);
1388
+ if (client) {
1389
+ failoverClient = client;
1390
+ }
1391
+ return result;
1513
1392
  };
1514
1393
  // Use failover client if bucket failover happened, otherwise use original client
1515
1394
  const executeRequest = () => {
@@ -1568,7 +1447,7 @@ export class OpenAIProvider extends BaseProvider {
1568
1447
  }
1569
1448
  if (!compressedOnce &&
1570
1449
  this.shouldCompressToolMessages(error, logger) &&
1571
- this.compressToolMessages(requestBody.messages, MAX_TOOL_RESPONSE_RETRY_CHARS, logger)) {
1450
+ this.compressToolMessages(requestBody.messages, 512, logger)) {
1572
1451
  compressedOnce = true;
1573
1452
  logger.warn(() => `[OpenAIProvider] Retrying request after compressing tool responses due to provider 400`);
1574
1453
  continue;
@@ -2472,8 +2351,6 @@ export class OpenAIProvider extends BaseProvider {
2472
2351
  metadataKeys: Object.keys(metadata ?? {}),
2473
2352
  });
2474
2353
  }
2475
- // Determine tool replay mode for model compatibility (e.g., polaris-alpha)
2476
- const toolReplayMode = this.determineToolReplayMode(model);
2477
2354
  // Detect the tool format to use BEFORE building messages
2478
2355
  // This is needed so that Kimi K2 tool IDs can be generated in the correct format
2479
2356
  const detectedFormat = this.detectToolFormat();
@@ -2486,13 +2363,7 @@ export class OpenAIProvider extends BaseProvider {
2486
2363
  // Convert IContent to OpenAI messages format
2487
2364
  // Use buildMessagesWithReasoning for reasoning-aware message building
2488
2365
  // Pass detectedFormat so that Kimi K2 tool IDs are generated correctly
2489
- const messages = toolReplayMode === 'native'
2490
- ? this.buildMessagesWithReasoning(contents, options, detectedFormat)
2491
- : this.convertToOpenAIMessages(contents, toolReplayMode, options.config ?? options.runtime?.config ?? this.globalConfig);
2492
- // Log tool replay mode usage for debugging
2493
- if (logger.enabled && toolReplayMode !== 'native') {
2494
- logger.debug(() => `[OpenAIProvider] Using textual tool replay mode for model '${model}'`);
2495
- }
2366
+ const messages = this.buildMessagesWithReasoning(contents, options, detectedFormat);
2496
2367
  // Convert Gemini format tools to OpenAI format using the schema converter
2497
2368
  // This ensures required fields are always present in tool schemas
2498
2369
  let formattedTools = convertToolsToOpenAI(tools);
@@ -2643,22 +2514,11 @@ export class OpenAIProvider extends BaseProvider {
2643
2514
  // Bucket failover callback for 429 errors - tools mode
2644
2515
  // @plan PLAN-20251213issue686 Bucket failover integration for OpenAIProvider
2645
2516
  const onPersistent429CallbackTools = async () => {
2646
- // Try to get the bucket failover handler from runtime context config
2647
- const failoverHandler = options.runtime?.config?.getBucketFailoverHandler();
2648
- if (failoverHandler && failoverHandler.isEnabled()) {
2649
- logger.debug(() => 'Attempting bucket failover on persistent 429');
2650
- const success = await failoverHandler.tryFailover();
2651
- if (success) {
2652
- // Rebuild client with fresh credentials from new bucket
2653
- failoverClientTools = await this.getClient(options);
2654
- logger.debug(() => `Bucket failover successful, new bucket: ${failoverHandler.getCurrentBucket()}`);
2655
- return true; // Signal retry with new bucket
2656
- }
2657
- logger.debug(() => 'Bucket failover failed - no more buckets available');
2658
- return false; // No more buckets, stop retrying
2659
- }
2660
- // No bucket failover configured
2661
- return null;
2517
+ const { result, client } = await this.handleBucketFailoverOnPersistent429(options, logger);
2518
+ if (client) {
2519
+ failoverClientTools = client;
2520
+ }
2521
+ return result;
2662
2522
  };
2663
2523
  if (streamingEnabled) {
2664
2524
  // Streaming mode - use retry loop with compression support
@@ -2707,7 +2567,7 @@ export class OpenAIProvider extends BaseProvider {
2707
2567
  // Tool message compression logic
2708
2568
  if (!compressedOnce &&
2709
2569
  this.shouldCompressToolMessages(error, logger) &&
2710
- this.compressToolMessages(requestBody.messages, MAX_TOOL_RESPONSE_RETRY_CHARS, logger)) {
2570
+ this.compressToolMessages(requestBody.messages, 512, logger)) {
2711
2571
  compressedOnce = true;
2712
2572
  logger.warn(() => `[OpenAIProvider] Retrying streaming request after compressing tool responses due to provider 400`);
2713
2573
  continue;
@@ -2790,7 +2650,7 @@ export class OpenAIProvider extends BaseProvider {
2790
2650
  // Tool message compression logic
2791
2651
  if (!compressedOnce &&
2792
2652
  this.shouldCompressToolMessages(error, logger) &&
2793
- this.compressToolMessages(requestBody.messages, MAX_TOOL_RESPONSE_RETRY_CHARS, logger)) {
2653
+ this.compressToolMessages(requestBody.messages, 512, logger)) {
2794
2654
  compressedOnce = true;
2795
2655
  logger.warn(() => `[OpenAIProvider] Retrying request after compressing tool responses due to provider 400`);
2796
2656
  continue;