@vybestack/llxprt-code-core 0.4.7 → 0.5.0-nightly.251102.6bb3db7a
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 +235 -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.d.ts +17 -1
- package/dist/src/parsers/TextToolCallParser.js +542 -148
- 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 +392 -457
- 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 +65 -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 +65 -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/editor.js +10 -8
- package/dist/src/utils/editor.js.map +1 -1
- 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,334 +21,212 @@ 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;
|
|
41
46
|
// Detect if this is a Qwen endpoint
|
|
42
47
|
// CRITICAL FIX: For now, only use base URL check in constructor since `this.name` isn't available yet
|
|
43
48
|
// The name-based check will be handled in the supportsOAuth() method after construction
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
baseURL.
|
|
49
|
+
let isQwenEndpoint = false;
|
|
50
|
+
if (baseURL) {
|
|
51
|
+
try {
|
|
52
|
+
const hostname = new URL(baseURL).hostname.toLowerCase();
|
|
53
|
+
isQwenEndpoint =
|
|
54
|
+
hostname === 'dashscope.aliyuncs.com' ||
|
|
55
|
+
hostname.endsWith('.dashscope.aliyuncs.com') ||
|
|
56
|
+
hostname === 'api.qwen.com' ||
|
|
57
|
+
hostname.endsWith('.qwen.com');
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
const lowered = baseURL.toLowerCase();
|
|
61
|
+
isQwenEndpoint =
|
|
62
|
+
lowered.includes('dashscope.aliyuncs.com') ||
|
|
63
|
+
lowered.includes('api.qwen.com') ||
|
|
64
|
+
lowered.includes('qwen.com');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const forceQwenOAuth = Boolean(config?.forceQwenOAuth);
|
|
48
68
|
// Initialize base provider with auth configuration
|
|
49
69
|
super({
|
|
50
70
|
name: 'openai',
|
|
51
71
|
apiKey: normalizedApiKey,
|
|
52
72
|
baseURL,
|
|
53
73
|
envKeyNames: ['OPENAI_API_KEY'], // Support environment variable fallback
|
|
54
|
-
isOAuthEnabled: isQwenEndpoint && !!oauthManager,
|
|
55
|
-
oauthProvider: isQwenEndpoint ? 'qwen' : undefined,
|
|
74
|
+
isOAuthEnabled: (isQwenEndpoint || forceQwenOAuth) && !!oauthManager,
|
|
75
|
+
oauthProvider: isQwenEndpoint || forceQwenOAuth ? 'qwen' : undefined,
|
|
56
76
|
oauthManager,
|
|
57
77
|
}, 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
|
-
});
|
|
78
|
+
// @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
79
|
+
// @requirement:REQ-SP4-002
|
|
80
|
+
// No constructor-captured state - all values sourced from normalized options per call
|
|
65
81
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Create HTTP/HTTPS agents with socket configuration for local AI servers
|
|
84
|
+
* Returns undefined if no socket settings are configured
|
|
85
|
+
*
|
|
86
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
87
|
+
* @requirement:REQ-SP4-003
|
|
88
|
+
* Now sources ephemeral settings from call options instead of provider config
|
|
89
|
+
*/
|
|
90
|
+
createHttpAgents(options) {
|
|
91
|
+
// Get socket configuration from call options or fallback to provider config
|
|
92
|
+
const settingsFromInvocation = options?.invocation?.ephemerals;
|
|
93
|
+
const settings = settingsFromInvocation ??
|
|
94
|
+
this.providerConfig?.getEphemeralSettings?.() ??
|
|
95
|
+
{};
|
|
96
|
+
// Check if any socket settings are explicitly configured
|
|
97
|
+
const hasSocketSettings = 'socket-timeout' in settings ||
|
|
98
|
+
'socket-keepalive' in settings ||
|
|
99
|
+
'socket-nodelay' in settings;
|
|
100
|
+
// Only create custom agents if socket settings are configured
|
|
101
|
+
if (!hasSocketSettings) {
|
|
75
102
|
return undefined;
|
|
76
103
|
}
|
|
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);
|
|
104
|
+
// Socket configuration with defaults for when settings ARE configured
|
|
105
|
+
const socketTimeout = settings['socket-timeout'] || 60000; // 60 seconds default
|
|
106
|
+
const socketKeepAlive = settings['socket-keepalive'] !== false; // true by default
|
|
107
|
+
const socketNoDelay = settings['socket-nodelay'] !== false; // true by default
|
|
108
|
+
// Create HTTP agent with socket options
|
|
109
|
+
const httpAgent = new http.Agent({
|
|
110
|
+
keepAlive: socketKeepAlive,
|
|
111
|
+
keepAliveMsecs: 1000,
|
|
112
|
+
timeout: socketTimeout,
|
|
183
113
|
});
|
|
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) => {
|
|
114
|
+
// Create HTTPS agent with socket options
|
|
115
|
+
const httpsAgent = new https.Agent({
|
|
116
|
+
keepAlive: socketKeepAlive,
|
|
117
|
+
keepAliveMsecs: 1000,
|
|
118
|
+
timeout: socketTimeout,
|
|
119
|
+
});
|
|
120
|
+
// Apply TCP_NODELAY if enabled (reduces latency for local servers)
|
|
121
|
+
if (socketNoDelay) {
|
|
122
|
+
const originalCreateConnection = httpAgent.createConnection;
|
|
123
|
+
httpAgent.createConnection = function (options, callback) {
|
|
124
|
+
const socket = originalCreateConnection.call(this, options, callback);
|
|
240
125
|
if (socket instanceof net.Socket) {
|
|
241
|
-
socket.
|
|
242
|
-
socket.setKeepAlive(keepAlive, 1000);
|
|
243
|
-
socket.setNoDelay(noDelay);
|
|
126
|
+
socket.setNoDelay(true);
|
|
244
127
|
}
|
|
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
|
-
}
|
|
128
|
+
return socket;
|
|
129
|
+
};
|
|
130
|
+
const originalHttpsCreateConnection = httpsAgent.createConnection;
|
|
131
|
+
httpsAgent.createConnection = function (options, callback) {
|
|
132
|
+
const socket = originalHttpsCreateConnection.call(this, options, callback);
|
|
133
|
+
if (socket instanceof net.Socket) {
|
|
134
|
+
socket.setNoDelay(true);
|
|
276
135
|
}
|
|
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;
|
|
136
|
+
return socket;
|
|
137
|
+
};
|
|
300
138
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
139
|
+
return { httpAgent, httpsAgent };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
143
|
+
* @requirement:REQ-SP4-002
|
|
144
|
+
* Extract model parameters from normalized options instead of settings service
|
|
145
|
+
*/
|
|
146
|
+
extractModelParamsFromOptions(options) {
|
|
147
|
+
const providerSettings = options.settings?.getProviderSettings(this.name) ?? {};
|
|
148
|
+
const configEphemerals = options.invocation?.ephemerals ?? {};
|
|
149
|
+
const filteredProviderParams = filterOpenAIRequestParams(providerSettings);
|
|
150
|
+
const filteredEphemeralParams = filterOpenAIRequestParams(configEphemerals);
|
|
151
|
+
if (!filteredProviderParams && !filteredEphemeralParams) {
|
|
152
|
+
return undefined;
|
|
304
153
|
}
|
|
305
|
-
return
|
|
154
|
+
return {
|
|
155
|
+
...(filteredProviderParams ?? {}),
|
|
156
|
+
...(filteredEphemeralParams ?? {}),
|
|
157
|
+
};
|
|
306
158
|
}
|
|
307
159
|
/**
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
160
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
161
|
+
* @requirement:REQ-SP4-003
|
|
162
|
+
* Resolve runtime key from normalized options for client scoping
|
|
311
163
|
*/
|
|
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;
|
|
164
|
+
resolveRuntimeKey(options) {
|
|
165
|
+
if (options.runtime?.runtimeId) {
|
|
166
|
+
return options.runtime.runtimeId;
|
|
325
167
|
}
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
return
|
|
168
|
+
const metadataRuntimeId = options.metadata?.runtimeId;
|
|
169
|
+
if (typeof metadataRuntimeId === 'string' && metadataRuntimeId.trim()) {
|
|
170
|
+
return metadataRuntimeId.trim();
|
|
329
171
|
}
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
172
|
+
const callId = options.settings.get('call-id');
|
|
173
|
+
if (typeof callId === 'string' && callId.trim()) {
|
|
174
|
+
return `call:${callId.trim()}`;
|
|
175
|
+
}
|
|
176
|
+
return 'openai.runtime.unscoped';
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Tool formatter instances cannot be shared between stateless calls,
|
|
180
|
+
* so construct a fresh one for every invocation.
|
|
181
|
+
*
|
|
182
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
183
|
+
* @requirement:REQ-SP4-003
|
|
184
|
+
*/
|
|
185
|
+
createToolFormatter() {
|
|
186
|
+
return new ToolFormatter();
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P09
|
|
190
|
+
* @requirement:REQ-SP4-002
|
|
191
|
+
* Instantiates a fresh OpenAI client per call to preserve stateless behaviour.
|
|
192
|
+
*/
|
|
193
|
+
instantiateClient(authToken, baseURL, agents) {
|
|
194
|
+
const clientOptions = {
|
|
195
|
+
apiKey: authToken || '',
|
|
335
196
|
maxRetries: 0,
|
|
336
197
|
};
|
|
337
|
-
if (
|
|
338
|
-
|
|
339
|
-
baseOptions.fetch = this.createSocketAwareFetch(socketSettings);
|
|
198
|
+
if (baseURL && baseURL.trim() !== '') {
|
|
199
|
+
clientOptions.baseURL = baseURL;
|
|
340
200
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return
|
|
201
|
+
if (agents) {
|
|
202
|
+
clientOptions.httpAgent = agents.httpAgent;
|
|
203
|
+
clientOptions.httpsAgent = agents.httpsAgent;
|
|
204
|
+
}
|
|
205
|
+
return new OpenAI(clientOptions);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P09
|
|
209
|
+
* @requirement:REQ-SP4-002
|
|
210
|
+
* Creates a client scoped to the active runtime metadata without caching.
|
|
211
|
+
*/
|
|
212
|
+
async getClient(options) {
|
|
213
|
+
const authToken = (await resolveRuntimeAuthToken(options.resolved.authToken)) ?? '';
|
|
214
|
+
if (!authToken) {
|
|
215
|
+
throw new Error(`ProviderCacheError("Auth token unavailable for runtimeId=${options.runtime?.runtimeId} (REQ-SP4-003).")`);
|
|
216
|
+
}
|
|
217
|
+
const baseURL = options.resolved.baseURL ?? this.baseProviderConfig.baseURL;
|
|
218
|
+
const agents = this.createHttpAgents(options);
|
|
219
|
+
return this.instantiateClient(authToken, baseURL, agents);
|
|
346
220
|
}
|
|
347
221
|
/**
|
|
348
222
|
* Check if OAuth is supported for this provider
|
|
349
223
|
* Qwen endpoints support OAuth, standard OpenAI does not
|
|
350
224
|
*/
|
|
351
225
|
supportsOAuth() {
|
|
226
|
+
const providerConfig = this.providerConfig;
|
|
227
|
+
if (providerConfig?.forceQwenOAuth) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
352
230
|
// CRITICAL FIX: Check provider name first for cases where base URL is changed by profiles
|
|
353
231
|
// This handles the cerebrasqwen3 profile case where base-url is changed to cerebras.ai
|
|
354
232
|
// but the provider name is still 'qwen' due to Object.defineProperty override
|
|
@@ -370,7 +248,10 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
370
248
|
try {
|
|
371
249
|
// Always try to fetch models, regardless of auth status
|
|
372
250
|
// Local endpoints often work without authentication
|
|
373
|
-
const
|
|
251
|
+
const authToken = await this.getAuthToken();
|
|
252
|
+
const baseURL = this.getBaseURL();
|
|
253
|
+
const agents = this.createHttpAgents();
|
|
254
|
+
const client = this.instantiateClient(authToken, baseURL, agents);
|
|
374
255
|
const response = await client.models.list();
|
|
375
256
|
const models = [];
|
|
376
257
|
for await (const model of response) {
|
|
@@ -387,7 +268,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
387
268
|
return models;
|
|
388
269
|
}
|
|
389
270
|
catch (error) {
|
|
390
|
-
this.
|
|
271
|
+
this.getLogger().debug(() => `Error fetching models from OpenAI: ${error}`);
|
|
391
272
|
// Return a hardcoded list as fallback
|
|
392
273
|
return this.getFallbackModels();
|
|
393
274
|
}
|
|
@@ -397,9 +278,6 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
397
278
|
}
|
|
398
279
|
getDefaultModel() {
|
|
399
280
|
// Return hardcoded default - do NOT call getModel() to avoid circular dependency
|
|
400
|
-
if (this.providerConfig?.defaultModel) {
|
|
401
|
-
return this.providerConfig.defaultModel;
|
|
402
|
-
}
|
|
403
281
|
// Check if this is a Qwen provider instance based on baseURL
|
|
404
282
|
const baseURL = this.getBaseURL();
|
|
405
283
|
if (baseURL &&
|
|
@@ -408,15 +286,6 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
408
286
|
}
|
|
409
287
|
return process.env.LLXPRT_DEFAULT_MODEL || 'gpt-5';
|
|
410
288
|
}
|
|
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
289
|
/**
|
|
421
290
|
* Get the currently selected model
|
|
422
291
|
*/
|
|
@@ -424,32 +293,51 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
424
293
|
return this.getModel();
|
|
425
294
|
}
|
|
426
295
|
/**
|
|
427
|
-
*
|
|
428
|
-
*
|
|
296
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P09
|
|
297
|
+
* @requirement:REQ-SP4-002
|
|
298
|
+
* No-op retained for compatibility because clients are no longer cached.
|
|
429
299
|
*/
|
|
430
300
|
// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility
|
|
431
|
-
clearClientCache() {
|
|
432
|
-
|
|
433
|
-
this._cachedClientKey = undefined;
|
|
301
|
+
clearClientCache(runtimeKey) {
|
|
302
|
+
void runtimeKey;
|
|
434
303
|
}
|
|
435
304
|
/**
|
|
436
305
|
* Override isAuthenticated for qwen provider to check OAuth directly
|
|
437
306
|
*/
|
|
438
307
|
async isAuthenticated() {
|
|
439
308
|
const config = this.providerConfig;
|
|
309
|
+
const directApiKey = this.baseProviderConfig.apiKey;
|
|
310
|
+
if (typeof directApiKey === 'string' && directApiKey.trim() !== '') {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const nonOAuthToken = await this.authResolver.resolveAuthentication({
|
|
315
|
+
settingsService: this.resolveSettingsService(),
|
|
316
|
+
includeOAuth: false,
|
|
317
|
+
});
|
|
318
|
+
if (typeof nonOAuthToken === 'string' && nonOAuthToken.trim() !== '') {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (process.env.DEBUG) {
|
|
324
|
+
this.getLogger().debug(() => `[openai] non-OAuth authentication resolution failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
440
327
|
if (this.name === 'qwen' && config?.forceQwenOAuth) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
328
|
+
try {
|
|
329
|
+
const token = await this.authResolver.resolveAuthentication({
|
|
330
|
+
settingsService: this.resolveSettingsService(),
|
|
331
|
+
includeOAuth: true,
|
|
332
|
+
});
|
|
333
|
+
return typeof token === 'string' && token.trim() !== '';
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (process.env.DEBUG) {
|
|
337
|
+
this.getLogger().debug(() => `[openai] forced OAuth authentication failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
450
338
|
}
|
|
339
|
+
return false;
|
|
451
340
|
}
|
|
452
|
-
return false;
|
|
453
341
|
}
|
|
454
342
|
// For non-qwen providers, use the normal check
|
|
455
343
|
return super.isAuthenticated();
|
|
@@ -462,15 +350,6 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
462
350
|
// If this is the qwen provider and we have forceQwenOAuth, skip SettingsService checks
|
|
463
351
|
const config = this.providerConfig;
|
|
464
352
|
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
353
|
// For qwen, skip directly to OAuth without checking SettingsService
|
|
475
354
|
// Use 'qwen' as the provider name even if baseProviderConfig.oauthProvider is not set
|
|
476
355
|
const oauthProviderName = this.baseProviderConfig.oauthProvider || 'qwen';
|
|
@@ -478,9 +357,6 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
478
357
|
try {
|
|
479
358
|
const token = await this.baseProviderConfig.oauthManager.getToken(oauthProviderName);
|
|
480
359
|
if (token) {
|
|
481
|
-
// Cache the token briefly
|
|
482
|
-
this.cachedAuthToken = token;
|
|
483
|
-
this.authCacheTimestamp = Date.now();
|
|
484
360
|
return token;
|
|
485
361
|
}
|
|
486
362
|
}
|
|
@@ -558,25 +434,36 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
558
434
|
return 'hist_tool_' + id;
|
|
559
435
|
}
|
|
560
436
|
/**
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
* @
|
|
564
|
-
* @
|
|
437
|
+
* @plan PLAN-20250218-STATELESSPROVIDER.P04
|
|
438
|
+
* @requirement REQ-SP-001
|
|
439
|
+
* @pseudocode base-provider.md lines 7-15
|
|
440
|
+
* @pseudocode provider-invocation.md lines 8-12
|
|
565
441
|
*/
|
|
566
|
-
|
|
442
|
+
/**
|
|
443
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P09
|
|
444
|
+
* @requirement:REQ-SP4-002
|
|
445
|
+
* Generate chat completion with per-call client instantiation.
|
|
446
|
+
*/
|
|
447
|
+
async *generateChatCompletionWithOptions(options) {
|
|
448
|
+
const callFormatter = this.createToolFormatter();
|
|
449
|
+
const client = await this.getClient(options);
|
|
450
|
+
const runtimeKey = this.resolveRuntimeKey(options);
|
|
451
|
+
const { tools } = options;
|
|
452
|
+
const logger = new DebugLogger('llxprt:provider:openai');
|
|
567
453
|
// Debug log what we receive
|
|
568
|
-
if (
|
|
569
|
-
|
|
454
|
+
if (logger.enabled) {
|
|
455
|
+
logger.debug(() => `[OpenAIProvider] generateChatCompletion received tools:`, {
|
|
570
456
|
hasTools: !!tools,
|
|
571
457
|
toolsLength: tools?.length,
|
|
572
458
|
toolsType: typeof tools,
|
|
573
459
|
isArray: Array.isArray(tools),
|
|
574
460
|
firstToolName: tools?.[0]?.functionDeclarations?.[0]?.name,
|
|
575
461
|
toolsStructure: tools ? 'available' : 'undefined',
|
|
462
|
+
runtimeKey,
|
|
576
463
|
});
|
|
577
464
|
}
|
|
578
|
-
// Pass tools directly in Gemini format - they'll be converted
|
|
579
|
-
const generator = this.generateChatCompletionImpl(
|
|
465
|
+
// Pass tools directly in Gemini format - they'll be converted per call
|
|
466
|
+
const generator = this.generateChatCompletionImpl(options, callFormatter, client, logger);
|
|
580
467
|
for await (const item of generator) {
|
|
581
468
|
yield item;
|
|
582
469
|
}
|
|
@@ -646,27 +533,44 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
646
533
|
return messages;
|
|
647
534
|
}
|
|
648
535
|
/**
|
|
649
|
-
*
|
|
536
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
537
|
+
* @requirement:REQ-SP4-003
|
|
538
|
+
* Internal implementation for chat completion using normalized options
|
|
650
539
|
*/
|
|
651
|
-
async *generateChatCompletionImpl(
|
|
652
|
-
|
|
653
|
-
const model =
|
|
540
|
+
async *generateChatCompletionImpl(options, toolFormatter, client, logger) {
|
|
541
|
+
const { contents, tools, metadata } = options;
|
|
542
|
+
const model = options.resolved.model || this.getDefaultModel();
|
|
543
|
+
const abortSignal = metadata?.abortSignal;
|
|
544
|
+
const ephemeralSettings = options.invocation?.ephemerals ?? {};
|
|
545
|
+
if (logger.enabled) {
|
|
546
|
+
const resolved = options.resolved;
|
|
547
|
+
logger.debug(() => `[OpenAIProvider] Resolved request context`, {
|
|
548
|
+
provider: this.name,
|
|
549
|
+
model,
|
|
550
|
+
resolvedModel: resolved.model,
|
|
551
|
+
resolvedBaseUrl: resolved.baseURL,
|
|
552
|
+
authTokenPresent: Boolean(resolved.authToken),
|
|
553
|
+
messageCount: contents.length,
|
|
554
|
+
toolCount: tools?.length ?? 0,
|
|
555
|
+
metadataKeys: Object.keys(metadata ?? {}),
|
|
556
|
+
});
|
|
557
|
+
}
|
|
654
558
|
// Convert IContent to OpenAI messages format
|
|
655
559
|
const messages = this.convertToOpenAIMessages(contents);
|
|
656
560
|
// Detect the tool format to use (once at the start of the method)
|
|
657
561
|
const detectedFormat = this.detectToolFormat();
|
|
658
562
|
// Log the detected format for debugging
|
|
659
|
-
|
|
563
|
+
logger.debug(() => `[OpenAIProvider] Using tool format '${detectedFormat}' for model '${model}'`, {
|
|
660
564
|
model,
|
|
661
565
|
detectedFormat,
|
|
662
566
|
provider: this.name,
|
|
663
567
|
});
|
|
664
568
|
// Convert Gemini format tools to the detected format
|
|
665
|
-
let formattedTools =
|
|
569
|
+
let formattedTools = toolFormatter.convertGeminiToFormat(tools, detectedFormat);
|
|
666
570
|
// CRITICAL FIX: Ensure we never pass an empty tools array
|
|
667
571
|
// The OpenAI API errors when tools=[] but a tool call is attempted
|
|
668
572
|
if (Array.isArray(formattedTools) && formattedTools.length === 0) {
|
|
669
|
-
|
|
573
|
+
logger.warn(() => `[OpenAIProvider] CRITICAL: Formatted tools is empty array! Setting to undefined to prevent API errors.`, {
|
|
670
574
|
model,
|
|
671
575
|
inputTools: tools,
|
|
672
576
|
inputToolsLength: tools?.length,
|
|
@@ -676,8 +580,8 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
676
580
|
formattedTools = undefined;
|
|
677
581
|
}
|
|
678
582
|
// Debug log the conversion result - enhanced logging for intermittent issues
|
|
679
|
-
if (
|
|
680
|
-
|
|
583
|
+
if (logger.enabled && formattedTools) {
|
|
584
|
+
logger.debug(() => `[OpenAIProvider] Tool conversion summary:`, {
|
|
681
585
|
detectedFormat,
|
|
682
586
|
inputHadTools: !!tools,
|
|
683
587
|
inputToolsLength: tools?.length,
|
|
@@ -689,48 +593,61 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
689
593
|
});
|
|
690
594
|
}
|
|
691
595
|
// Get streaming setting from ephemeral settings (default: enabled)
|
|
692
|
-
const streamingSetting =
|
|
596
|
+
const streamingSetting = ephemeralSettings['streaming'];
|
|
693
597
|
const streamingEnabled = streamingSetting !== 'disabled';
|
|
694
598
|
// Get the system prompt
|
|
695
599
|
const flattenedToolNames = tools?.flatMap((group) => group.functionDeclarations
|
|
696
600
|
.map((decl) => decl.name)
|
|
697
601
|
.filter((name) => !!name)) ?? [];
|
|
698
602
|
const toolNamesArg = tools === undefined ? undefined : Array.from(new Set(flattenedToolNames));
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
603
|
+
/**
|
|
604
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
605
|
+
* @requirement:REQ-SP4-003
|
|
606
|
+
* Source user memory from normalized options instead of global config
|
|
607
|
+
*/
|
|
608
|
+
const userMemory = await resolveUserMemory(options.userMemory, () => options.invocation?.userMemory);
|
|
702
609
|
const systemPrompt = await getCoreSystemPromptAsync(userMemory, model, toolNamesArg);
|
|
703
610
|
// Add system prompt as the first message in the array
|
|
704
611
|
const messagesWithSystem = [
|
|
705
612
|
{ role: 'system', content: systemPrompt },
|
|
706
613
|
...messages,
|
|
707
614
|
];
|
|
615
|
+
const maxTokens = metadata?.maxTokens ??
|
|
616
|
+
ephemeralSettings['max-tokens'];
|
|
708
617
|
// Build request - only include tools if they exist and are not empty
|
|
709
618
|
// IMPORTANT: Create a deep copy of tools to prevent mutation issues
|
|
710
619
|
const requestBody = {
|
|
711
620
|
model,
|
|
712
621
|
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
622
|
stream: streamingEnabled,
|
|
723
623
|
};
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
624
|
+
if (formattedTools && formattedTools.length > 0) {
|
|
625
|
+
requestBody.tools = JSON.parse(JSON.stringify(formattedTools));
|
|
626
|
+
requestBody.tool_choice = 'auto';
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
630
|
+
* @requirement:REQ-SP4-002
|
|
631
|
+
* Extract per-call request overrides from normalized options instead of cached state
|
|
632
|
+
*/
|
|
633
|
+
const requestOverrides = this.extractModelParamsFromOptions(options);
|
|
634
|
+
if (requestOverrides) {
|
|
635
|
+
if (logger.enabled) {
|
|
636
|
+
logger.debug(() => `[OpenAIProvider] Applying request overrides`, {
|
|
637
|
+
overrideKeys: Object.keys(requestOverrides),
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
Object.assign(requestBody, requestOverrides);
|
|
641
|
+
}
|
|
642
|
+
if (typeof maxTokens === 'number' && Number.isFinite(maxTokens)) {
|
|
643
|
+
requestBody.max_tokens = maxTokens;
|
|
727
644
|
}
|
|
728
645
|
// Debug log request summary for Cerebras/Qwen
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
baseURL
|
|
646
|
+
const baseURL = options.resolved.baseURL ?? this.getBaseURL();
|
|
647
|
+
if (logger.enabled &&
|
|
648
|
+
(model.toLowerCase().includes('qwen') || baseURL?.includes('cerebras'))) {
|
|
649
|
+
logger.debug(() => `Request to ${baseURL} for model ${model}:`, {
|
|
650
|
+
baseURL,
|
|
734
651
|
model,
|
|
735
652
|
streamingEnabled,
|
|
736
653
|
hasTools: 'tools' in requestBody,
|
|
@@ -739,10 +656,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
739
656
|
toolsInRequest: 'tools' in requestBody ? requestBody.tools?.length : 'not included',
|
|
740
657
|
});
|
|
741
658
|
}
|
|
742
|
-
// Get OpenAI client
|
|
743
|
-
const client = await this.getClient();
|
|
744
659
|
// Get retry settings from ephemeral settings
|
|
745
|
-
const ephemeralSettings = this.providerConfig?.getEphemeralSettings?.() || {};
|
|
746
660
|
const maxRetries = ephemeralSettings['retries'] ?? 6; // Default for OpenAI
|
|
747
661
|
const initialDelayMs = ephemeralSettings['retrywait'] ?? 4000; // Default for OpenAI
|
|
748
662
|
// Get stream options from ephemeral settings (default: include usage for token tracking)
|
|
@@ -752,23 +666,43 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
752
666
|
Object.assign(requestBody, { stream_options: streamOptions });
|
|
753
667
|
}
|
|
754
668
|
// Log the exact tools being sent for debugging
|
|
755
|
-
if (
|
|
756
|
-
|
|
669
|
+
if (logger.enabled && 'tools' in requestBody) {
|
|
670
|
+
logger.debug(() => `[OpenAIProvider] Exact tools being sent to API:`, {
|
|
757
671
|
toolCount: requestBody.tools?.length,
|
|
758
672
|
toolNames: requestBody.tools?.map((t) => 'function' in t ? t.function?.name : undefined),
|
|
759
673
|
firstTool: requestBody.tools?.[0],
|
|
760
674
|
});
|
|
761
675
|
}
|
|
762
676
|
// Wrap the API call with retry logic using centralized retry utility
|
|
677
|
+
if (logger.enabled) {
|
|
678
|
+
logger.debug(() => `[OpenAIProvider] Sending chat request`, {
|
|
679
|
+
model,
|
|
680
|
+
baseURL: baseURL ?? this.getBaseURL(),
|
|
681
|
+
streamingEnabled,
|
|
682
|
+
toolCount: formattedTools?.length ?? 0,
|
|
683
|
+
hasAuthToken: Boolean(options.resolved.authToken),
|
|
684
|
+
requestHasSystemPrompt: Boolean(systemPrompt?.length),
|
|
685
|
+
messageCount: messagesWithSystem.length,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
763
688
|
let response;
|
|
764
689
|
// Debug log throttle tracker status
|
|
765
|
-
|
|
690
|
+
logger.debug(() => `Retry configuration:`, {
|
|
766
691
|
hasThrottleTracker: !!this.throttleTracker,
|
|
767
692
|
throttleTrackerType: typeof this.throttleTracker,
|
|
768
693
|
maxRetries,
|
|
769
694
|
initialDelayMs,
|
|
770
695
|
});
|
|
771
696
|
const customHeaders = this.getCustomHeaders();
|
|
697
|
+
if (logger.enabled) {
|
|
698
|
+
logger.debug(() => `[OpenAIProvider] Request body preview`, {
|
|
699
|
+
model: requestBody.model,
|
|
700
|
+
hasStop: 'stop' in requestBody,
|
|
701
|
+
hasMaxTokens: 'max_tokens' in requestBody,
|
|
702
|
+
hasResponseFormat: 'response_format' in requestBody,
|
|
703
|
+
overrideKeys: requestOverrides ? Object.keys(requestOverrides) : [],
|
|
704
|
+
});
|
|
705
|
+
}
|
|
772
706
|
try {
|
|
773
707
|
response = await retryWithBackoff(() => client.chat.completions.create(requestBody, {
|
|
774
708
|
...(abortSignal ? { signal: abortSignal } : {}),
|
|
@@ -786,7 +720,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
786
720
|
if (errorMessage.includes('Tool is not present in the tools list') &&
|
|
787
721
|
(model.toLowerCase().includes('qwen') ||
|
|
788
722
|
this.getBaseURL()?.includes('cerebras'))) {
|
|
789
|
-
|
|
723
|
+
logger.error('Cerebras/Qwen API error: Tool not found despite being in request. This is a known API issue.', {
|
|
790
724
|
error,
|
|
791
725
|
model,
|
|
792
726
|
toolsProvided: formattedTools?.length || 0,
|
|
@@ -800,6 +734,21 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
800
734
|
throw enhancedError;
|
|
801
735
|
}
|
|
802
736
|
// Re-throw other errors as-is
|
|
737
|
+
const capturedErrorMessage = error instanceof Error ? error.message : String(error);
|
|
738
|
+
const status = typeof error === 'object' &&
|
|
739
|
+
error !== null &&
|
|
740
|
+
'status' in error &&
|
|
741
|
+
typeof error.status === 'number'
|
|
742
|
+
? error.status
|
|
743
|
+
: undefined;
|
|
744
|
+
logger.error(() => `[OpenAIProvider] Chat completion failed for model '${model}' at '${baseURL ?? this.getBaseURL() ?? 'default'}': ${capturedErrorMessage}`, {
|
|
745
|
+
model,
|
|
746
|
+
baseURL: baseURL ?? this.getBaseURL(),
|
|
747
|
+
streamingEnabled,
|
|
748
|
+
hasTools: formattedTools?.length ?? 0,
|
|
749
|
+
requestHasSystemPrompt: !!systemPrompt,
|
|
750
|
+
status,
|
|
751
|
+
});
|
|
803
752
|
throw error;
|
|
804
753
|
}
|
|
805
754
|
// Check if response is streaming or not
|
|
@@ -829,7 +778,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
829
778
|
continue;
|
|
830
779
|
// Check for finish_reason to detect proper stream ending
|
|
831
780
|
if (choice.finish_reason) {
|
|
832
|
-
|
|
781
|
+
logger.debug(() => `[Streaming] Stream finished with reason: ${choice.finish_reason}`, {
|
|
833
782
|
model,
|
|
834
783
|
finishReason: choice.finish_reason,
|
|
835
784
|
hasAccumulatedText: _accumulatedText.length > 0,
|
|
@@ -838,7 +787,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
838
787
|
});
|
|
839
788
|
// If finish_reason is 'length', the response was cut off
|
|
840
789
|
if (choice.finish_reason === 'length') {
|
|
841
|
-
|
|
790
|
+
logger.debug(() => `Response truncated due to length limit for model ${model}`);
|
|
842
791
|
}
|
|
843
792
|
// Flush any buffered text when stream finishes
|
|
844
793
|
if (textBuffer.length > 0) {
|
|
@@ -860,7 +809,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
860
809
|
_accumulatedText += deltaContent;
|
|
861
810
|
// Debug log for providers that need buffering
|
|
862
811
|
if (shouldBufferText) {
|
|
863
|
-
|
|
812
|
+
logger.debug(() => `[Streaming] Chunk content for ${detectedFormat} format:`, {
|
|
864
813
|
deltaContent,
|
|
865
814
|
length: deltaContent.length,
|
|
866
815
|
hasNewline: deltaContent.includes('\n'),
|
|
@@ -871,7 +820,8 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
871
820
|
textBuffer += deltaContent;
|
|
872
821
|
// Emit buffered text when we have a complete sentence or paragraph
|
|
873
822
|
// Look for natural break points
|
|
874
|
-
if (textBuffer.
|
|
823
|
+
if (textBuffer.includes('\n') ||
|
|
824
|
+
textBuffer.endsWith('. ') ||
|
|
875
825
|
textBuffer.endsWith('! ') ||
|
|
876
826
|
textBuffer.endsWith('? ') ||
|
|
877
827
|
textBuffer.length > 100) {
|
|
@@ -941,7 +891,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
941
891
|
if (errorMessage.includes('Tool is not present in the tools list') &&
|
|
942
892
|
(model.toLowerCase().includes('qwen') ||
|
|
943
893
|
this.getBaseURL()?.includes('cerebras'))) {
|
|
944
|
-
|
|
894
|
+
logger.error('Cerebras/Qwen API error: Tool not found despite being in request. This is a known API issue.', {
|
|
945
895
|
error,
|
|
946
896
|
model,
|
|
947
897
|
toolsProvided: formattedTools?.length || 0,
|
|
@@ -952,7 +902,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
952
902
|
enhancedError.originalError = error;
|
|
953
903
|
throw enhancedError;
|
|
954
904
|
}
|
|
955
|
-
|
|
905
|
+
logger.error('Error processing streaming response:', error);
|
|
956
906
|
throw error;
|
|
957
907
|
}
|
|
958
908
|
}
|
|
@@ -1031,7 +981,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1031
981
|
}
|
|
1032
982
|
// Log finish reason for debugging Qwen issues
|
|
1033
983
|
if (choice.finish_reason) {
|
|
1034
|
-
|
|
984
|
+
logger.debug(() => `[Non-streaming] Response finish_reason: ${choice.finish_reason}`, {
|
|
1035
985
|
model,
|
|
1036
986
|
finishReason: choice.finish_reason,
|
|
1037
987
|
hasContent: !!choice.message?.content,
|
|
@@ -1042,7 +992,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1042
992
|
});
|
|
1043
993
|
// Warn if the response was truncated
|
|
1044
994
|
if (choice.finish_reason === 'length') {
|
|
1045
|
-
|
|
995
|
+
logger.warn(() => `Response truncated due to max_tokens limit for model ${model}. Consider increasing max_tokens.`);
|
|
1046
996
|
}
|
|
1047
997
|
}
|
|
1048
998
|
const blocks = [];
|
|
@@ -1108,103 +1058,89 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1108
1058
|
}
|
|
1109
1059
|
}
|
|
1110
1060
|
/**
|
|
1111
|
-
*
|
|
1061
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
1062
|
+
* @requirement:REQ-SP4-002
|
|
1063
|
+
* Memoization of model parameters disabled for stateless provider
|
|
1112
1064
|
*/
|
|
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
|
-
});
|
|
1065
|
+
setModelParams(_params) {
|
|
1066
|
+
throw new Error('ProviderCacheError("Attempted to memoize model parameters for openai")');
|
|
1134
1067
|
}
|
|
1068
|
+
/**
|
|
1069
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
1070
|
+
* @requirement:REQ-SP4-003
|
|
1071
|
+
* Gets model parameters from SettingsService per call (stateless)
|
|
1072
|
+
*/
|
|
1135
1073
|
getModelParams() {
|
|
1136
|
-
|
|
1074
|
+
try {
|
|
1075
|
+
const settingsService = this.resolveSettingsService();
|
|
1076
|
+
const providerSettings = settingsService.getProviderSettings(this.name);
|
|
1077
|
+
const reservedKeys = new Set([
|
|
1078
|
+
'enabled',
|
|
1079
|
+
'apiKey',
|
|
1080
|
+
'api-key',
|
|
1081
|
+
'apiKeyfile',
|
|
1082
|
+
'api-keyfile',
|
|
1083
|
+
'baseUrl',
|
|
1084
|
+
'base-url',
|
|
1085
|
+
'model',
|
|
1086
|
+
'toolFormat',
|
|
1087
|
+
'tool-format',
|
|
1088
|
+
'toolFormatOverride',
|
|
1089
|
+
'tool-format-override',
|
|
1090
|
+
'defaultModel',
|
|
1091
|
+
]);
|
|
1092
|
+
const params = {};
|
|
1093
|
+
if (providerSettings) {
|
|
1094
|
+
for (const [key, value] of Object.entries(providerSettings)) {
|
|
1095
|
+
if (reservedKeys.has(key) || value === undefined || value === null) {
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
params[key] = value;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return Object.keys(params).length > 0 ? params : undefined;
|
|
1102
|
+
}
|
|
1103
|
+
catch (error) {
|
|
1104
|
+
this.getLogger().debug(() => `Failed to get OpenAI provider settings from SettingsService: ${error}`);
|
|
1105
|
+
return undefined;
|
|
1106
|
+
}
|
|
1137
1107
|
}
|
|
1138
1108
|
/**
|
|
1139
|
-
*
|
|
1109
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
1110
|
+
* @requirement:REQ-SP4-003
|
|
1111
|
+
* Get the tool format for this provider using normalized options
|
|
1140
1112
|
* @returns The tool format to use
|
|
1141
1113
|
*/
|
|
1142
1114
|
getToolFormat() {
|
|
1143
1115
|
const format = this.detectToolFormat();
|
|
1144
|
-
|
|
1116
|
+
const logger = new DebugLogger('llxprt:provider:openai');
|
|
1117
|
+
logger.debug(() => `getToolFormat() called, returning: ${format}`, {
|
|
1145
1118
|
provider: this.name,
|
|
1146
1119
|
model: this.getModel(),
|
|
1147
1120
|
format,
|
|
1148
1121
|
});
|
|
1149
1122
|
return format;
|
|
1150
1123
|
}
|
|
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
1124
|
/**
|
|
1170
1125
|
* Detects the tool call format based on the model being used
|
|
1171
1126
|
* @returns The detected tool format ('openai' or 'qwen')
|
|
1172
1127
|
*/
|
|
1173
1128
|
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
1129
|
// Auto-detect based on model name if set to 'auto' or not set
|
|
1190
1130
|
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}`);
|
|
1131
|
+
const logger = new DebugLogger('llxprt:provider:openai');
|
|
1132
|
+
// Check for GLM-4 models (glm-4, glm-4.5, glm-4.6, glm-4-5, etc.)
|
|
1133
|
+
if (modelName.includes('glm-4')) {
|
|
1134
|
+
logger.debug(() => `Auto-detected 'qwen' format for GLM-4.x model: ${modelName}`);
|
|
1199
1135
|
return 'qwen';
|
|
1200
1136
|
}
|
|
1201
1137
|
// Check for qwen models
|
|
1202
1138
|
if (modelName.includes('qwen')) {
|
|
1203
|
-
|
|
1139
|
+
logger.debug(() => `Auto-detected 'qwen' format for Qwen model: ${modelName}`);
|
|
1204
1140
|
return 'qwen';
|
|
1205
1141
|
}
|
|
1206
1142
|
// Default to 'openai' format
|
|
1207
|
-
|
|
1143
|
+
logger.debug(() => `Using default 'openai' format for model: ${modelName}`);
|
|
1208
1144
|
return 'openai';
|
|
1209
1145
|
}
|
|
1210
1146
|
/**
|
|
@@ -1218,11 +1154,14 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1218
1154
|
return response;
|
|
1219
1155
|
}
|
|
1220
1156
|
/**
|
|
1157
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
1158
|
+
* @requirement:REQ-SP4-003
|
|
1221
1159
|
* Determines whether a response should be retried based on error codes
|
|
1222
1160
|
* @param error The error object from the API response
|
|
1223
1161
|
* @returns true if the request should be retried, false otherwise
|
|
1224
1162
|
*/
|
|
1225
1163
|
shouldRetryResponse(error) {
|
|
1164
|
+
const logger = new DebugLogger('llxprt:provider:openai');
|
|
1226
1165
|
// Don't retry if we're streaming chunks - just continue processing
|
|
1227
1166
|
if (error &&
|
|
1228
1167
|
typeof error === 'object' &&
|
|
@@ -1250,7 +1189,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1250
1189
|
}
|
|
1251
1190
|
}
|
|
1252
1191
|
// Log what we're seeing
|
|
1253
|
-
|
|
1192
|
+
logger.debug(() => `shouldRetryResponse checking error:`, {
|
|
1254
1193
|
hasError: !!error,
|
|
1255
1194
|
errorType: error?.constructor?.name,
|
|
1256
1195
|
status,
|
|
@@ -1258,15 +1197,11 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1258
1197
|
errorKeys: error && typeof error === 'object' ? Object.keys(error) : [],
|
|
1259
1198
|
});
|
|
1260
1199
|
// Retry on 429 rate limit errors or 5xx server errors
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1200
|
+
const shouldRetry = Boolean(status === 429 || (status && status >= 500 && status < 600));
|
|
1201
|
+
if (shouldRetry) {
|
|
1202
|
+
logger.debug(() => `Will retry request due to status ${status}`);
|
|
1264
1203
|
}
|
|
1265
|
-
|
|
1266
|
-
this.logger.debug(() => 'Will retry request due to transient network error signature (connection-level failure).');
|
|
1267
|
-
return true;
|
|
1268
|
-
}
|
|
1269
|
-
return false;
|
|
1204
|
+
return shouldRetry;
|
|
1270
1205
|
}
|
|
1271
1206
|
}
|
|
1272
1207
|
//# sourceMappingURL=OpenAIProvider.js.map
|