@vybestack/llxprt-code-core 0.4.7 → 0.5.0-nightly.251102.f115237d
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/prompt-config/defaults/default-prompts.json +4 -17
- package/dist/src/auth/precedence.d.ts +69 -9
- package/dist/src/auth/precedence.js +467 -69
- package/dist/src/auth/precedence.js.map +1 -1
- package/dist/src/auth/types.d.ts +2 -2
- package/dist/src/config/config.d.ts +18 -1
- package/dist/src/config/config.js +123 -6
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/index.d.ts +6 -0
- package/dist/src/config/index.js +5 -0
- package/dist/src/config/index.js.map +1 -1
- package/dist/src/config/profileManager.d.ts +23 -3
- package/dist/src/config/profileManager.js +54 -7
- package/dist/src/config/profileManager.js.map +1 -1
- package/dist/src/config/subagentManager.d.ts +96 -0
- package/dist/src/config/subagentManager.js +371 -0
- package/dist/src/config/subagentManager.js.map +1 -0
- package/dist/src/config/types.d.ts +18 -0
- package/dist/src/config/types.js +3 -0
- package/dist/src/config/types.js.map +1 -0
- package/dist/src/core/client.d.ts +27 -7
- package/dist/src/core/client.js +221 -56
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/contentGenerator.d.ts +3 -1
- package/dist/src/core/contentGenerator.js +3 -0
- package/dist/src/core/contentGenerator.js.map +1 -1
- package/dist/src/core/coreToolScheduler.d.ts +1 -5
- package/dist/src/core/coreToolScheduler.js +95 -23
- package/dist/src/core/coreToolScheduler.js.map +1 -1
- package/dist/src/core/geminiChat.d.ts +42 -12
- package/dist/src/core/geminiChat.js +413 -207
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/nonInteractiveToolExecutor.d.ts +3 -2
- package/dist/src/core/nonInteractiveToolExecutor.js +94 -10
- package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
- package/dist/src/core/subagent.d.ts +86 -7
- package/dist/src/core/subagent.js +809 -79
- package/dist/src/core/subagent.js.map +1 -1
- package/dist/src/core/subagentOrchestrator.d.ts +73 -0
- package/dist/src/core/subagentOrchestrator.js +383 -0
- package/dist/src/core/subagentOrchestrator.js.map +1 -0
- package/dist/src/core/subagentScheduler.d.ts +16 -0
- package/dist/src/core/subagentScheduler.js +7 -0
- package/dist/src/core/subagentScheduler.js.map +1 -0
- package/dist/src/core/turn.d.ts +5 -1
- package/dist/src/core/turn.js +5 -1
- package/dist/src/core/turn.js.map +1 -1
- package/dist/src/hooks/tool-render-suppression-hook.js +6 -1
- package/dist/src/hooks/tool-render-suppression-hook.js.map +1 -1
- package/dist/src/ide/ideContext.d.ts +32 -32
- package/dist/src/index.d.ts +19 -1
- package/dist/src/index.js +15 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/interfaces/index.d.ts +1 -0
- package/dist/src/interfaces/index.js +4 -0
- package/dist/src/interfaces/index.js.map +1 -0
- package/dist/src/interfaces/nodejs-error.interface.d.ts +4 -0
- package/dist/src/interfaces/nodejs-error.interface.js +2 -0
- package/dist/src/interfaces/nodejs-error.interface.js.map +1 -0
- package/dist/src/parsers/TextToolCallParser.js +41 -1
- package/dist/src/parsers/TextToolCallParser.js.map +1 -1
- package/dist/src/prompt-config/defaults/core.md +15 -0
- package/dist/src/prompt-config/defaults/providers/gemini/core.md +203 -119
- package/dist/src/prompt-config/defaults/tool-defaults.js +2 -0
- package/dist/src/prompt-config/defaults/tool-defaults.js.map +1 -1
- package/dist/src/prompt-config/defaults/tools/list-subagents.md +7 -0
- package/dist/src/prompt-config/defaults/tools/task.md +8 -0
- package/dist/src/providers/BaseProvider.d.ts +115 -30
- package/dist/src/providers/BaseProvider.js +445 -109
- package/dist/src/providers/BaseProvider.js.map +1 -1
- package/dist/src/providers/IProvider.d.ts +50 -18
- package/dist/src/providers/LoggingProviderWrapper.d.ts +60 -16
- package/dist/src/providers/LoggingProviderWrapper.js +213 -60
- package/dist/src/providers/LoggingProviderWrapper.js.map +1 -1
- package/dist/src/providers/ProviderManager.d.ts +73 -2
- package/dist/src/providers/ProviderManager.js +492 -40
- package/dist/src/providers/ProviderManager.js.map +1 -1
- package/dist/src/providers/anthropic/AnthropicProvider.d.ts +35 -38
- package/dist/src/providers/anthropic/AnthropicProvider.js +222 -227
- package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
- package/dist/src/providers/errors.d.ts +86 -0
- package/dist/src/providers/errors.js +89 -0
- package/dist/src/providers/errors.js.map +1 -1
- package/dist/src/providers/gemini/GeminiProvider.d.ts +101 -41
- package/dist/src/providers/gemini/GeminiProvider.js +386 -311
- package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
- package/dist/src/providers/openai/ConversationCache.d.ts +5 -3
- package/dist/src/providers/openai/ConversationCache.js +93 -32
- package/dist/src/providers/openai/ConversationCache.js.map +1 -1
- package/dist/src/providers/openai/OpenAIProvider.d.ts +82 -42
- package/dist/src/providers/openai/OpenAIProvider.js +374 -453
- package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
- package/dist/src/providers/openai/getOpenAIProviderInfo.d.ts +1 -1
- package/dist/src/providers/openai/getOpenAIProviderInfo.js +52 -22
- package/dist/src/providers/openai/getOpenAIProviderInfo.js.map +1 -1
- package/dist/src/providers/openai/openaiRequestParams.d.ts +7 -0
- package/dist/src/providers/openai/openaiRequestParams.js +66 -0
- package/dist/src/providers/openai/openaiRequestParams.js.map +1 -0
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.d.ts +6 -33
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js +84 -183
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js.map +1 -1
- package/dist/src/providers/types/providerRuntime.d.ts +17 -0
- package/dist/src/providers/types/providerRuntime.js +7 -0
- package/dist/src/providers/types/providerRuntime.js.map +1 -0
- package/dist/src/providers/utils/authToken.d.ts +12 -0
- package/dist/src/providers/utils/authToken.js +17 -0
- package/dist/src/providers/utils/authToken.js.map +1 -0
- package/dist/src/providers/utils/userMemory.d.ts +8 -0
- package/dist/src/providers/utils/userMemory.js +34 -0
- package/dist/src/providers/utils/userMemory.js.map +1 -0
- package/dist/src/runtime/AgentRuntimeContext.d.ts +213 -0
- package/dist/src/runtime/AgentRuntimeContext.js +17 -0
- package/dist/src/runtime/AgentRuntimeContext.js.map +1 -0
- package/dist/src/runtime/AgentRuntimeLoader.d.ts +47 -0
- package/dist/src/runtime/AgentRuntimeLoader.js +122 -0
- package/dist/src/runtime/AgentRuntimeLoader.js.map +1 -0
- package/dist/src/runtime/AgentRuntimeState.d.ts +232 -0
- package/dist/src/runtime/AgentRuntimeState.js +439 -0
- package/dist/src/runtime/AgentRuntimeState.js.map +1 -0
- package/dist/src/runtime/RuntimeInvocationContext.d.ts +51 -0
- package/dist/src/runtime/RuntimeInvocationContext.js +52 -0
- package/dist/src/runtime/RuntimeInvocationContext.js.map +1 -0
- package/dist/src/runtime/createAgentRuntimeContext.d.ts +7 -0
- package/dist/src/runtime/createAgentRuntimeContext.js +58 -0
- package/dist/src/runtime/createAgentRuntimeContext.js.map +1 -0
- package/dist/src/runtime/index.d.ts +13 -0
- package/dist/src/runtime/index.js +14 -0
- package/dist/src/runtime/index.js.map +1 -0
- package/dist/src/runtime/providerRuntimeContext.d.ts +30 -0
- package/dist/src/runtime/providerRuntimeContext.js +70 -0
- package/dist/src/runtime/providerRuntimeContext.js.map +1 -0
- package/dist/src/runtime/runtimeAdapters.d.ts +22 -0
- package/dist/src/runtime/runtimeAdapters.js +81 -0
- package/dist/src/runtime/runtimeAdapters.js.map +1 -0
- package/dist/src/runtime/runtimeStateFactory.d.ts +21 -0
- package/dist/src/runtime/runtimeStateFactory.js +104 -0
- package/dist/src/runtime/runtimeStateFactory.js.map +1 -0
- package/dist/src/services/history/ContentConverters.js +3 -5
- package/dist/src/services/history/ContentConverters.js.map +1 -1
- package/dist/src/services/shellExecutionService.js +2 -2
- package/dist/src/services/shellExecutionService.js.map +1 -1
- package/dist/src/services/todo-context-tracker.d.ts +10 -8
- package/dist/src/services/todo-context-tracker.js +26 -10
- package/dist/src/services/todo-context-tracker.js.map +1 -1
- package/dist/src/services/tool-call-tracker-service.d.ts +11 -7
- package/dist/src/services/tool-call-tracker-service.js +89 -29
- package/dist/src/services/tool-call-tracker-service.js.map +1 -1
- package/dist/src/settings/SettingsService.d.ts +4 -0
- package/dist/src/settings/SettingsService.js +58 -2
- package/dist/src/settings/SettingsService.js.map +1 -1
- package/dist/src/settings/settingsServiceInstance.d.ts +6 -1
- package/dist/src/settings/settingsServiceInstance.js +28 -8
- package/dist/src/settings/settingsServiceInstance.js.map +1 -1
- package/dist/src/telemetry/loggers.d.ts +5 -1
- package/dist/src/telemetry/loggers.js.map +1 -1
- package/dist/src/telemetry/loggers.test.circular.js +4 -0
- package/dist/src/telemetry/loggers.test.circular.js.map +1 -1
- package/dist/src/telemetry/metrics.d.ts +3 -1
- package/dist/src/telemetry/metrics.js.map +1 -1
- package/dist/src/telemetry/types.d.ts +1 -0
- package/dist/src/telemetry/types.js +3 -0
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/test-utils/index.d.ts +2 -0
- package/dist/src/test-utils/index.js +2 -0
- package/dist/src/test-utils/index.js.map +1 -1
- package/dist/src/test-utils/mockWorkspaceContext.d.ts +0 -3
- package/dist/src/test-utils/mockWorkspaceContext.js +3 -4
- package/dist/src/test-utils/mockWorkspaceContext.js.map +1 -1
- package/dist/src/test-utils/providerCallOptions.d.ts +43 -0
- package/dist/src/test-utils/providerCallOptions.js +137 -0
- package/dist/src/test-utils/providerCallOptions.js.map +1 -0
- package/dist/src/test-utils/runtime.d.ts +92 -0
- package/dist/src/test-utils/runtime.js +226 -0
- package/dist/src/test-utils/runtime.js.map +1 -0
- package/dist/src/test-utils/tools.d.ts +4 -4
- package/dist/src/test-utils/tools.js +20 -10
- package/dist/src/test-utils/tools.js.map +1 -1
- package/dist/src/tools/list-subagents.d.ts +31 -0
- package/dist/src/tools/list-subagents.js +109 -0
- package/dist/src/tools/list-subagents.js.map +1 -0
- package/dist/src/tools/task.d.ts +87 -0
- package/dist/src/tools/task.js +427 -0
- package/dist/src/tools/task.js.map +1 -0
- package/dist/src/tools/todo-read.js +1 -1
- package/dist/src/tools/todo-read.js.map +1 -1
- package/dist/src/tools/todo-store.js +4 -2
- package/dist/src/tools/todo-store.js.map +1 -1
- package/dist/src/tools/todo-write.js +4 -2
- package/dist/src/tools/todo-write.js.map +1 -1
- package/dist/src/tools/tool-error.d.ts +1 -0
- package/dist/src/tools/tool-error.js +1 -0
- package/dist/src/tools/tool-error.js.map +1 -1
- package/dist/src/tools/tool-registry.d.ts +2 -0
- package/dist/src/tools/tool-registry.js +46 -21
- package/dist/src/tools/tool-registry.js.map +1 -1
- package/dist/src/types/modelParams.d.ts +4 -0
- package/dist/src/utils/gitIgnoreParser.js +15 -3
- package/dist/src/utils/gitIgnoreParser.js.map +1 -1
- package/dist/src/utils/memoryImportProcessor.js +22 -3
- package/dist/src/utils/memoryImportProcessor.js.map +1 -1
- package/package.json +1 -1
- package/dist/src/prompt-config/defaults/providers/anthropic/core.md +0 -97
- package/dist/src/prompt-config/defaults/providers/anthropic/tools/glob.md +0 -34
- package/dist/src/prompt-config/defaults/providers/anthropic/tools/list-directory.md +0 -11
- package/dist/src/prompt-config/defaults/providers/anthropic/tools/read-file.md +0 -14
- package/dist/src/prompt-config/defaults/providers/anthropic/tools/read-many-files.md +0 -31
- package/dist/src/prompt-config/defaults/providers/anthropic/tools/replace.md +0 -41
- package/dist/src/prompt-config/defaults/providers/anthropic/tools/run-shell-command.md +0 -32
- package/dist/src/prompt-config/defaults/providers/anthropic/tools/save-memory.md +0 -35
- package/dist/src/prompt-config/defaults/providers/anthropic/tools/search-file-content.md +0 -44
- package/dist/src/prompt-config/defaults/providers/anthropic/tools/todo-write.md +0 -45
- package/dist/src/prompt-config/defaults/providers/anthropic/tools/write-file.md +0 -11
- package/dist/src/prompt-config/defaults/providers/openai/core.md +0 -97
- package/dist/src/prompt-config/defaults/providers/openai/tools/todo-pause.md +0 -28
- package/dist/src/prompt-config/defaults/providers/openai/tools/todo-read.md +0 -5
- package/dist/src/prompt-config/defaults/providers/openai/tools/todo-write.md +0 -45
|
@@ -21,20 +21,25 @@ import OpenAI from 'openai';
|
|
|
21
21
|
import * as http from 'http';
|
|
22
22
|
import * as https from 'https';
|
|
23
23
|
import * as net from 'net';
|
|
24
|
-
import { BaseProvider } from '../BaseProvider.js';
|
|
24
|
+
import { BaseProvider, } from '../BaseProvider.js';
|
|
25
25
|
import { DebugLogger } from '../../debug/index.js';
|
|
26
|
-
import { getSettingsService } from '../../settings/settingsServiceInstance.js';
|
|
27
26
|
import { ToolFormatter } from '../../tools/ToolFormatter.js';
|
|
28
27
|
import { processToolParameters } from '../../tools/doubleEscapeUtils.js';
|
|
29
28
|
import { getCoreSystemPromptAsync } from '../../core/prompts.js';
|
|
30
|
-
import { retryWithBackoff
|
|
29
|
+
import { retryWithBackoff } from '../../utils/retry.js';
|
|
30
|
+
import { resolveUserMemory } from '../utils/userMemory.js';
|
|
31
|
+
import { resolveRuntimeAuthToken } from '../utils/authToken.js';
|
|
32
|
+
import { filterOpenAIRequestParams } from './openaiRequestParams.js';
|
|
31
33
|
export class OpenAIProvider extends BaseProvider {
|
|
32
34
|
name = 'openai';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
getLogger() {
|
|
36
|
+
return new DebugLogger('llxprt:provider:openai');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
40
|
+
* @requirement:REQ-SP4-003
|
|
41
|
+
* Constructor reduced to minimal initialization - no state captured
|
|
42
|
+
*/
|
|
38
43
|
constructor(apiKey, baseURL, config, oauthManager) {
|
|
39
44
|
// Normalize empty string to undefined for proper precedence handling
|
|
40
45
|
const normalizedApiKey = apiKey && apiKey.trim() !== '' ? apiKey : undefined;
|
|
@@ -45,310 +50,169 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
45
50
|
(baseURL.includes('dashscope.aliyuncs.com') ||
|
|
46
51
|
baseURL.includes('api.qwen.com') ||
|
|
47
52
|
baseURL.includes('qwen')));
|
|
53
|
+
const forceQwenOAuth = Boolean(config?.forceQwenOAuth);
|
|
48
54
|
// Initialize base provider with auth configuration
|
|
49
55
|
super({
|
|
50
56
|
name: 'openai',
|
|
51
57
|
apiKey: normalizedApiKey,
|
|
52
58
|
baseURL,
|
|
53
59
|
envKeyNames: ['OPENAI_API_KEY'], // Support environment variable fallback
|
|
54
|
-
isOAuthEnabled: isQwenEndpoint && !!oauthManager,
|
|
55
|
-
oauthProvider: isQwenEndpoint ? 'qwen' : undefined,
|
|
60
|
+
isOAuthEnabled: (isQwenEndpoint || forceQwenOAuth) && !!oauthManager,
|
|
61
|
+
oauthProvider: isQwenEndpoint || forceQwenOAuth ? 'qwen' : undefined,
|
|
56
62
|
oauthManager,
|
|
57
63
|
}, config);
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
this.logger = new DebugLogger('llxprt:provider:openai');
|
|
62
|
-
this.loadModelParamsFromSettings().catch((error) => {
|
|
63
|
-
this.logger.debug(() => `Failed to initialize model params from SettingsService: ${error}`);
|
|
64
|
-
});
|
|
64
|
+
// @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
65
|
+
// @requirement:REQ-SP4-002
|
|
66
|
+
// No constructor-captured state - all values sourced from normalized options per call
|
|
65
67
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Create HTTP/HTTPS agents with socket configuration for local AI servers
|
|
70
|
+
* Returns undefined if no socket settings are configured
|
|
71
|
+
*
|
|
72
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
73
|
+
* @requirement:REQ-SP4-003
|
|
74
|
+
* Now sources ephemeral settings from call options instead of provider config
|
|
75
|
+
*/
|
|
76
|
+
createHttpAgents(options) {
|
|
77
|
+
// Get socket configuration from call options or fallback to provider config
|
|
78
|
+
const settingsFromInvocation = options?.invocation?.ephemerals;
|
|
79
|
+
const settings = settingsFromInvocation ??
|
|
80
|
+
this.providerConfig?.getEphemeralSettings?.() ??
|
|
81
|
+
{};
|
|
82
|
+
// Check if any socket settings are explicitly configured
|
|
83
|
+
const hasSocketSettings = 'socket-timeout' in settings ||
|
|
84
|
+
'socket-keepalive' in settings ||
|
|
85
|
+
'socket-nodelay' in settings;
|
|
86
|
+
// Only create custom agents if socket settings are configured
|
|
87
|
+
if (!hasSocketSettings) {
|
|
75
88
|
return undefined;
|
|
76
89
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
:
|
|
85
|
-
|
|
86
|
-
? true
|
|
87
|
-
: noDelaySetting !== false;
|
|
88
|
-
return {
|
|
89
|
-
timeout,
|
|
90
|
-
keepAlive,
|
|
91
|
-
noDelay,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
createSocketAwareFetch(config) {
|
|
95
|
-
const { timeout, keepAlive, noDelay } = config;
|
|
96
|
-
const maxRetries = 2;
|
|
97
|
-
const retryDelay = 1000;
|
|
98
|
-
const partialResponseThreshold = 2;
|
|
99
|
-
const buildHeaders = (init) => {
|
|
100
|
-
const baseHeaders = {
|
|
101
|
-
Accept: 'text/event-stream',
|
|
102
|
-
Connection: keepAlive ? 'keep-alive' : 'close',
|
|
103
|
-
'Cache-Control': 'no-cache',
|
|
104
|
-
};
|
|
105
|
-
if (!init?.headers) {
|
|
106
|
-
return baseHeaders;
|
|
107
|
-
}
|
|
108
|
-
const appendHeader = (key, value) => {
|
|
109
|
-
baseHeaders[key] = value;
|
|
110
|
-
};
|
|
111
|
-
const headers = init.headers;
|
|
112
|
-
if (headers instanceof Headers) {
|
|
113
|
-
headers.forEach((value, key) => {
|
|
114
|
-
appendHeader(key, value);
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
else if (Array.isArray(headers)) {
|
|
118
|
-
headers.forEach(([key, value]) => {
|
|
119
|
-
if (typeof value === 'string') {
|
|
120
|
-
appendHeader(key, value);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
else if (typeof headers === 'object') {
|
|
125
|
-
Object.entries(headers).forEach(([key, value]) => {
|
|
126
|
-
if (typeof value === 'string') {
|
|
127
|
-
appendHeader(key, value);
|
|
128
|
-
}
|
|
129
|
-
else if (Array.isArray(value)) {
|
|
130
|
-
appendHeader(key, value.join(', '));
|
|
131
|
-
}
|
|
132
|
-
else if (value !== undefined && value !== null) {
|
|
133
|
-
appendHeader(key, String(value));
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
return baseHeaders;
|
|
138
|
-
};
|
|
139
|
-
const collectResponseHeaders = (rawHeaders) => {
|
|
140
|
-
const headers = new Headers();
|
|
141
|
-
for (const [key, value] of Object.entries(rawHeaders)) {
|
|
142
|
-
if (!key)
|
|
143
|
-
continue;
|
|
144
|
-
if (Array.isArray(value)) {
|
|
145
|
-
headers.append(key, value.join(', '));
|
|
146
|
-
}
|
|
147
|
-
else if (value !== undefined) {
|
|
148
|
-
headers.append(key, value);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return headers;
|
|
152
|
-
};
|
|
153
|
-
const writeRequestBody = (req, body) => {
|
|
154
|
-
if (!body) {
|
|
155
|
-
req.end();
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
if (typeof body === 'string' || body instanceof Buffer) {
|
|
159
|
-
req.write(body);
|
|
160
|
-
req.end();
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if (body instanceof ArrayBuffer) {
|
|
164
|
-
req.write(Buffer.from(body));
|
|
165
|
-
req.end();
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (ArrayBuffer.isView(body)) {
|
|
169
|
-
req.write(Buffer.from(body.buffer, body.byteOffset, body.byteLength));
|
|
170
|
-
req.end();
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
try {
|
|
174
|
-
req.write(body);
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
req.write(String(body));
|
|
178
|
-
}
|
|
179
|
-
req.end();
|
|
180
|
-
};
|
|
181
|
-
const delay = (ms) => new Promise((resolve) => {
|
|
182
|
-
setTimeout(resolve, ms);
|
|
90
|
+
// Socket configuration with defaults for when settings ARE configured
|
|
91
|
+
const socketTimeout = settings['socket-timeout'] || 60000; // 60 seconds default
|
|
92
|
+
const socketKeepAlive = settings['socket-keepalive'] !== false; // true by default
|
|
93
|
+
const socketNoDelay = settings['socket-nodelay'] !== false; // true by default
|
|
94
|
+
// Create HTTP agent with socket options
|
|
95
|
+
const httpAgent = new http.Agent({
|
|
96
|
+
keepAlive: socketKeepAlive,
|
|
97
|
+
keepAliveMsecs: 1000,
|
|
98
|
+
timeout: socketTimeout,
|
|
183
99
|
});
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const options = {
|
|
196
|
-
hostname: parsedUrl.hostname,
|
|
197
|
-
port: parsedUrl.port ? Number(parsedUrl.port) : isHttps ? 443 : 80,
|
|
198
|
-
path: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
199
|
-
method: init?.method?.toUpperCase() || 'GET',
|
|
200
|
-
headers: buildHeaders(init),
|
|
201
|
-
};
|
|
202
|
-
const req = httpModule.request(options, (res) => {
|
|
203
|
-
const chunks = [];
|
|
204
|
-
let chunkCount = 0;
|
|
205
|
-
res.on('data', (chunk) => {
|
|
206
|
-
chunkCount += 1;
|
|
207
|
-
if (typeof chunk === 'string') {
|
|
208
|
-
chunks.push(Buffer.from(chunk));
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
chunks.push(chunk);
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
res.on('end', () => {
|
|
215
|
-
const bodyBuffer = Buffer.concat(chunks);
|
|
216
|
-
resolve(new Response(bodyBuffer, {
|
|
217
|
-
status: res.statusCode ?? 0,
|
|
218
|
-
statusText: res.statusMessage ?? '',
|
|
219
|
-
headers: collectResponseHeaders(res.headers),
|
|
220
|
-
}));
|
|
221
|
-
});
|
|
222
|
-
res.on('error', async (error) => {
|
|
223
|
-
if (chunkCount >= partialResponseThreshold &&
|
|
224
|
-
attempt < maxRetries) {
|
|
225
|
-
await delay(retryDelay);
|
|
226
|
-
try {
|
|
227
|
-
const retryResponse = await makeRequest(url, init, attempt + 1);
|
|
228
|
-
resolve(retryResponse);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
catch (retryError) {
|
|
232
|
-
reject(retryError);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
reject(new Error(`Response stream error: ${String(error)}`));
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
req.on('socket', (socket) => {
|
|
100
|
+
// Create HTTPS agent with socket options
|
|
101
|
+
const httpsAgent = new https.Agent({
|
|
102
|
+
keepAlive: socketKeepAlive,
|
|
103
|
+
keepAliveMsecs: 1000,
|
|
104
|
+
timeout: socketTimeout,
|
|
105
|
+
});
|
|
106
|
+
// Apply TCP_NODELAY if enabled (reduces latency for local servers)
|
|
107
|
+
if (socketNoDelay) {
|
|
108
|
+
const originalCreateConnection = httpAgent.createConnection;
|
|
109
|
+
httpAgent.createConnection = function (options, callback) {
|
|
110
|
+
const socket = originalCreateConnection.call(this, options, callback);
|
|
240
111
|
if (socket instanceof net.Socket) {
|
|
241
|
-
socket.
|
|
242
|
-
socket.setKeepAlive(keepAlive, 1000);
|
|
243
|
-
socket.setNoDelay(noDelay);
|
|
112
|
+
socket.setNoDelay(true);
|
|
244
113
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
abortError.name = 'AbortError';
|
|
253
|
-
req.destroy(abortError);
|
|
254
|
-
};
|
|
255
|
-
if (init.signal.aborted) {
|
|
256
|
-
abortHandler();
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
init.signal.addEventListener('abort', abortHandler);
|
|
260
|
-
req.on('close', () => {
|
|
261
|
-
init.signal?.removeEventListener('abort', abortHandler);
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
req.on('error', async (error) => {
|
|
265
|
-
if (attempt < maxRetries) {
|
|
266
|
-
await delay(retryDelay);
|
|
267
|
-
try {
|
|
268
|
-
const retryResponse = await makeRequest(url, init, attempt + 1);
|
|
269
|
-
resolve(retryResponse);
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
catch (retryError) {
|
|
273
|
-
reject(retryError);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
114
|
+
return socket;
|
|
115
|
+
};
|
|
116
|
+
const originalHttpsCreateConnection = httpsAgent.createConnection;
|
|
117
|
+
httpsAgent.createConnection = function (options, callback) {
|
|
118
|
+
const socket = originalHttpsCreateConnection.call(this, options, callback);
|
|
119
|
+
if (socket instanceof net.Socket) {
|
|
120
|
+
socket.setNoDelay(true);
|
|
276
121
|
}
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
writeRequestBody(req, init?.body ?? null);
|
|
280
|
-
});
|
|
281
|
-
return async (input, init) => {
|
|
282
|
-
const url = typeof input === 'string'
|
|
283
|
-
? input
|
|
284
|
-
: input instanceof URL
|
|
285
|
-
? input.href
|
|
286
|
-
: input.url;
|
|
287
|
-
if (typeof url !== 'string') {
|
|
288
|
-
return fetch(input, init);
|
|
289
|
-
}
|
|
290
|
-
return makeRequest(url, init);
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
async loadModelParamsFromSettings() {
|
|
294
|
-
const params = await this.getModelParamsFromSettings();
|
|
295
|
-
this.modelParams = params;
|
|
296
|
-
}
|
|
297
|
-
async resolveModelParams() {
|
|
298
|
-
if (this.modelParams) {
|
|
299
|
-
return this.modelParams;
|
|
122
|
+
return socket;
|
|
123
|
+
};
|
|
300
124
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
125
|
+
return { httpAgent, httpsAgent };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
129
|
+
* @requirement:REQ-SP4-002
|
|
130
|
+
* Extract model parameters from normalized options instead of settings service
|
|
131
|
+
*/
|
|
132
|
+
extractModelParamsFromOptions(options) {
|
|
133
|
+
const providerSettings = options.settings?.getProviderSettings(this.name) ?? {};
|
|
134
|
+
const configEphemerals = options.invocation?.ephemerals ?? {};
|
|
135
|
+
const filteredProviderParams = filterOpenAIRequestParams(providerSettings);
|
|
136
|
+
const filteredEphemeralParams = filterOpenAIRequestParams(configEphemerals);
|
|
137
|
+
if (!filteredProviderParams && !filteredEphemeralParams) {
|
|
138
|
+
return undefined;
|
|
304
139
|
}
|
|
305
|
-
return
|
|
140
|
+
return {
|
|
141
|
+
...(filteredProviderParams ?? {}),
|
|
142
|
+
...(filteredEphemeralParams ?? {}),
|
|
143
|
+
};
|
|
306
144
|
}
|
|
307
145
|
/**
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
146
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
147
|
+
* @requirement:REQ-SP4-003
|
|
148
|
+
* Resolve runtime key from normalized options for client scoping
|
|
311
149
|
*/
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const baseURL = this.getBaseURL();
|
|
316
|
-
const socketSettings = this.getSocketSettings();
|
|
317
|
-
const socketKey = socketSettings
|
|
318
|
-
? JSON.stringify(socketSettings)
|
|
319
|
-
: 'default';
|
|
320
|
-
const clientKey = `${baseURL}-${resolvedKey}-${socketKey}`;
|
|
321
|
-
// Clear cache if we have no valid auth (e.g., after logout)
|
|
322
|
-
if (!resolvedKey && this._cachedClient) {
|
|
323
|
-
this._cachedClient = undefined;
|
|
324
|
-
this._cachedClientKey = undefined;
|
|
150
|
+
resolveRuntimeKey(options) {
|
|
151
|
+
if (options.runtime?.runtimeId) {
|
|
152
|
+
return options.runtime.runtimeId;
|
|
325
153
|
}
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
return
|
|
154
|
+
const metadataRuntimeId = options.metadata?.runtimeId;
|
|
155
|
+
if (typeof metadataRuntimeId === 'string' && metadataRuntimeId.trim()) {
|
|
156
|
+
return metadataRuntimeId.trim();
|
|
329
157
|
}
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
158
|
+
const callId = options.settings.get('call-id');
|
|
159
|
+
if (typeof callId === 'string' && callId.trim()) {
|
|
160
|
+
return `call:${callId.trim()}`;
|
|
161
|
+
}
|
|
162
|
+
return 'openai.runtime.unscoped';
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Tool formatter instances cannot be shared between stateless calls,
|
|
166
|
+
* so construct a fresh one for every invocation.
|
|
167
|
+
*
|
|
168
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
169
|
+
* @requirement:REQ-SP4-003
|
|
170
|
+
*/
|
|
171
|
+
createToolFormatter() {
|
|
172
|
+
return new ToolFormatter();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P09
|
|
176
|
+
* @requirement:REQ-SP4-002
|
|
177
|
+
* Instantiates a fresh OpenAI client per call to preserve stateless behaviour.
|
|
178
|
+
*/
|
|
179
|
+
instantiateClient(authToken, baseURL, agents) {
|
|
180
|
+
const clientOptions = {
|
|
181
|
+
apiKey: authToken || '',
|
|
335
182
|
maxRetries: 0,
|
|
336
183
|
};
|
|
337
|
-
if (
|
|
338
|
-
|
|
339
|
-
|
|
184
|
+
if (baseURL && baseURL.trim() !== '') {
|
|
185
|
+
clientOptions.baseURL = baseURL;
|
|
186
|
+
}
|
|
187
|
+
if (agents) {
|
|
188
|
+
clientOptions.httpAgent = agents.httpAgent;
|
|
189
|
+
clientOptions.httpsAgent = agents.httpsAgent;
|
|
340
190
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
191
|
+
return new OpenAI(clientOptions);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P09
|
|
195
|
+
* @requirement:REQ-SP4-002
|
|
196
|
+
* Creates a client scoped to the active runtime metadata without caching.
|
|
197
|
+
*/
|
|
198
|
+
async getClient(options) {
|
|
199
|
+
const authToken = (await resolveRuntimeAuthToken(options.resolved.authToken)) ?? '';
|
|
200
|
+
if (!authToken) {
|
|
201
|
+
throw new Error(`ProviderCacheError("Auth token unavailable for runtimeId=${options.runtime?.runtimeId} (REQ-SP4-003).")`);
|
|
202
|
+
}
|
|
203
|
+
const baseURL = options.resolved.baseURL ?? this.baseProviderConfig.baseURL;
|
|
204
|
+
const agents = this.createHttpAgents(options);
|
|
205
|
+
return this.instantiateClient(authToken, baseURL, agents);
|
|
346
206
|
}
|
|
347
207
|
/**
|
|
348
208
|
* Check if OAuth is supported for this provider
|
|
349
209
|
* Qwen endpoints support OAuth, standard OpenAI does not
|
|
350
210
|
*/
|
|
351
211
|
supportsOAuth() {
|
|
212
|
+
const providerConfig = this.providerConfig;
|
|
213
|
+
if (providerConfig?.forceQwenOAuth) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
352
216
|
// CRITICAL FIX: Check provider name first for cases where base URL is changed by profiles
|
|
353
217
|
// This handles the cerebrasqwen3 profile case where base-url is changed to cerebras.ai
|
|
354
218
|
// but the provider name is still 'qwen' due to Object.defineProperty override
|
|
@@ -370,7 +234,10 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
370
234
|
try {
|
|
371
235
|
// Always try to fetch models, regardless of auth status
|
|
372
236
|
// Local endpoints often work without authentication
|
|
373
|
-
const
|
|
237
|
+
const authToken = await this.getAuthToken();
|
|
238
|
+
const baseURL = this.getBaseURL();
|
|
239
|
+
const agents = this.createHttpAgents();
|
|
240
|
+
const client = this.instantiateClient(authToken, baseURL, agents);
|
|
374
241
|
const response = await client.models.list();
|
|
375
242
|
const models = [];
|
|
376
243
|
for await (const model of response) {
|
|
@@ -387,7 +254,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
387
254
|
return models;
|
|
388
255
|
}
|
|
389
256
|
catch (error) {
|
|
390
|
-
this.
|
|
257
|
+
this.getLogger().debug(() => `Error fetching models from OpenAI: ${error}`);
|
|
391
258
|
// Return a hardcoded list as fallback
|
|
392
259
|
return this.getFallbackModels();
|
|
393
260
|
}
|
|
@@ -397,9 +264,6 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
397
264
|
}
|
|
398
265
|
getDefaultModel() {
|
|
399
266
|
// Return hardcoded default - do NOT call getModel() to avoid circular dependency
|
|
400
|
-
if (this.providerConfig?.defaultModel) {
|
|
401
|
-
return this.providerConfig.defaultModel;
|
|
402
|
-
}
|
|
403
267
|
// Check if this is a Qwen provider instance based on baseURL
|
|
404
268
|
const baseURL = this.getBaseURL();
|
|
405
269
|
if (baseURL &&
|
|
@@ -408,15 +272,6 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
408
272
|
}
|
|
409
273
|
return process.env.LLXPRT_DEFAULT_MODEL || 'gpt-5';
|
|
410
274
|
}
|
|
411
|
-
/**
|
|
412
|
-
* Set the model to use for this provider
|
|
413
|
-
* This updates the model in ephemeral settings so it's immediately available
|
|
414
|
-
*/
|
|
415
|
-
setModel(modelId) {
|
|
416
|
-
const settingsService = getSettingsService();
|
|
417
|
-
settingsService.set('model', modelId);
|
|
418
|
-
this.logger.debug(() => `Model set to: ${modelId}`);
|
|
419
|
-
}
|
|
420
275
|
/**
|
|
421
276
|
* Get the currently selected model
|
|
422
277
|
*/
|
|
@@ -424,32 +279,51 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
424
279
|
return this.getModel();
|
|
425
280
|
}
|
|
426
281
|
/**
|
|
427
|
-
*
|
|
428
|
-
*
|
|
282
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P09
|
|
283
|
+
* @requirement:REQ-SP4-002
|
|
284
|
+
* No-op retained for compatibility because clients are no longer cached.
|
|
429
285
|
*/
|
|
430
286
|
// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility
|
|
431
|
-
clearClientCache() {
|
|
432
|
-
|
|
433
|
-
this._cachedClientKey = undefined;
|
|
287
|
+
clearClientCache(runtimeKey) {
|
|
288
|
+
void runtimeKey;
|
|
434
289
|
}
|
|
435
290
|
/**
|
|
436
291
|
* Override isAuthenticated for qwen provider to check OAuth directly
|
|
437
292
|
*/
|
|
438
293
|
async isAuthenticated() {
|
|
439
294
|
const config = this.providerConfig;
|
|
295
|
+
const directApiKey = this.baseProviderConfig.apiKey;
|
|
296
|
+
if (typeof directApiKey === 'string' && directApiKey.trim() !== '') {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const nonOAuthToken = await this.authResolver.resolveAuthentication({
|
|
301
|
+
settingsService: this.resolveSettingsService(),
|
|
302
|
+
includeOAuth: false,
|
|
303
|
+
});
|
|
304
|
+
if (typeof nonOAuthToken === 'string' && nonOAuthToken.trim() !== '') {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
if (process.env.DEBUG) {
|
|
310
|
+
this.getLogger().debug(() => `[openai] non-OAuth authentication resolution failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
440
313
|
if (this.name === 'qwen' && config?.forceQwenOAuth) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
314
|
+
try {
|
|
315
|
+
const token = await this.authResolver.resolveAuthentication({
|
|
316
|
+
settingsService: this.resolveSettingsService(),
|
|
317
|
+
includeOAuth: true,
|
|
318
|
+
});
|
|
319
|
+
return typeof token === 'string' && token.trim() !== '';
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
if (process.env.DEBUG) {
|
|
323
|
+
this.getLogger().debug(() => `[openai] forced OAuth authentication failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
450
324
|
}
|
|
325
|
+
return false;
|
|
451
326
|
}
|
|
452
|
-
return false;
|
|
453
327
|
}
|
|
454
328
|
// For non-qwen providers, use the normal check
|
|
455
329
|
return super.isAuthenticated();
|
|
@@ -462,15 +336,6 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
462
336
|
// If this is the qwen provider and we have forceQwenOAuth, skip SettingsService checks
|
|
463
337
|
const config = this.providerConfig;
|
|
464
338
|
if (this.name === 'qwen' && config?.forceQwenOAuth) {
|
|
465
|
-
// Check cache first (short-lived cache to avoid repeated OAuth calls)
|
|
466
|
-
if (this.cachedAuthToken &&
|
|
467
|
-
this.authCacheTimestamp &&
|
|
468
|
-
Date.now() - this.authCacheTimestamp < this.AUTH_CACHE_DURATION) {
|
|
469
|
-
return this.cachedAuthToken;
|
|
470
|
-
}
|
|
471
|
-
// Clear stale cache
|
|
472
|
-
this.cachedAuthToken = undefined;
|
|
473
|
-
this.authCacheTimestamp = undefined;
|
|
474
339
|
// For qwen, skip directly to OAuth without checking SettingsService
|
|
475
340
|
// Use 'qwen' as the provider name even if baseProviderConfig.oauthProvider is not set
|
|
476
341
|
const oauthProviderName = this.baseProviderConfig.oauthProvider || 'qwen';
|
|
@@ -478,9 +343,6 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
478
343
|
try {
|
|
479
344
|
const token = await this.baseProviderConfig.oauthManager.getToken(oauthProviderName);
|
|
480
345
|
if (token) {
|
|
481
|
-
// Cache the token briefly
|
|
482
|
-
this.cachedAuthToken = token;
|
|
483
|
-
this.authCacheTimestamp = Date.now();
|
|
484
346
|
return token;
|
|
485
347
|
}
|
|
486
348
|
}
|
|
@@ -558,25 +420,36 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
558
420
|
return 'hist_tool_' + id;
|
|
559
421
|
}
|
|
560
422
|
/**
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
* @
|
|
564
|
-
* @
|
|
423
|
+
* @plan PLAN-20250218-STATELESSPROVIDER.P04
|
|
424
|
+
* @requirement REQ-SP-001
|
|
425
|
+
* @pseudocode base-provider.md lines 7-15
|
|
426
|
+
* @pseudocode provider-invocation.md lines 8-12
|
|
427
|
+
*/
|
|
428
|
+
/**
|
|
429
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P09
|
|
430
|
+
* @requirement:REQ-SP4-002
|
|
431
|
+
* Generate chat completion with per-call client instantiation.
|
|
565
432
|
*/
|
|
566
|
-
async *
|
|
433
|
+
async *generateChatCompletionWithOptions(options) {
|
|
434
|
+
const callFormatter = this.createToolFormatter();
|
|
435
|
+
const client = await this.getClient(options);
|
|
436
|
+
const runtimeKey = this.resolveRuntimeKey(options);
|
|
437
|
+
const { tools } = options;
|
|
438
|
+
const logger = new DebugLogger('llxprt:provider:openai');
|
|
567
439
|
// Debug log what we receive
|
|
568
|
-
if (
|
|
569
|
-
|
|
440
|
+
if (logger.enabled) {
|
|
441
|
+
logger.debug(() => `[OpenAIProvider] generateChatCompletion received tools:`, {
|
|
570
442
|
hasTools: !!tools,
|
|
571
443
|
toolsLength: tools?.length,
|
|
572
444
|
toolsType: typeof tools,
|
|
573
445
|
isArray: Array.isArray(tools),
|
|
574
446
|
firstToolName: tools?.[0]?.functionDeclarations?.[0]?.name,
|
|
575
447
|
toolsStructure: tools ? 'available' : 'undefined',
|
|
448
|
+
runtimeKey,
|
|
576
449
|
});
|
|
577
450
|
}
|
|
578
|
-
// Pass tools directly in Gemini format - they'll be converted
|
|
579
|
-
const generator = this.generateChatCompletionImpl(
|
|
451
|
+
// Pass tools directly in Gemini format - they'll be converted per call
|
|
452
|
+
const generator = this.generateChatCompletionImpl(options, callFormatter, client, logger);
|
|
580
453
|
for await (const item of generator) {
|
|
581
454
|
yield item;
|
|
582
455
|
}
|
|
@@ -646,27 +519,44 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
646
519
|
return messages;
|
|
647
520
|
}
|
|
648
521
|
/**
|
|
649
|
-
*
|
|
522
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
523
|
+
* @requirement:REQ-SP4-003
|
|
524
|
+
* Internal implementation for chat completion using normalized options
|
|
650
525
|
*/
|
|
651
|
-
async *generateChatCompletionImpl(
|
|
652
|
-
|
|
653
|
-
const model =
|
|
526
|
+
async *generateChatCompletionImpl(options, toolFormatter, client, logger) {
|
|
527
|
+
const { contents, tools, metadata } = options;
|
|
528
|
+
const model = options.resolved.model || this.getDefaultModel();
|
|
529
|
+
const abortSignal = metadata?.abortSignal;
|
|
530
|
+
const ephemeralSettings = options.invocation?.ephemerals ?? {};
|
|
531
|
+
if (logger.enabled) {
|
|
532
|
+
const resolved = options.resolved;
|
|
533
|
+
logger.debug(() => `[OpenAIProvider] Resolved request context`, {
|
|
534
|
+
provider: this.name,
|
|
535
|
+
model,
|
|
536
|
+
resolvedModel: resolved.model,
|
|
537
|
+
resolvedBaseUrl: resolved.baseURL,
|
|
538
|
+
authTokenPresent: Boolean(resolved.authToken),
|
|
539
|
+
messageCount: contents.length,
|
|
540
|
+
toolCount: tools?.length ?? 0,
|
|
541
|
+
metadataKeys: Object.keys(metadata ?? {}),
|
|
542
|
+
});
|
|
543
|
+
}
|
|
654
544
|
// Convert IContent to OpenAI messages format
|
|
655
545
|
const messages = this.convertToOpenAIMessages(contents);
|
|
656
546
|
// Detect the tool format to use (once at the start of the method)
|
|
657
547
|
const detectedFormat = this.detectToolFormat();
|
|
658
548
|
// Log the detected format for debugging
|
|
659
|
-
|
|
549
|
+
logger.debug(() => `[OpenAIProvider] Using tool format '${detectedFormat}' for model '${model}'`, {
|
|
660
550
|
model,
|
|
661
551
|
detectedFormat,
|
|
662
552
|
provider: this.name,
|
|
663
553
|
});
|
|
664
554
|
// Convert Gemini format tools to the detected format
|
|
665
|
-
let formattedTools =
|
|
555
|
+
let formattedTools = toolFormatter.convertGeminiToFormat(tools, detectedFormat);
|
|
666
556
|
// CRITICAL FIX: Ensure we never pass an empty tools array
|
|
667
557
|
// The OpenAI API errors when tools=[] but a tool call is attempted
|
|
668
558
|
if (Array.isArray(formattedTools) && formattedTools.length === 0) {
|
|
669
|
-
|
|
559
|
+
logger.warn(() => `[OpenAIProvider] CRITICAL: Formatted tools is empty array! Setting to undefined to prevent API errors.`, {
|
|
670
560
|
model,
|
|
671
561
|
inputTools: tools,
|
|
672
562
|
inputToolsLength: tools?.length,
|
|
@@ -676,8 +566,8 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
676
566
|
formattedTools = undefined;
|
|
677
567
|
}
|
|
678
568
|
// Debug log the conversion result - enhanced logging for intermittent issues
|
|
679
|
-
if (
|
|
680
|
-
|
|
569
|
+
if (logger.enabled && formattedTools) {
|
|
570
|
+
logger.debug(() => `[OpenAIProvider] Tool conversion summary:`, {
|
|
681
571
|
detectedFormat,
|
|
682
572
|
inputHadTools: !!tools,
|
|
683
573
|
inputToolsLength: tools?.length,
|
|
@@ -689,48 +579,61 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
689
579
|
});
|
|
690
580
|
}
|
|
691
581
|
// Get streaming setting from ephemeral settings (default: enabled)
|
|
692
|
-
const streamingSetting =
|
|
582
|
+
const streamingSetting = ephemeralSettings['streaming'];
|
|
693
583
|
const streamingEnabled = streamingSetting !== 'disabled';
|
|
694
584
|
// Get the system prompt
|
|
695
585
|
const flattenedToolNames = tools?.flatMap((group) => group.functionDeclarations
|
|
696
586
|
.map((decl) => decl.name)
|
|
697
587
|
.filter((name) => !!name)) ?? [];
|
|
698
588
|
const toolNamesArg = tools === undefined ? undefined : Array.from(new Set(flattenedToolNames));
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
589
|
+
/**
|
|
590
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
591
|
+
* @requirement:REQ-SP4-003
|
|
592
|
+
* Source user memory from normalized options instead of global config
|
|
593
|
+
*/
|
|
594
|
+
const userMemory = await resolveUserMemory(options.userMemory, () => options.invocation?.userMemory);
|
|
702
595
|
const systemPrompt = await getCoreSystemPromptAsync(userMemory, model, toolNamesArg);
|
|
703
596
|
// Add system prompt as the first message in the array
|
|
704
597
|
const messagesWithSystem = [
|
|
705
598
|
{ role: 'system', content: systemPrompt },
|
|
706
599
|
...messages,
|
|
707
600
|
];
|
|
601
|
+
const maxTokens = metadata?.maxTokens ??
|
|
602
|
+
ephemeralSettings['max-tokens'];
|
|
708
603
|
// Build request - only include tools if they exist and are not empty
|
|
709
604
|
// IMPORTANT: Create a deep copy of tools to prevent mutation issues
|
|
710
605
|
const requestBody = {
|
|
711
606
|
model,
|
|
712
607
|
messages: messagesWithSystem,
|
|
713
|
-
...(formattedTools && formattedTools.length > 0
|
|
714
|
-
? {
|
|
715
|
-
// Deep clone the tools array to prevent any mutation issues
|
|
716
|
-
tools: JSON.parse(JSON.stringify(formattedTools)),
|
|
717
|
-
// Add tool_choice for Qwen/Cerebras to ensure proper tool calling
|
|
718
|
-
tool_choice: 'auto',
|
|
719
|
-
}
|
|
720
|
-
: {}),
|
|
721
|
-
max_tokens: maxTokens,
|
|
722
608
|
stream: streamingEnabled,
|
|
723
609
|
};
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
610
|
+
if (formattedTools && formattedTools.length > 0) {
|
|
611
|
+
requestBody.tools = JSON.parse(JSON.stringify(formattedTools));
|
|
612
|
+
requestBody.tool_choice = 'auto';
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
616
|
+
* @requirement:REQ-SP4-002
|
|
617
|
+
* Extract per-call request overrides from normalized options instead of cached state
|
|
618
|
+
*/
|
|
619
|
+
const requestOverrides = this.extractModelParamsFromOptions(options);
|
|
620
|
+
if (requestOverrides) {
|
|
621
|
+
if (logger.enabled) {
|
|
622
|
+
logger.debug(() => `[OpenAIProvider] Applying request overrides`, {
|
|
623
|
+
overrideKeys: Object.keys(requestOverrides),
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
Object.assign(requestBody, requestOverrides);
|
|
627
|
+
}
|
|
628
|
+
if (typeof maxTokens === 'number' && Number.isFinite(maxTokens)) {
|
|
629
|
+
requestBody.max_tokens = maxTokens;
|
|
727
630
|
}
|
|
728
631
|
// Debug log request summary for Cerebras/Qwen
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
baseURL
|
|
632
|
+
const baseURL = options.resolved.baseURL ?? this.getBaseURL();
|
|
633
|
+
if (logger.enabled &&
|
|
634
|
+
(model.toLowerCase().includes('qwen') || baseURL?.includes('cerebras'))) {
|
|
635
|
+
logger.debug(() => `Request to ${baseURL} for model ${model}:`, {
|
|
636
|
+
baseURL,
|
|
734
637
|
model,
|
|
735
638
|
streamingEnabled,
|
|
736
639
|
hasTools: 'tools' in requestBody,
|
|
@@ -739,10 +642,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
739
642
|
toolsInRequest: 'tools' in requestBody ? requestBody.tools?.length : 'not included',
|
|
740
643
|
});
|
|
741
644
|
}
|
|
742
|
-
// Get OpenAI client
|
|
743
|
-
const client = await this.getClient();
|
|
744
645
|
// Get retry settings from ephemeral settings
|
|
745
|
-
const ephemeralSettings = this.providerConfig?.getEphemeralSettings?.() || {};
|
|
746
646
|
const maxRetries = ephemeralSettings['retries'] ?? 6; // Default for OpenAI
|
|
747
647
|
const initialDelayMs = ephemeralSettings['retrywait'] ?? 4000; // Default for OpenAI
|
|
748
648
|
// Get stream options from ephemeral settings (default: include usage for token tracking)
|
|
@@ -752,23 +652,43 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
752
652
|
Object.assign(requestBody, { stream_options: streamOptions });
|
|
753
653
|
}
|
|
754
654
|
// Log the exact tools being sent for debugging
|
|
755
|
-
if (
|
|
756
|
-
|
|
655
|
+
if (logger.enabled && 'tools' in requestBody) {
|
|
656
|
+
logger.debug(() => `[OpenAIProvider] Exact tools being sent to API:`, {
|
|
757
657
|
toolCount: requestBody.tools?.length,
|
|
758
658
|
toolNames: requestBody.tools?.map((t) => 'function' in t ? t.function?.name : undefined),
|
|
759
659
|
firstTool: requestBody.tools?.[0],
|
|
760
660
|
});
|
|
761
661
|
}
|
|
762
662
|
// Wrap the API call with retry logic using centralized retry utility
|
|
663
|
+
if (logger.enabled) {
|
|
664
|
+
logger.debug(() => `[OpenAIProvider] Sending chat request`, {
|
|
665
|
+
model,
|
|
666
|
+
baseURL: baseURL ?? this.getBaseURL(),
|
|
667
|
+
streamingEnabled,
|
|
668
|
+
toolCount: formattedTools?.length ?? 0,
|
|
669
|
+
hasAuthToken: Boolean(options.resolved.authToken),
|
|
670
|
+
requestHasSystemPrompt: Boolean(systemPrompt?.length),
|
|
671
|
+
messageCount: messagesWithSystem.length,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
763
674
|
let response;
|
|
764
675
|
// Debug log throttle tracker status
|
|
765
|
-
|
|
676
|
+
logger.debug(() => `Retry configuration:`, {
|
|
766
677
|
hasThrottleTracker: !!this.throttleTracker,
|
|
767
678
|
throttleTrackerType: typeof this.throttleTracker,
|
|
768
679
|
maxRetries,
|
|
769
680
|
initialDelayMs,
|
|
770
681
|
});
|
|
771
682
|
const customHeaders = this.getCustomHeaders();
|
|
683
|
+
if (logger.enabled) {
|
|
684
|
+
logger.debug(() => `[OpenAIProvider] Request body preview`, {
|
|
685
|
+
model: requestBody.model,
|
|
686
|
+
hasStop: 'stop' in requestBody,
|
|
687
|
+
hasMaxTokens: 'max_tokens' in requestBody,
|
|
688
|
+
hasResponseFormat: 'response_format' in requestBody,
|
|
689
|
+
overrideKeys: requestOverrides ? Object.keys(requestOverrides) : [],
|
|
690
|
+
});
|
|
691
|
+
}
|
|
772
692
|
try {
|
|
773
693
|
response = await retryWithBackoff(() => client.chat.completions.create(requestBody, {
|
|
774
694
|
...(abortSignal ? { signal: abortSignal } : {}),
|
|
@@ -786,7 +706,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
786
706
|
if (errorMessage.includes('Tool is not present in the tools list') &&
|
|
787
707
|
(model.toLowerCase().includes('qwen') ||
|
|
788
708
|
this.getBaseURL()?.includes('cerebras'))) {
|
|
789
|
-
|
|
709
|
+
logger.error('Cerebras/Qwen API error: Tool not found despite being in request. This is a known API issue.', {
|
|
790
710
|
error,
|
|
791
711
|
model,
|
|
792
712
|
toolsProvided: formattedTools?.length || 0,
|
|
@@ -800,6 +720,21 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
800
720
|
throw enhancedError;
|
|
801
721
|
}
|
|
802
722
|
// Re-throw other errors as-is
|
|
723
|
+
const capturedErrorMessage = error instanceof Error ? error.message : String(error);
|
|
724
|
+
const status = typeof error === 'object' &&
|
|
725
|
+
error !== null &&
|
|
726
|
+
'status' in error &&
|
|
727
|
+
typeof error.status === 'number'
|
|
728
|
+
? error.status
|
|
729
|
+
: undefined;
|
|
730
|
+
logger.error(() => `[OpenAIProvider] Chat completion failed for model '${model}' at '${baseURL ?? this.getBaseURL() ?? 'default'}': ${capturedErrorMessage}`, {
|
|
731
|
+
model,
|
|
732
|
+
baseURL: baseURL ?? this.getBaseURL(),
|
|
733
|
+
streamingEnabled,
|
|
734
|
+
hasTools: formattedTools?.length ?? 0,
|
|
735
|
+
requestHasSystemPrompt: !!systemPrompt,
|
|
736
|
+
status,
|
|
737
|
+
});
|
|
803
738
|
throw error;
|
|
804
739
|
}
|
|
805
740
|
// Check if response is streaming or not
|
|
@@ -829,7 +764,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
829
764
|
continue;
|
|
830
765
|
// Check for finish_reason to detect proper stream ending
|
|
831
766
|
if (choice.finish_reason) {
|
|
832
|
-
|
|
767
|
+
logger.debug(() => `[Streaming] Stream finished with reason: ${choice.finish_reason}`, {
|
|
833
768
|
model,
|
|
834
769
|
finishReason: choice.finish_reason,
|
|
835
770
|
hasAccumulatedText: _accumulatedText.length > 0,
|
|
@@ -838,7 +773,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
838
773
|
});
|
|
839
774
|
// If finish_reason is 'length', the response was cut off
|
|
840
775
|
if (choice.finish_reason === 'length') {
|
|
841
|
-
|
|
776
|
+
logger.debug(() => `Response truncated due to length limit for model ${model}`);
|
|
842
777
|
}
|
|
843
778
|
// Flush any buffered text when stream finishes
|
|
844
779
|
if (textBuffer.length > 0) {
|
|
@@ -860,7 +795,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
860
795
|
_accumulatedText += deltaContent;
|
|
861
796
|
// Debug log for providers that need buffering
|
|
862
797
|
if (shouldBufferText) {
|
|
863
|
-
|
|
798
|
+
logger.debug(() => `[Streaming] Chunk content for ${detectedFormat} format:`, {
|
|
864
799
|
deltaContent,
|
|
865
800
|
length: deltaContent.length,
|
|
866
801
|
hasNewline: deltaContent.includes('\n'),
|
|
@@ -871,7 +806,8 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
871
806
|
textBuffer += deltaContent;
|
|
872
807
|
// Emit buffered text when we have a complete sentence or paragraph
|
|
873
808
|
// Look for natural break points
|
|
874
|
-
if (textBuffer.
|
|
809
|
+
if (textBuffer.includes('\n') ||
|
|
810
|
+
textBuffer.endsWith('. ') ||
|
|
875
811
|
textBuffer.endsWith('! ') ||
|
|
876
812
|
textBuffer.endsWith('? ') ||
|
|
877
813
|
textBuffer.length > 100) {
|
|
@@ -941,7 +877,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
941
877
|
if (errorMessage.includes('Tool is not present in the tools list') &&
|
|
942
878
|
(model.toLowerCase().includes('qwen') ||
|
|
943
879
|
this.getBaseURL()?.includes('cerebras'))) {
|
|
944
|
-
|
|
880
|
+
logger.error('Cerebras/Qwen API error: Tool not found despite being in request. This is a known API issue.', {
|
|
945
881
|
error,
|
|
946
882
|
model,
|
|
947
883
|
toolsProvided: formattedTools?.length || 0,
|
|
@@ -952,7 +888,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
952
888
|
enhancedError.originalError = error;
|
|
953
889
|
throw enhancedError;
|
|
954
890
|
}
|
|
955
|
-
|
|
891
|
+
logger.error('Error processing streaming response:', error);
|
|
956
892
|
throw error;
|
|
957
893
|
}
|
|
958
894
|
}
|
|
@@ -1031,7 +967,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1031
967
|
}
|
|
1032
968
|
// Log finish reason for debugging Qwen issues
|
|
1033
969
|
if (choice.finish_reason) {
|
|
1034
|
-
|
|
970
|
+
logger.debug(() => `[Non-streaming] Response finish_reason: ${choice.finish_reason}`, {
|
|
1035
971
|
model,
|
|
1036
972
|
finishReason: choice.finish_reason,
|
|
1037
973
|
hasContent: !!choice.message?.content,
|
|
@@ -1042,7 +978,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1042
978
|
});
|
|
1043
979
|
// Warn if the response was truncated
|
|
1044
980
|
if (choice.finish_reason === 'length') {
|
|
1045
|
-
|
|
981
|
+
logger.warn(() => `Response truncated due to max_tokens limit for model ${model}. Consider increasing max_tokens.`);
|
|
1046
982
|
}
|
|
1047
983
|
}
|
|
1048
984
|
const blocks = [];
|
|
@@ -1108,103 +1044,89 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1108
1044
|
}
|
|
1109
1045
|
}
|
|
1110
1046
|
/**
|
|
1111
|
-
*
|
|
1047
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
1048
|
+
* @requirement:REQ-SP4-002
|
|
1049
|
+
* Memoization of model parameters disabled for stateless provider
|
|
1112
1050
|
*/
|
|
1113
|
-
setModelParams(
|
|
1114
|
-
|
|
1115
|
-
this.modelParams = undefined;
|
|
1116
|
-
this.setModelParamsInSettings(undefined).catch((error) => {
|
|
1117
|
-
this.logger.debug(() => `Failed to clear model params in SettingsService: ${error}`);
|
|
1118
|
-
});
|
|
1119
|
-
return;
|
|
1120
|
-
}
|
|
1121
|
-
const updated = { ...(this.modelParams ?? {}) };
|
|
1122
|
-
for (const [key, value] of Object.entries(params)) {
|
|
1123
|
-
if (value === undefined || value === null) {
|
|
1124
|
-
delete updated[key];
|
|
1125
|
-
}
|
|
1126
|
-
else {
|
|
1127
|
-
updated[key] = value;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
this.modelParams = Object.keys(updated).length > 0 ? updated : undefined;
|
|
1131
|
-
this.setModelParamsInSettings(this.modelParams).catch((error) => {
|
|
1132
|
-
this.logger.debug(() => `Failed to persist model params to SettingsService: ${error}`);
|
|
1133
|
-
});
|
|
1051
|
+
setModelParams(_params) {
|
|
1052
|
+
throw new Error('ProviderCacheError("Attempted to memoize model parameters for openai")');
|
|
1134
1053
|
}
|
|
1054
|
+
/**
|
|
1055
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
1056
|
+
* @requirement:REQ-SP4-003
|
|
1057
|
+
* Gets model parameters from SettingsService per call (stateless)
|
|
1058
|
+
*/
|
|
1135
1059
|
getModelParams() {
|
|
1136
|
-
|
|
1060
|
+
try {
|
|
1061
|
+
const settingsService = this.resolveSettingsService();
|
|
1062
|
+
const providerSettings = settingsService.getProviderSettings(this.name);
|
|
1063
|
+
const reservedKeys = new Set([
|
|
1064
|
+
'enabled',
|
|
1065
|
+
'apiKey',
|
|
1066
|
+
'api-key',
|
|
1067
|
+
'apiKeyfile',
|
|
1068
|
+
'api-keyfile',
|
|
1069
|
+
'baseUrl',
|
|
1070
|
+
'base-url',
|
|
1071
|
+
'model',
|
|
1072
|
+
'toolFormat',
|
|
1073
|
+
'tool-format',
|
|
1074
|
+
'toolFormatOverride',
|
|
1075
|
+
'tool-format-override',
|
|
1076
|
+
'defaultModel',
|
|
1077
|
+
]);
|
|
1078
|
+
const params = {};
|
|
1079
|
+
if (providerSettings) {
|
|
1080
|
+
for (const [key, value] of Object.entries(providerSettings)) {
|
|
1081
|
+
if (reservedKeys.has(key) || value === undefined || value === null) {
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
params[key] = value;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return Object.keys(params).length > 0 ? params : undefined;
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
this.getLogger().debug(() => `Failed to get OpenAI provider settings from SettingsService: ${error}`);
|
|
1091
|
+
return undefined;
|
|
1092
|
+
}
|
|
1137
1093
|
}
|
|
1138
1094
|
/**
|
|
1139
|
-
*
|
|
1095
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
1096
|
+
* @requirement:REQ-SP4-003
|
|
1097
|
+
* Get the tool format for this provider using normalized options
|
|
1140
1098
|
* @returns The tool format to use
|
|
1141
1099
|
*/
|
|
1142
1100
|
getToolFormat() {
|
|
1143
1101
|
const format = this.detectToolFormat();
|
|
1144
|
-
|
|
1102
|
+
const logger = new DebugLogger('llxprt:provider:openai');
|
|
1103
|
+
logger.debug(() => `getToolFormat() called, returning: ${format}`, {
|
|
1145
1104
|
provider: this.name,
|
|
1146
1105
|
model: this.getModel(),
|
|
1147
1106
|
format,
|
|
1148
1107
|
});
|
|
1149
1108
|
return format;
|
|
1150
1109
|
}
|
|
1151
|
-
/**
|
|
1152
|
-
* Set tool format override for this provider
|
|
1153
|
-
* @param format The format to use, or null to clear override
|
|
1154
|
-
*/
|
|
1155
|
-
setToolFormatOverride(format) {
|
|
1156
|
-
const settingsService = getSettingsService();
|
|
1157
|
-
if (format === null) {
|
|
1158
|
-
settingsService.setProviderSetting(this.name, 'toolFormat', 'auto');
|
|
1159
|
-
this.logger.debug(() => `Tool format override cleared for ${this.name}`);
|
|
1160
|
-
}
|
|
1161
|
-
else {
|
|
1162
|
-
settingsService.setProviderSetting(this.name, 'toolFormat', format);
|
|
1163
|
-
this.logger.debug(() => `Tool format override set to '${format}' for ${this.name}`);
|
|
1164
|
-
}
|
|
1165
|
-
// Clear cached client to ensure new format takes effect
|
|
1166
|
-
this._cachedClient = undefined;
|
|
1167
|
-
this._cachedClientKey = undefined;
|
|
1168
|
-
}
|
|
1169
1110
|
/**
|
|
1170
1111
|
* Detects the tool call format based on the model being used
|
|
1171
1112
|
* @returns The detected tool format ('openai' or 'qwen')
|
|
1172
1113
|
*/
|
|
1173
1114
|
detectToolFormat() {
|
|
1174
|
-
try {
|
|
1175
|
-
// Check for toolFormat override in provider settings
|
|
1176
|
-
const settingsService = getSettingsService();
|
|
1177
|
-
const currentSettings = settingsService['settings'];
|
|
1178
|
-
const providerSettings = currentSettings?.providers?.[this.name];
|
|
1179
|
-
const toolFormatOverride = providerSettings?.toolFormat;
|
|
1180
|
-
// If explicitly set to a specific format (not 'auto'), use it
|
|
1181
|
-
if (toolFormatOverride && toolFormatOverride !== 'auto') {
|
|
1182
|
-
this.logger.debug(() => `Using tool format override '${toolFormatOverride}' for ${this.name}`);
|
|
1183
|
-
return toolFormatOverride;
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
catch (error) {
|
|
1187
|
-
this.logger.debug(() => `Failed to detect tool format from SettingsService: ${error}`);
|
|
1188
|
-
}
|
|
1189
1115
|
// Auto-detect based on model name if set to 'auto' or not set
|
|
1190
1116
|
const modelName = (this.getModel() || this.getDefaultModel()).toLowerCase();
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
}
|
|
1196
|
-
// Check for MiniMax models (minimax, mini-max, etc.) which require Qwen handling
|
|
1197
|
-
if (modelName.includes('minimax') || modelName.includes('mini-max')) {
|
|
1198
|
-
this.logger.debug(() => `Auto-detected 'qwen' format for MiniMax model: ${modelName}`);
|
|
1117
|
+
const logger = new DebugLogger('llxprt:provider:openai');
|
|
1118
|
+
// Check for GLM-4 models (glm-4, glm-4.5, glm-4.6, glm-4-5, etc.)
|
|
1119
|
+
if (modelName.includes('glm-4')) {
|
|
1120
|
+
logger.debug(() => `Auto-detected 'qwen' format for GLM-4.x model: ${modelName}`);
|
|
1199
1121
|
return 'qwen';
|
|
1200
1122
|
}
|
|
1201
1123
|
// Check for qwen models
|
|
1202
1124
|
if (modelName.includes('qwen')) {
|
|
1203
|
-
|
|
1125
|
+
logger.debug(() => `Auto-detected 'qwen' format for Qwen model: ${modelName}`);
|
|
1204
1126
|
return 'qwen';
|
|
1205
1127
|
}
|
|
1206
1128
|
// Default to 'openai' format
|
|
1207
|
-
|
|
1129
|
+
logger.debug(() => `Using default 'openai' format for model: ${modelName}`);
|
|
1208
1130
|
return 'openai';
|
|
1209
1131
|
}
|
|
1210
1132
|
/**
|
|
@@ -1218,11 +1140,14 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1218
1140
|
return response;
|
|
1219
1141
|
}
|
|
1220
1142
|
/**
|
|
1143
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
1144
|
+
* @requirement:REQ-SP4-003
|
|
1221
1145
|
* Determines whether a response should be retried based on error codes
|
|
1222
1146
|
* @param error The error object from the API response
|
|
1223
1147
|
* @returns true if the request should be retried, false otherwise
|
|
1224
1148
|
*/
|
|
1225
1149
|
shouldRetryResponse(error) {
|
|
1150
|
+
const logger = new DebugLogger('llxprt:provider:openai');
|
|
1226
1151
|
// Don't retry if we're streaming chunks - just continue processing
|
|
1227
1152
|
if (error &&
|
|
1228
1153
|
typeof error === 'object' &&
|
|
@@ -1250,7 +1175,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1250
1175
|
}
|
|
1251
1176
|
}
|
|
1252
1177
|
// Log what we're seeing
|
|
1253
|
-
|
|
1178
|
+
logger.debug(() => `shouldRetryResponse checking error:`, {
|
|
1254
1179
|
hasError: !!error,
|
|
1255
1180
|
errorType: error?.constructor?.name,
|
|
1256
1181
|
status,
|
|
@@ -1258,15 +1183,11 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1258
1183
|
errorKeys: error && typeof error === 'object' ? Object.keys(error) : [],
|
|
1259
1184
|
});
|
|
1260
1185
|
// Retry on 429 rate limit errors or 5xx server errors
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1186
|
+
const shouldRetry = Boolean(status === 429 || (status && status >= 500 && status < 600));
|
|
1187
|
+
if (shouldRetry) {
|
|
1188
|
+
logger.debug(() => `Will retry request due to status ${status}`);
|
|
1264
1189
|
}
|
|
1265
|
-
|
|
1266
|
-
this.logger.debug(() => 'Will retry request due to transient network error signature (connection-level failure).');
|
|
1267
|
-
return true;
|
|
1268
|
-
}
|
|
1269
|
-
return false;
|
|
1190
|
+
return shouldRetry;
|
|
1270
1191
|
}
|
|
1271
1192
|
}
|
|
1272
1193
|
//# sourceMappingURL=OpenAIProvider.js.map
|