@vybestack/llxprt-code-core 0.7.0-nightly.251218.47baadc14 → 0.7.0
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/dist/src/auth/types.d.ts +18 -18
- package/dist/src/core/geminiChat.d.ts +1 -0
- package/dist/src/core/geminiChat.js +52 -64
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/providers/ProviderManager.js +15 -6
- package/dist/src/providers/ProviderManager.js.map +1 -1
- package/dist/src/providers/anthropic/AnthropicProvider.js +11 -0
- package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
- package/dist/src/providers/openai/OpenAIProvider.d.ts +1 -7
- package/dist/src/providers/openai/OpenAIProvider.js +49 -189
- package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js +11 -2
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js.map +1 -1
- package/dist/src/providers/openai-responses/buildResponsesInputFromContent.d.ts +2 -1
- package/dist/src/providers/openai-responses/buildResponsesInputFromContent.js +8 -3
- package/dist/src/providers/openai-responses/buildResponsesInputFromContent.js.map +1 -1
- package/dist/src/providers/utils/toolResponsePayload.js +15 -47
- package/dist/src/providers/utils/toolResponsePayload.js.map +1 -1
- package/dist/src/runtime/AgentRuntimeContext.d.ts +1 -0
- package/dist/src/runtime/runtimeAdapters.js +7 -0
- package/dist/src/runtime/runtimeAdapters.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
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,
|
|
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,
|
|
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;
|