@vybestack/llxprt-code-core 0.4.8 → 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 +15 -1
- package/dist/src/config/config.js +118 -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 +231 -55
- 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 +405 -205
- 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 +391 -536
- 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/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/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
|
|
441
|
+
*/
|
|
442
|
+
/**
|
|
443
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P09
|
|
444
|
+
* @requirement:REQ-SP4-002
|
|
445
|
+
* Generate chat completion with per-call client instantiation.
|
|
565
446
|
*/
|
|
566
|
-
async *
|
|
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,58 +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
|
-
|
|
727
|
-
if (this.getModel()?.toLowerCase().includes('glm') &&
|
|
728
|
-
this.getBaseURL()?.includes('cerebras') &&
|
|
729
|
-
formattedTools &&
|
|
730
|
-
formattedTools.length > 0) {
|
|
731
|
-
// Add a dummy user message with content to bypass Cerebras validation
|
|
732
|
-
requestBody.messages.push({ role: 'user', content: '\n' });
|
|
624
|
+
if (formattedTools && formattedTools.length > 0) {
|
|
625
|
+
requestBody.tools = JSON.parse(JSON.stringify(formattedTools));
|
|
626
|
+
requestBody.tool_choice = 'auto';
|
|
733
627
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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;
|
|
737
644
|
}
|
|
738
645
|
// Debug log request summary for Cerebras/Qwen
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
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,
|
|
744
651
|
model,
|
|
745
652
|
streamingEnabled,
|
|
746
653
|
hasTools: 'tools' in requestBody,
|
|
@@ -749,10 +656,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
749
656
|
toolsInRequest: 'tools' in requestBody ? requestBody.tools?.length : 'not included',
|
|
750
657
|
});
|
|
751
658
|
}
|
|
752
|
-
// Get OpenAI client
|
|
753
|
-
const client = await this.getClient();
|
|
754
659
|
// Get retry settings from ephemeral settings
|
|
755
|
-
const ephemeralSettings = this.providerConfig?.getEphemeralSettings?.() || {};
|
|
756
660
|
const maxRetries = ephemeralSettings['retries'] ?? 6; // Default for OpenAI
|
|
757
661
|
const initialDelayMs = ephemeralSettings['retrywait'] ?? 4000; // Default for OpenAI
|
|
758
662
|
// Get stream options from ephemeral settings (default: include usage for token tracking)
|
|
@@ -762,41 +666,43 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
762
666
|
Object.assign(requestBody, { stream_options: streamOptions });
|
|
763
667
|
}
|
|
764
668
|
// Log the exact tools being sent for debugging
|
|
765
|
-
if (
|
|
766
|
-
|
|
669
|
+
if (logger.enabled && 'tools' in requestBody) {
|
|
670
|
+
logger.debug(() => `[OpenAIProvider] Exact tools being sent to API:`, {
|
|
767
671
|
toolCount: requestBody.tools?.length,
|
|
768
672
|
toolNames: requestBody.tools?.map((t) => 'function' in t ? t.function?.name : undefined),
|
|
769
673
|
firstTool: requestBody.tools?.[0],
|
|
770
674
|
});
|
|
771
675
|
}
|
|
772
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
|
+
}
|
|
773
688
|
let response;
|
|
774
689
|
// Debug log throttle tracker status
|
|
775
|
-
|
|
690
|
+
logger.debug(() => `Retry configuration:`, {
|
|
776
691
|
hasThrottleTracker: !!this.throttleTracker,
|
|
777
692
|
throttleTrackerType: typeof this.throttleTracker,
|
|
778
693
|
maxRetries,
|
|
779
694
|
initialDelayMs,
|
|
780
695
|
});
|
|
781
696
|
const customHeaders = this.getCustomHeaders();
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
requestBody
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
lastThreeMessages: requestBody.messages.slice(-3),
|
|
792
|
-
messagesWithToolCalls: requestBody.messages.filter((m) => 'tool_calls' in m && m.tool_calls),
|
|
793
|
-
messagesWithToolRole: requestBody.messages.filter((m) => m.role === 'tool'),
|
|
794
|
-
fullRequestBody: requestBody,
|
|
795
|
-
});
|
|
796
|
-
// Check if dumponerror is enabled from either CLI flag or ephemeral setting
|
|
797
|
-
const ephemeralSettingsForDump = this.providerConfig?.getEphemeralSettings?.() || {};
|
|
798
|
-
const dumpOnError = this.globalConfig?.getDumpOnError?.() ||
|
|
799
|
-
ephemeralSettingsForDump['dumponerror'] === 'enabled';
|
|
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
|
+
}
|
|
800
706
|
try {
|
|
801
707
|
response = await retryWithBackoff(() => client.chat.completions.create(requestBody, {
|
|
802
708
|
...(abortSignal ? { signal: abortSignal } : {}),
|
|
@@ -809,59 +715,12 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
809
715
|
});
|
|
810
716
|
}
|
|
811
717
|
catch (error) {
|
|
812
|
-
// Log the error details
|
|
813
|
-
this.logger.error(() => `[OpenAIProvider] API call failed:`, {
|
|
814
|
-
error,
|
|
815
|
-
errorType: error?.constructor?.name,
|
|
816
|
-
errorMessage: error instanceof Error ? error.message : String(error),
|
|
817
|
-
errorStatus: error?.status,
|
|
818
|
-
errorHeaders: error?.headers,
|
|
819
|
-
errorBody: error?.error,
|
|
820
|
-
model,
|
|
821
|
-
baseURL: this.getBaseURL(),
|
|
822
|
-
});
|
|
823
|
-
// Dump request body on error if enabled
|
|
824
|
-
if (dumpOnError) {
|
|
825
|
-
try {
|
|
826
|
-
const fs = await import('fs');
|
|
827
|
-
const path = await import('path');
|
|
828
|
-
const os = await import('os');
|
|
829
|
-
const homeDir = os.homedir();
|
|
830
|
-
const dumpDir = path.join(homeDir, '.llxprt', 'dumps');
|
|
831
|
-
// Ensure dumps directory exists
|
|
832
|
-
if (!fs.existsSync(dumpDir)) {
|
|
833
|
-
fs.mkdirSync(dumpDir, { recursive: true });
|
|
834
|
-
}
|
|
835
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
836
|
-
const dumpFilePath = path.join(dumpDir, `request-dump-${timestamp}.json`);
|
|
837
|
-
const dumpContent = JSON.stringify({
|
|
838
|
-
timestamp: new Date().toISOString(),
|
|
839
|
-
error: {
|
|
840
|
-
message: error instanceof Error ? error.message : String(error),
|
|
841
|
-
status: error?.status,
|
|
842
|
-
type: error?.type,
|
|
843
|
-
code: error?.code,
|
|
844
|
-
param: error?.param,
|
|
845
|
-
fullError: error,
|
|
846
|
-
},
|
|
847
|
-
request: requestBody,
|
|
848
|
-
baseURL: this.getBaseURL(),
|
|
849
|
-
model,
|
|
850
|
-
notes: 'This dump contains the ACTUAL request sent to the API in OpenAI format. Messages with role:tool have tool_call_id set.',
|
|
851
|
-
}, null, 2);
|
|
852
|
-
fs.writeFileSync(dumpFilePath, dumpContent, 'utf-8');
|
|
853
|
-
this.logger.debug(() => `Request body dumped to ${dumpFilePath} (error occurred)`);
|
|
854
|
-
}
|
|
855
|
-
catch (dumpError) {
|
|
856
|
-
this.logger.debug(() => `Failed to dump request body: ${dumpError}`);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
718
|
// Special handling for Cerebras/Qwen "Tool not present" errors
|
|
860
719
|
const errorMessage = String(error);
|
|
861
720
|
if (errorMessage.includes('Tool is not present in the tools list') &&
|
|
862
721
|
(model.toLowerCase().includes('qwen') ||
|
|
863
722
|
this.getBaseURL()?.includes('cerebras'))) {
|
|
864
|
-
|
|
723
|
+
logger.error('Cerebras/Qwen API error: Tool not found despite being in request. This is a known API issue.', {
|
|
865
724
|
error,
|
|
866
725
|
model,
|
|
867
726
|
toolsProvided: formattedTools?.length || 0,
|
|
@@ -875,13 +734,23 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
875
734
|
throw enhancedError;
|
|
876
735
|
}
|
|
877
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
|
+
});
|
|
878
752
|
throw error;
|
|
879
753
|
}
|
|
880
|
-
// Log successful response start
|
|
881
|
-
this.logger.debug(() => `[OpenAIProvider] API call succeeded, processing response...`, {
|
|
882
|
-
streaming: streamingEnabled,
|
|
883
|
-
model,
|
|
884
|
-
});
|
|
885
754
|
// Check if response is streaming or not
|
|
886
755
|
if (streamingEnabled) {
|
|
887
756
|
// Process streaming response
|
|
@@ -909,7 +778,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
909
778
|
continue;
|
|
910
779
|
// Check for finish_reason to detect proper stream ending
|
|
911
780
|
if (choice.finish_reason) {
|
|
912
|
-
|
|
781
|
+
logger.debug(() => `[Streaming] Stream finished with reason: ${choice.finish_reason}`, {
|
|
913
782
|
model,
|
|
914
783
|
finishReason: choice.finish_reason,
|
|
915
784
|
hasAccumulatedText: _accumulatedText.length > 0,
|
|
@@ -918,7 +787,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
918
787
|
});
|
|
919
788
|
// If finish_reason is 'length', the response was cut off
|
|
920
789
|
if (choice.finish_reason === 'length') {
|
|
921
|
-
|
|
790
|
+
logger.debug(() => `Response truncated due to length limit for model ${model}`);
|
|
922
791
|
}
|
|
923
792
|
// Flush any buffered text when stream finishes
|
|
924
793
|
if (textBuffer.length > 0) {
|
|
@@ -940,7 +809,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
940
809
|
_accumulatedText += deltaContent;
|
|
941
810
|
// Debug log for providers that need buffering
|
|
942
811
|
if (shouldBufferText) {
|
|
943
|
-
|
|
812
|
+
logger.debug(() => `[Streaming] Chunk content for ${detectedFormat} format:`, {
|
|
944
813
|
deltaContent,
|
|
945
814
|
length: deltaContent.length,
|
|
946
815
|
hasNewline: deltaContent.includes('\n'),
|
|
@@ -951,7 +820,8 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
951
820
|
textBuffer += deltaContent;
|
|
952
821
|
// Emit buffered text when we have a complete sentence or paragraph
|
|
953
822
|
// Look for natural break points
|
|
954
|
-
if (textBuffer.
|
|
823
|
+
if (textBuffer.includes('\n') ||
|
|
824
|
+
textBuffer.endsWith('. ') ||
|
|
955
825
|
textBuffer.endsWith('! ') ||
|
|
956
826
|
textBuffer.endsWith('? ') ||
|
|
957
827
|
textBuffer.length > 100) {
|
|
@@ -1021,7 +891,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1021
891
|
if (errorMessage.includes('Tool is not present in the tools list') &&
|
|
1022
892
|
(model.toLowerCase().includes('qwen') ||
|
|
1023
893
|
this.getBaseURL()?.includes('cerebras'))) {
|
|
1024
|
-
|
|
894
|
+
logger.error('Cerebras/Qwen API error: Tool not found despite being in request. This is a known API issue.', {
|
|
1025
895
|
error,
|
|
1026
896
|
model,
|
|
1027
897
|
toolsProvided: formattedTools?.length || 0,
|
|
@@ -1032,7 +902,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1032
902
|
enhancedError.originalError = error;
|
|
1033
903
|
throw enhancedError;
|
|
1034
904
|
}
|
|
1035
|
-
|
|
905
|
+
logger.error('Error processing streaming response:', error);
|
|
1036
906
|
throw error;
|
|
1037
907
|
}
|
|
1038
908
|
}
|
|
@@ -1111,7 +981,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1111
981
|
}
|
|
1112
982
|
// Log finish reason for debugging Qwen issues
|
|
1113
983
|
if (choice.finish_reason) {
|
|
1114
|
-
|
|
984
|
+
logger.debug(() => `[Non-streaming] Response finish_reason: ${choice.finish_reason}`, {
|
|
1115
985
|
model,
|
|
1116
986
|
finishReason: choice.finish_reason,
|
|
1117
987
|
hasContent: !!choice.message?.content,
|
|
@@ -1122,7 +992,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1122
992
|
});
|
|
1123
993
|
// Warn if the response was truncated
|
|
1124
994
|
if (choice.finish_reason === 'length') {
|
|
1125
|
-
|
|
995
|
+
logger.warn(() => `Response truncated due to max_tokens limit for model ${model}. Consider increasing max_tokens.`);
|
|
1126
996
|
}
|
|
1127
997
|
}
|
|
1128
998
|
const blocks = [];
|
|
@@ -1188,103 +1058,89 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1188
1058
|
}
|
|
1189
1059
|
}
|
|
1190
1060
|
/**
|
|
1191
|
-
*
|
|
1061
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
1062
|
+
* @requirement:REQ-SP4-002
|
|
1063
|
+
* Memoization of model parameters disabled for stateless provider
|
|
1192
1064
|
*/
|
|
1193
|
-
setModelParams(
|
|
1194
|
-
|
|
1195
|
-
this.modelParams = undefined;
|
|
1196
|
-
this.setModelParamsInSettings(undefined).catch((error) => {
|
|
1197
|
-
this.logger.debug(() => `Failed to clear model params in SettingsService: ${error}`);
|
|
1198
|
-
});
|
|
1199
|
-
return;
|
|
1200
|
-
}
|
|
1201
|
-
const updated = { ...(this.modelParams ?? {}) };
|
|
1202
|
-
for (const [key, value] of Object.entries(params)) {
|
|
1203
|
-
if (value === undefined || value === null) {
|
|
1204
|
-
delete updated[key];
|
|
1205
|
-
}
|
|
1206
|
-
else {
|
|
1207
|
-
updated[key] = value;
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
this.modelParams = Object.keys(updated).length > 0 ? updated : undefined;
|
|
1211
|
-
this.setModelParamsInSettings(this.modelParams).catch((error) => {
|
|
1212
|
-
this.logger.debug(() => `Failed to persist model params to SettingsService: ${error}`);
|
|
1213
|
-
});
|
|
1065
|
+
setModelParams(_params) {
|
|
1066
|
+
throw new Error('ProviderCacheError("Attempted to memoize model parameters for openai")');
|
|
1214
1067
|
}
|
|
1068
|
+
/**
|
|
1069
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
1070
|
+
* @requirement:REQ-SP4-003
|
|
1071
|
+
* Gets model parameters from SettingsService per call (stateless)
|
|
1072
|
+
*/
|
|
1215
1073
|
getModelParams() {
|
|
1216
|
-
|
|
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
|
+
}
|
|
1217
1107
|
}
|
|
1218
1108
|
/**
|
|
1219
|
-
*
|
|
1109
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
1110
|
+
* @requirement:REQ-SP4-003
|
|
1111
|
+
* Get the tool format for this provider using normalized options
|
|
1220
1112
|
* @returns The tool format to use
|
|
1221
1113
|
*/
|
|
1222
1114
|
getToolFormat() {
|
|
1223
1115
|
const format = this.detectToolFormat();
|
|
1224
|
-
|
|
1116
|
+
const logger = new DebugLogger('llxprt:provider:openai');
|
|
1117
|
+
logger.debug(() => `getToolFormat() called, returning: ${format}`, {
|
|
1225
1118
|
provider: this.name,
|
|
1226
1119
|
model: this.getModel(),
|
|
1227
1120
|
format,
|
|
1228
1121
|
});
|
|
1229
1122
|
return format;
|
|
1230
1123
|
}
|
|
1231
|
-
/**
|
|
1232
|
-
* Set tool format override for this provider
|
|
1233
|
-
* @param format The format to use, or null to clear override
|
|
1234
|
-
*/
|
|
1235
|
-
setToolFormatOverride(format) {
|
|
1236
|
-
const settingsService = getSettingsService();
|
|
1237
|
-
if (format === null) {
|
|
1238
|
-
settingsService.setProviderSetting(this.name, 'toolFormat', 'auto');
|
|
1239
|
-
this.logger.debug(() => `Tool format override cleared for ${this.name}`);
|
|
1240
|
-
}
|
|
1241
|
-
else {
|
|
1242
|
-
settingsService.setProviderSetting(this.name, 'toolFormat', format);
|
|
1243
|
-
this.logger.debug(() => `Tool format override set to '${format}' for ${this.name}`);
|
|
1244
|
-
}
|
|
1245
|
-
// Clear cached client to ensure new format takes effect
|
|
1246
|
-
this._cachedClient = undefined;
|
|
1247
|
-
this._cachedClientKey = undefined;
|
|
1248
|
-
}
|
|
1249
1124
|
/**
|
|
1250
1125
|
* Detects the tool call format based on the model being used
|
|
1251
1126
|
* @returns The detected tool format ('openai' or 'qwen')
|
|
1252
1127
|
*/
|
|
1253
1128
|
detectToolFormat() {
|
|
1254
|
-
try {
|
|
1255
|
-
// Check for toolFormat override in provider settings
|
|
1256
|
-
const settingsService = getSettingsService();
|
|
1257
|
-
const currentSettings = settingsService['settings'];
|
|
1258
|
-
const providerSettings = currentSettings?.providers?.[this.name];
|
|
1259
|
-
const toolFormatOverride = providerSettings?.toolFormat;
|
|
1260
|
-
// If explicitly set to a specific format (not 'auto'), use it
|
|
1261
|
-
if (toolFormatOverride && toolFormatOverride !== 'auto') {
|
|
1262
|
-
this.logger.debug(() => `Using tool format override '${toolFormatOverride}' for ${this.name}`);
|
|
1263
|
-
return toolFormatOverride;
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
catch (error) {
|
|
1267
|
-
this.logger.debug(() => `Failed to detect tool format from SettingsService: ${error}`);
|
|
1268
|
-
}
|
|
1269
1129
|
// Auto-detect based on model name if set to 'auto' or not set
|
|
1270
1130
|
const modelName = (this.getModel() || this.getDefaultModel()).toLowerCase();
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
}
|
|
1276
|
-
// Check for MiniMax models (minimax, mini-max, etc.) which require Qwen handling
|
|
1277
|
-
if (modelName.includes('minimax') || modelName.includes('mini-max')) {
|
|
1278
|
-
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}`);
|
|
1279
1135
|
return 'qwen';
|
|
1280
1136
|
}
|
|
1281
1137
|
// Check for qwen models
|
|
1282
1138
|
if (modelName.includes('qwen')) {
|
|
1283
|
-
|
|
1139
|
+
logger.debug(() => `Auto-detected 'qwen' format for Qwen model: ${modelName}`);
|
|
1284
1140
|
return 'qwen';
|
|
1285
1141
|
}
|
|
1286
1142
|
// Default to 'openai' format
|
|
1287
|
-
|
|
1143
|
+
logger.debug(() => `Using default 'openai' format for model: ${modelName}`);
|
|
1288
1144
|
return 'openai';
|
|
1289
1145
|
}
|
|
1290
1146
|
/**
|
|
@@ -1298,11 +1154,14 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1298
1154
|
return response;
|
|
1299
1155
|
}
|
|
1300
1156
|
/**
|
|
1157
|
+
* @plan:PLAN-20251023-STATELESS-HARDENING.P08
|
|
1158
|
+
* @requirement:REQ-SP4-003
|
|
1301
1159
|
* Determines whether a response should be retried based on error codes
|
|
1302
1160
|
* @param error The error object from the API response
|
|
1303
1161
|
* @returns true if the request should be retried, false otherwise
|
|
1304
1162
|
*/
|
|
1305
1163
|
shouldRetryResponse(error) {
|
|
1164
|
+
const logger = new DebugLogger('llxprt:provider:openai');
|
|
1306
1165
|
// Don't retry if we're streaming chunks - just continue processing
|
|
1307
1166
|
if (error &&
|
|
1308
1167
|
typeof error === 'object' &&
|
|
@@ -1330,7 +1189,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1330
1189
|
}
|
|
1331
1190
|
}
|
|
1332
1191
|
// Log what we're seeing
|
|
1333
|
-
|
|
1192
|
+
logger.debug(() => `shouldRetryResponse checking error:`, {
|
|
1334
1193
|
hasError: !!error,
|
|
1335
1194
|
errorType: error?.constructor?.name,
|
|
1336
1195
|
status,
|
|
@@ -1338,15 +1197,11 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
1338
1197
|
errorKeys: error && typeof error === 'object' ? Object.keys(error) : [],
|
|
1339
1198
|
});
|
|
1340
1199
|
// Retry on 429 rate limit errors or 5xx server errors
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1200
|
+
const shouldRetry = Boolean(status === 429 || (status && status >= 500 && status < 600));
|
|
1201
|
+
if (shouldRetry) {
|
|
1202
|
+
logger.debug(() => `Will retry request due to status ${status}`);
|
|
1344
1203
|
}
|
|
1345
|
-
|
|
1346
|
-
this.logger.debug(() => 'Will retry request due to transient network error signature (connection-level failure).');
|
|
1347
|
-
return true;
|
|
1348
|
-
}
|
|
1349
|
-
return false;
|
|
1204
|
+
return shouldRetry;
|
|
1350
1205
|
}
|
|
1351
1206
|
}
|
|
1352
1207
|
//# sourceMappingURL=OpenAIProvider.js.map
|