@vybestack/llxprt-code-core 0.1.23 → 0.2.2-nightly.250908.7b895396
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/README.md +21 -17
- package/dist/src/adapters/IStreamAdapter.d.ts +3 -3
- package/dist/src/auth/oauth-errors.d.ts +173 -0
- package/dist/src/auth/oauth-errors.js +461 -0
- package/dist/src/auth/oauth-errors.js.map +1 -0
- package/dist/src/auth/precedence.d.ts +1 -5
- package/dist/src/auth/precedence.js +28 -48
- package/dist/src/auth/precedence.js.map +1 -1
- package/dist/src/auth/token-store.js +2 -2
- package/dist/src/auth/token-store.js.map +1 -1
- package/dist/src/auth/types.d.ts +4 -4
- package/dist/src/code_assist/codeAssist.js +19 -6
- package/dist/src/code_assist/codeAssist.js.map +1 -1
- package/dist/src/code_assist/oauth2.d.ts +7 -0
- package/dist/src/code_assist/oauth2.js +82 -32
- package/dist/src/code_assist/oauth2.js.map +1 -1
- package/dist/src/code_assist/server.js +15 -4
- package/dist/src/code_assist/server.js.map +1 -1
- package/dist/src/code_assist/setup.js +9 -0
- package/dist/src/code_assist/setup.js.map +1 -1
- package/dist/src/config/index.d.ts +7 -0
- package/dist/src/config/index.js +8 -0
- package/dist/src/config/index.js.map +1 -0
- package/dist/src/core/client.d.ts +15 -20
- package/dist/src/core/client.js +98 -124
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/compression-config.d.ts +10 -0
- package/dist/src/core/compression-config.js +17 -0
- package/dist/src/core/compression-config.js.map +1 -0
- package/dist/src/core/coreToolScheduler.js +50 -15
- package/dist/src/core/coreToolScheduler.js.map +1 -1
- package/dist/src/core/geminiChat.d.ts +68 -9
- package/dist/src/core/geminiChat.js +940 -405
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/nonInteractiveToolExecutor.js +70 -19
- package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
- package/dist/src/core/prompts.js +35 -25
- package/dist/src/core/prompts.js.map +1 -1
- package/dist/src/core/turn.d.ts +1 -0
- package/dist/src/core/turn.js +8 -6
- package/dist/src/core/turn.js.map +1 -1
- package/dist/src/ide/ide-client.d.ts +1 -1
- package/dist/src/ide/ide-client.js +12 -6
- package/dist/src/ide/ide-client.js.map +1 -1
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +5 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/prompt-config/TemplateEngine.js +17 -0
- package/dist/src/prompt-config/TemplateEngine.js.map +1 -1
- package/dist/src/prompt-config/defaults/core-defaults.js +39 -32
- package/dist/src/prompt-config/defaults/core-defaults.js.map +1 -1
- package/dist/src/prompt-config/defaults/core.md +2 -0
- package/dist/src/prompt-config/defaults/provider-defaults.js +34 -27
- package/dist/src/prompt-config/defaults/provider-defaults.js.map +1 -1
- package/dist/src/prompt-config/defaults/providers/gemini/core.md +270 -0
- package/dist/src/prompt-config/defaults/providers/gemini/models/gemini-2.5-flash/core.md +12 -0
- package/dist/src/prompt-config/defaults/providers/gemini/models/gemini-2.5-flash/gemini-2-5-flash/core.md +12 -0
- package/dist/src/prompt-config/types.d.ts +2 -0
- package/dist/src/providers/BaseProvider.d.ts +39 -13
- package/dist/src/providers/BaseProvider.js +102 -28
- package/dist/src/providers/BaseProvider.js.map +1 -1
- package/dist/src/providers/IProvider.d.ts +17 -3
- package/dist/src/providers/LoggingProviderWrapper.d.ts +10 -3
- package/dist/src/providers/LoggingProviderWrapper.js +33 -27
- package/dist/src/providers/LoggingProviderWrapper.js.map +1 -1
- package/dist/src/providers/ProviderContentGenerator.d.ts +2 -2
- package/dist/src/providers/ProviderContentGenerator.js +9 -6
- package/dist/src/providers/ProviderContentGenerator.js.map +1 -1
- package/dist/src/providers/ProviderManager.d.ts +4 -0
- package/dist/src/providers/ProviderManager.js +6 -0
- package/dist/src/providers/ProviderManager.js.map +1 -1
- package/dist/src/providers/anthropic/AnthropicProvider.d.ts +34 -21
- package/dist/src/providers/anthropic/AnthropicProvider.js +505 -492
- package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
- package/dist/src/providers/gemini/GeminiProvider.d.ts +23 -9
- package/dist/src/providers/gemini/GeminiProvider.js +344 -515
- package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
- package/dist/src/providers/openai/ConversationCache.d.ts +3 -3
- package/dist/src/providers/openai/IChatGenerateParams.d.ts +9 -4
- package/dist/src/providers/openai/OpenAIProvider.d.ts +46 -96
- package/dist/src/providers/openai/OpenAIProvider.js +580 -1392
- package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
- package/dist/src/providers/openai/buildResponsesRequest.d.ts +3 -3
- package/dist/src/providers/openai/buildResponsesRequest.js +67 -37
- package/dist/src/providers/openai/buildResponsesRequest.js.map +1 -1
- package/dist/src/providers/openai/estimateRemoteTokens.d.ts +2 -2
- package/dist/src/providers/openai/estimateRemoteTokens.js +21 -8
- package/dist/src/providers/openai/estimateRemoteTokens.js.map +1 -1
- package/dist/src/providers/openai/parseResponsesStream.d.ts +6 -2
- package/dist/src/providers/openai/parseResponsesStream.js +99 -391
- package/dist/src/providers/openai/parseResponsesStream.js.map +1 -1
- package/dist/src/providers/openai/syntheticToolResponses.d.ts +5 -5
- package/dist/src/providers/openai/syntheticToolResponses.js +102 -91
- package/dist/src/providers/openai/syntheticToolResponses.js.map +1 -1
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.d.ts +89 -0
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js +451 -0
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js.map +1 -0
- package/dist/src/providers/openai-responses/index.d.ts +1 -0
- package/dist/src/providers/openai-responses/index.js +2 -0
- package/dist/src/providers/openai-responses/index.js.map +1 -0
- package/dist/src/providers/tokenizers/OpenAITokenizer.js +3 -3
- package/dist/src/providers/tokenizers/OpenAITokenizer.js.map +1 -1
- package/dist/src/providers/types.d.ts +1 -1
- package/dist/src/services/ClipboardService.d.ts +19 -0
- package/dist/src/services/ClipboardService.js +66 -0
- package/dist/src/services/ClipboardService.js.map +1 -0
- package/dist/src/services/history/ContentConverters.d.ts +43 -0
- package/dist/src/services/history/ContentConverters.js +325 -0
- package/dist/src/services/history/ContentConverters.js.map +1 -0
- package/dist/src/{providers/IMessage.d.ts → services/history/HistoryEvents.d.ts} +16 -22
- package/dist/src/{providers/IMessage.js → services/history/HistoryEvents.js} +1 -1
- package/dist/src/services/history/HistoryEvents.js.map +1 -0
- package/dist/src/services/history/HistoryService.d.ts +220 -0
- package/dist/src/services/history/HistoryService.js +673 -0
- package/dist/src/services/history/HistoryService.js.map +1 -0
- package/dist/src/services/history/IContent.d.ts +183 -0
- package/dist/src/services/history/IContent.js +104 -0
- package/dist/src/services/history/IContent.js.map +1 -0
- package/dist/src/services/index.d.ts +1 -0
- package/dist/src/services/index.js +1 -0
- package/dist/src/services/index.js.map +1 -1
- package/dist/src/settings/SettingsService.js.map +1 -1
- package/dist/src/telemetry/types.d.ts +16 -4
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/tools/IToolFormatter.d.ts +2 -2
- package/dist/src/tools/ToolFormatter.d.ts +42 -4
- package/dist/src/tools/ToolFormatter.js +151 -64
- package/dist/src/tools/ToolFormatter.js.map +1 -1
- package/dist/src/tools/doubleEscapeUtils.d.ts +57 -0
- package/dist/src/tools/doubleEscapeUtils.js +241 -0
- package/dist/src/tools/doubleEscapeUtils.js.map +1 -0
- package/dist/src/tools/read-file.d.ts +6 -1
- package/dist/src/tools/read-file.js +25 -11
- package/dist/src/tools/read-file.js.map +1 -1
- package/dist/src/tools/todo-schemas.d.ts +4 -4
- package/dist/src/tools/tool-registry.d.ts +8 -1
- package/dist/src/tools/tool-registry.js +79 -23
- package/dist/src/tools/tool-registry.js.map +1 -1
- package/dist/src/tools/tools.js +13 -0
- package/dist/src/tools/tools.js.map +1 -1
- package/dist/src/tools/write-file.d.ts +6 -1
- package/dist/src/tools/write-file.js +48 -26
- package/dist/src/tools/write-file.js.map +1 -1
- package/dist/src/types/modelParams.d.ts +12 -0
- package/dist/src/utils/bfsFileSearch.js +2 -6
- package/dist/src/utils/bfsFileSearch.js.map +1 -1
- package/dist/src/utils/schemaValidator.js +16 -1
- package/dist/src/utils/schemaValidator.js.map +1 -1
- package/package.json +8 -7
- package/dist/src/providers/IMessage.js.map +0 -1
- package/dist/src/providers/adapters/GeminiCompatibleWrapper.d.ts +0 -69
- package/dist/src/providers/adapters/GeminiCompatibleWrapper.js +0 -577
- package/dist/src/providers/adapters/GeminiCompatibleWrapper.js.map +0 -1
@@ -1,29 +1,16 @@
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
2
2
|
import { DebugLogger } from '../../debug/index.js';
|
3
|
-
import { retryWithBackoff } from '../../utils/retry.js';
|
4
3
|
import { ToolFormatter } from '../../tools/ToolFormatter.js';
|
5
4
|
import { BaseProvider } from '../BaseProvider.js';
|
6
5
|
import { getSettingsService } from '../../settings/settingsServiceInstance.js';
|
7
|
-
import {
|
6
|
+
import { processToolParameters, logDoubleEscapingInChunk, } from '../../tools/doubleEscapeUtils.js';
|
7
|
+
import { getCoreSystemPromptAsync } from '../../core/prompts.js';
|
8
8
|
export class AnthropicProvider extends BaseProvider {
|
9
9
|
logger;
|
10
10
|
anthropic;
|
11
11
|
toolFormatter;
|
12
12
|
toolFormat = 'anthropic';
|
13
|
-
|
14
|
-
_config;
|
15
|
-
currentModel = 'claude-sonnet-4-20250514'; // Default model
|
16
|
-
modelParams;
|
17
|
-
// Model cache for latest resolution
|
18
|
-
modelCache = null;
|
19
|
-
modelCacheTTL = 5 * 60 * 1000; // 5 minutes
|
20
|
-
// Retry configuration
|
21
|
-
retryableErrorMessages = [
|
22
|
-
'overloaded',
|
23
|
-
'rate_limit',
|
24
|
-
'server_error',
|
25
|
-
'service_unavailable',
|
26
|
-
];
|
13
|
+
_cachedAuthKey; // Track cached auth key for client recreation
|
27
14
|
// Model patterns for max output tokens
|
28
15
|
modelTokenPatterns = [
|
29
16
|
{ pattern: /claude-.*opus-4/i, tokens: 32000 },
|
@@ -41,21 +28,16 @@ export class AnthropicProvider extends BaseProvider {
|
|
41
28
|
name: 'anthropic',
|
42
29
|
apiKey,
|
43
30
|
baseURL,
|
44
|
-
cliKey: !apiKey || apiKey === '' ? undefined : apiKey,
|
45
31
|
envKeyNames: ['ANTHROPIC_API_KEY'],
|
46
32
|
isOAuthEnabled: !!oauthManager,
|
47
33
|
oauthProvider: oauthManager ? 'anthropic' : undefined,
|
48
34
|
oauthManager,
|
49
35
|
};
|
50
|
-
super(baseConfig);
|
36
|
+
super(baseConfig, config);
|
51
37
|
this.logger = new DebugLogger('llxprt:anthropic:provider');
|
52
|
-
this.baseURL = baseURL;
|
53
|
-
this._config = config;
|
54
|
-
// Config reserved for future provider customization
|
55
|
-
void this._config;
|
56
38
|
this.anthropic = new Anthropic({
|
57
39
|
apiKey: apiKey || '', // Empty string if OAuth will be used
|
58
|
-
baseURL,
|
40
|
+
baseURL: config?.baseUrl || baseURL,
|
59
41
|
dangerouslyAllowBrowser: true,
|
60
42
|
});
|
61
43
|
this.toolFormatter = new ToolFormatter();
|
@@ -69,37 +51,44 @@ export class AnthropicProvider extends BaseProvider {
|
|
69
51
|
return true;
|
70
52
|
}
|
71
53
|
/**
|
54
|
+
* @plan:PLAN-20250823-AUTHFIXES.P15
|
55
|
+
* @requirement:REQ-004
|
72
56
|
* Update the Anthropic client with resolved authentication if needed
|
73
57
|
*/
|
74
58
|
async updateClientWithResolvedAuth() {
|
75
59
|
const resolvedToken = await this.getAuthToken();
|
76
60
|
if (!resolvedToken) {
|
77
|
-
throw new Error('No authentication available for Anthropic API calls');
|
78
|
-
}
|
79
|
-
// Check if this is an OAuth token (starts with sk-ant-oat)
|
80
|
-
const isOAuthToken = resolvedToken.startsWith('sk-ant-oat');
|
81
|
-
if (isOAuthToken) {
|
82
|
-
// For OAuth tokens, use authToken field which sends Bearer token
|
83
|
-
// Don't pass apiKey at all - just authToken
|
84
|
-
const oauthConfig = {
|
85
|
-
authToken: resolvedToken, // Use authToken for OAuth Bearer tokens
|
86
|
-
baseURL: this.baseURL,
|
87
|
-
dangerouslyAllowBrowser: true,
|
88
|
-
defaultHeaders: {
|
89
|
-
'anthropic-beta': 'oauth-2025-04-20', // Still need the beta header
|
90
|
-
},
|
91
|
-
};
|
92
|
-
this.anthropic = new Anthropic(oauthConfig);
|
61
|
+
throw new Error('No authentication available for Anthropic API calls. Use /auth anthropic to re-authenticate or /auth anthropic logout to clear any expired session.');
|
93
62
|
}
|
94
|
-
|
95
|
-
|
96
|
-
if
|
63
|
+
// Only recreate client if auth changed
|
64
|
+
if (this._cachedAuthKey !== resolvedToken) {
|
65
|
+
// Check if this is an OAuth token (starts with sk-ant-oat)
|
66
|
+
const isOAuthToken = resolvedToken.startsWith('sk-ant-oat');
|
67
|
+
// Use the unified getBaseURL() method from BaseProvider
|
68
|
+
const baseURL = this.getBaseURL();
|
69
|
+
if (isOAuthToken) {
|
70
|
+
// For OAuth tokens, use authToken field which sends Bearer token
|
71
|
+
// Don't pass apiKey at all - just authToken
|
72
|
+
const oauthConfig = {
|
73
|
+
authToken: resolvedToken, // Use authToken for OAuth Bearer tokens
|
74
|
+
baseURL,
|
75
|
+
dangerouslyAllowBrowser: true,
|
76
|
+
defaultHeaders: {
|
77
|
+
'anthropic-beta': 'oauth-2025-04-20', // Still need the beta header
|
78
|
+
},
|
79
|
+
};
|
80
|
+
this.anthropic = new Anthropic(oauthConfig);
|
81
|
+
}
|
82
|
+
else {
|
83
|
+
// Regular API key auth
|
97
84
|
this.anthropic = new Anthropic({
|
98
85
|
apiKey: resolvedToken,
|
99
|
-
baseURL
|
86
|
+
baseURL,
|
100
87
|
dangerouslyAllowBrowser: true,
|
101
88
|
});
|
102
89
|
}
|
90
|
+
// Track the key to avoid unnecessary client recreation
|
91
|
+
this._cachedAuthKey = resolvedToken;
|
103
92
|
}
|
104
93
|
}
|
105
94
|
async getModels() {
|
@@ -172,331 +161,26 @@ export class AnthropicProvider extends BaseProvider {
|
|
172
161
|
return []; // Return empty array on error
|
173
162
|
}
|
174
163
|
}
|
175
|
-
async *generateChatCompletion(messages, tools, _toolFormat) {
|
176
|
-
const authToken = await this.getAuthToken();
|
177
|
-
if (!authToken) {
|
178
|
-
throw new Error('Authentication required to generate Anthropic chat completions');
|
179
|
-
}
|
180
|
-
// Get streaming setting from ephemeral settings (default: enabled)
|
181
|
-
const streamingSetting = this._config?.getEphemeralSettings?.()?.['streaming'];
|
182
|
-
const streamingEnabled = streamingSetting !== 'disabled';
|
183
|
-
// Update Anthropic client with resolved authentication if needed
|
184
|
-
await this.updateClientWithResolvedAuth();
|
185
|
-
const apiCall = async () => {
|
186
|
-
// Resolve model if it uses -latest alias
|
187
|
-
const resolvedModel = await this.resolveLatestModel(this.currentModel);
|
188
|
-
// Always validate and fix message history to prevent tool_use/tool_result mismatches
|
189
|
-
// This is necessary for both cancelled tools and retries
|
190
|
-
const validatedMessages = this.validateAndFixMessages(messages);
|
191
|
-
// Use the resolved model for the API call
|
192
|
-
const modelForApi = resolvedModel;
|
193
|
-
// Check if we're in OAuth mode early
|
194
|
-
const authToken = await this.getAuthToken();
|
195
|
-
const isOAuth = authToken && authToken.startsWith('sk-ant-oat');
|
196
|
-
// Extract system message if present and handle tool responses
|
197
|
-
let systemMessage;
|
198
|
-
let llxprtPrompts; // Store llxprt prompts separately
|
199
|
-
const anthropicMessages = [];
|
200
|
-
for (const msg of validatedMessages) {
|
201
|
-
if (msg.role === 'system') {
|
202
|
-
if (isOAuth) {
|
203
|
-
// In OAuth mode, save system content for injection as user message
|
204
|
-
llxprtPrompts = msg.content;
|
205
|
-
}
|
206
|
-
else {
|
207
|
-
// In normal mode, use as system message
|
208
|
-
systemMessage = msg.content;
|
209
|
-
}
|
210
|
-
}
|
211
|
-
else if (msg.role === 'tool') {
|
212
|
-
// Anthropic expects tool responses as user messages with tool_result content
|
213
|
-
anthropicMessages.push({
|
214
|
-
role: 'user',
|
215
|
-
content: [
|
216
|
-
{
|
217
|
-
type: 'tool_result',
|
218
|
-
tool_use_id: msg.tool_call_id || 'unknown',
|
219
|
-
content: msg.content,
|
220
|
-
},
|
221
|
-
],
|
222
|
-
});
|
223
|
-
}
|
224
|
-
else if (msg.role === 'assistant' && msg.tool_calls) {
|
225
|
-
// Handle assistant messages with tool calls
|
226
|
-
const content = [];
|
227
|
-
if (msg.content) {
|
228
|
-
content.push({ type: 'text', text: msg.content });
|
229
|
-
}
|
230
|
-
for (const toolCall of msg.tool_calls) {
|
231
|
-
content.push({
|
232
|
-
type: 'tool_use',
|
233
|
-
id: toolCall.id,
|
234
|
-
name: toolCall.function.name,
|
235
|
-
input: toolCall.function.arguments
|
236
|
-
? JSON.parse(toolCall.function.arguments)
|
237
|
-
: {},
|
238
|
-
});
|
239
|
-
}
|
240
|
-
anthropicMessages.push({
|
241
|
-
role: 'assistant',
|
242
|
-
content,
|
243
|
-
});
|
244
|
-
}
|
245
|
-
else {
|
246
|
-
// Regular user/assistant messages
|
247
|
-
anthropicMessages.push({
|
248
|
-
role: msg.role,
|
249
|
-
content: msg.content,
|
250
|
-
});
|
251
|
-
}
|
252
|
-
}
|
253
|
-
// In OAuth mode, inject llxprt prompts as conversation content
|
254
|
-
// ONLY for the very first message in a new conversation
|
255
|
-
if (isOAuth && llxprtPrompts && anthropicMessages.length === 0) {
|
256
|
-
// This is the very first message - inject the context
|
257
|
-
const contextMessage = `Important context for using llxprt tools:
|
258
|
-
|
259
|
-
Tool Parameter Reference:
|
260
|
-
- read_file uses parameter 'absolute_path' (not 'file_path')
|
261
|
-
- write_file uses parameter 'file_path' (not 'path')
|
262
|
-
- list_directory uses parameter 'path'
|
263
|
-
- replace uses 'file_path', 'old_string', 'new_string'
|
264
|
-
- search_file_content (grep) expects regex patterns, not literal text
|
265
|
-
- todo_write requires 'todos' array with {id, content, status, priority}
|
266
|
-
- All file paths must be absolute (starting with /)
|
267
|
-
|
268
|
-
${llxprtPrompts}`;
|
269
|
-
// Inject at the beginning of the conversation
|
270
|
-
anthropicMessages.unshift({
|
271
|
-
role: 'user',
|
272
|
-
content: contextMessage,
|
273
|
-
}, {
|
274
|
-
role: 'assistant',
|
275
|
-
content: "I understand the llxprt tool parameters and context. I'll use the correct parameter names for each tool. Ready to help with your tasks.",
|
276
|
-
});
|
277
|
-
}
|
278
|
-
// For ongoing conversations, the context was already injected in the first message
|
279
|
-
// so we don't need to inject it again
|
280
|
-
// Convert ITool[] to Anthropic's tool format if tools are provided
|
281
|
-
const anthropicTools = tools
|
282
|
-
? this.toolFormatter.toProviderFormat(tools, 'anthropic')
|
283
|
-
: undefined;
|
284
|
-
// Create the request options with proper typing
|
285
|
-
const createOptions = {
|
286
|
-
model: modelForApi,
|
287
|
-
messages: anthropicMessages,
|
288
|
-
max_tokens: this.getMaxTokensForModel(resolvedModel),
|
289
|
-
...this.modelParams, // Apply model params first
|
290
|
-
stream: streamingEnabled, // Use ephemeral streaming setting
|
291
|
-
};
|
292
|
-
// Set system message based on auth mode
|
293
|
-
if (isOAuth) {
|
294
|
-
// OAuth mode: Use Claude Code system prompt (required for Max/Pro)
|
295
|
-
createOptions.system =
|
296
|
-
"You are Claude Code, Anthropic's official CLI for Claude.";
|
297
|
-
// llxprt prompts were already injected as conversation content above
|
298
|
-
}
|
299
|
-
else if (systemMessage) {
|
300
|
-
// Normal mode: Use full llxprt system prompt
|
301
|
-
createOptions.system = systemMessage;
|
302
|
-
}
|
303
|
-
if (anthropicTools) {
|
304
|
-
createOptions.tools = anthropicTools;
|
305
|
-
}
|
306
|
-
if (streamingEnabled) {
|
307
|
-
return this.anthropic.messages.create(createOptions);
|
308
|
-
}
|
309
|
-
else {
|
310
|
-
return this.anthropic.messages.create(createOptions);
|
311
|
-
}
|
312
|
-
};
|
313
|
-
try {
|
314
|
-
const response = await retryWithBackoff(apiCall, {
|
315
|
-
shouldRetry: (error) => this.isRetryableError(error),
|
316
|
-
});
|
317
|
-
if (streamingEnabled) {
|
318
|
-
// Handle streaming response
|
319
|
-
const stream = response;
|
320
|
-
let currentUsage;
|
321
|
-
// Track current tool call being streamed
|
322
|
-
let currentToolCall;
|
323
|
-
// Process the stream
|
324
|
-
for await (const chunk of stream) {
|
325
|
-
this.logger.debug(() => `Received chunk type: ${chunk.type}${chunk.type === 'message_start'
|
326
|
-
? ` - ${JSON.stringify(chunk, null, 2)}`
|
327
|
-
: ''}`);
|
328
|
-
if (chunk.type === 'message_start') {
|
329
|
-
// Initial usage info
|
330
|
-
this.logger.debug(() => `message_start chunk: ${JSON.stringify(chunk, null, 2)}`);
|
331
|
-
if (chunk.message?.usage) {
|
332
|
-
const usage = chunk.message.usage;
|
333
|
-
// Don't require both fields - Anthropic might send them separately
|
334
|
-
currentUsage = {
|
335
|
-
input_tokens: usage.input_tokens ?? 0,
|
336
|
-
output_tokens: usage.output_tokens ?? 0,
|
337
|
-
};
|
338
|
-
this.logger.debug(() => `Set currentUsage from message_start: ${JSON.stringify(currentUsage)}`);
|
339
|
-
yield {
|
340
|
-
role: 'assistant',
|
341
|
-
content: '',
|
342
|
-
usage: {
|
343
|
-
prompt_tokens: currentUsage.input_tokens,
|
344
|
-
completion_tokens: currentUsage.output_tokens,
|
345
|
-
total_tokens: currentUsage.input_tokens + currentUsage.output_tokens,
|
346
|
-
},
|
347
|
-
};
|
348
|
-
}
|
349
|
-
}
|
350
|
-
else if (chunk.type === 'content_block_start') {
|
351
|
-
// Handle tool use blocks
|
352
|
-
if (chunk.content_block.type === 'tool_use') {
|
353
|
-
currentToolCall = {
|
354
|
-
id: chunk.content_block.id,
|
355
|
-
name: chunk.content_block.name,
|
356
|
-
input: '',
|
357
|
-
};
|
358
|
-
}
|
359
|
-
}
|
360
|
-
else if (chunk.type === 'content_block_delta') {
|
361
|
-
// Yield content chunks
|
362
|
-
if (chunk.delta.type === 'text_delta') {
|
363
|
-
yield {
|
364
|
-
role: 'assistant',
|
365
|
-
content: chunk.delta.text,
|
366
|
-
};
|
367
|
-
}
|
368
|
-
else if (chunk.delta.type === 'input_json_delta' &&
|
369
|
-
currentToolCall) {
|
370
|
-
// Handle input deltas for tool calls
|
371
|
-
currentToolCall.input += chunk.delta.partial_json;
|
372
|
-
}
|
373
|
-
}
|
374
|
-
else if (chunk.type === 'content_block_stop') {
|
375
|
-
// Complete the tool call
|
376
|
-
if (currentToolCall) {
|
377
|
-
const toolCallResult = this.toolFormatter.fromProviderFormat({
|
378
|
-
id: currentToolCall.id,
|
379
|
-
type: 'tool_use',
|
380
|
-
name: currentToolCall.name,
|
381
|
-
input: currentToolCall.input
|
382
|
-
? JSON.parse(currentToolCall.input)
|
383
|
-
: undefined,
|
384
|
-
}, 'anthropic');
|
385
|
-
yield {
|
386
|
-
role: 'assistant',
|
387
|
-
content: '',
|
388
|
-
tool_calls: toolCallResult,
|
389
|
-
};
|
390
|
-
currentToolCall = undefined;
|
391
|
-
}
|
392
|
-
}
|
393
|
-
else if (chunk.type === 'message_delta') {
|
394
|
-
// Update usage if provided
|
395
|
-
if (chunk.usage) {
|
396
|
-
this.logger.debug(() => `message_delta usage: ${JSON.stringify(chunk.usage, null, 2)}`);
|
397
|
-
}
|
398
|
-
if (chunk.usage) {
|
399
|
-
// Anthropic may send partial usage data - merge with existing
|
400
|
-
currentUsage = {
|
401
|
-
input_tokens: chunk.usage.input_tokens ?? currentUsage?.input_tokens ?? 0,
|
402
|
-
output_tokens: chunk.usage.output_tokens ?? currentUsage?.output_tokens ?? 0,
|
403
|
-
};
|
404
|
-
this.logger.debug(() => `Updated currentUsage from message_delta: ${JSON.stringify(currentUsage)}`);
|
405
|
-
yield {
|
406
|
-
role: 'assistant',
|
407
|
-
content: '',
|
408
|
-
usage: {
|
409
|
-
prompt_tokens: currentUsage.input_tokens,
|
410
|
-
completion_tokens: currentUsage.output_tokens,
|
411
|
-
total_tokens: currentUsage.input_tokens + currentUsage.output_tokens,
|
412
|
-
},
|
413
|
-
};
|
414
|
-
}
|
415
|
-
}
|
416
|
-
else if (chunk.type === 'message_stop') {
|
417
|
-
// Final usage info
|
418
|
-
if (currentUsage) {
|
419
|
-
this.logger.debug(() => `Yielding final usage: ${JSON.stringify(currentUsage)}`);
|
420
|
-
yield {
|
421
|
-
role: 'assistant',
|
422
|
-
content: '',
|
423
|
-
usage: {
|
424
|
-
prompt_tokens: currentUsage.input_tokens,
|
425
|
-
completion_tokens: currentUsage.output_tokens,
|
426
|
-
total_tokens: currentUsage.input_tokens + currentUsage.output_tokens,
|
427
|
-
},
|
428
|
-
};
|
429
|
-
}
|
430
|
-
else {
|
431
|
-
this.logger.debug(() => 'No currentUsage data at message_stop');
|
432
|
-
}
|
433
|
-
}
|
434
|
-
}
|
435
|
-
}
|
436
|
-
else {
|
437
|
-
// Handle non-streaming response
|
438
|
-
const message = response;
|
439
|
-
let fullContent = '';
|
440
|
-
const toolCalls = [];
|
441
|
-
// Process content blocks
|
442
|
-
for (const content of message.content) {
|
443
|
-
if (content.type === 'text') {
|
444
|
-
fullContent += content.text;
|
445
|
-
}
|
446
|
-
else if (content.type === 'tool_use') {
|
447
|
-
toolCalls.push({
|
448
|
-
id: content.id,
|
449
|
-
type: 'function',
|
450
|
-
function: {
|
451
|
-
name: content.name,
|
452
|
-
arguments: JSON.stringify(content.input),
|
453
|
-
},
|
454
|
-
});
|
455
|
-
}
|
456
|
-
}
|
457
|
-
// Build response message
|
458
|
-
const responseMessage = {
|
459
|
-
role: ContentGeneratorRole.ASSISTANT,
|
460
|
-
content: fullContent,
|
461
|
-
};
|
462
|
-
if (toolCalls.length > 0) {
|
463
|
-
responseMessage.tool_calls = toolCalls;
|
464
|
-
}
|
465
|
-
if (message.usage) {
|
466
|
-
responseMessage.usage = {
|
467
|
-
prompt_tokens: message.usage.input_tokens,
|
468
|
-
completion_tokens: message.usage.output_tokens,
|
469
|
-
total_tokens: message.usage.input_tokens + message.usage.output_tokens,
|
470
|
-
};
|
471
|
-
}
|
472
|
-
yield responseMessage;
|
473
|
-
}
|
474
|
-
}
|
475
|
-
catch (error) {
|
476
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
477
|
-
throw new Error(`Anthropic API error: ${errorMessage}`);
|
478
|
-
}
|
479
|
-
}
|
480
164
|
setApiKey(apiKey) {
|
481
165
|
// Call base provider implementation
|
482
|
-
super.setApiKey
|
166
|
+
super.setApiKey(apiKey);
|
483
167
|
// Create a new Anthropic client with the updated API key
|
168
|
+
const resolvedBaseURL = this.providerConfig?.baseUrl || this.baseProviderConfig.baseURL;
|
484
169
|
this.anthropic = new Anthropic({
|
485
170
|
apiKey,
|
486
|
-
baseURL:
|
171
|
+
baseURL: resolvedBaseURL,
|
487
172
|
dangerouslyAllowBrowser: true,
|
488
173
|
});
|
489
174
|
}
|
490
175
|
setBaseUrl(baseUrl) {
|
491
|
-
//
|
492
|
-
this.baseURL = baseUrl && baseUrl.trim() !== '' ? baseUrl : undefined;
|
493
|
-
// Call base provider implementation
|
176
|
+
// Call base provider implementation which stores in ephemeral settings
|
494
177
|
super.setBaseUrl?.(baseUrl);
|
495
178
|
// Create a new Anthropic client with the updated (or cleared) base URL
|
496
179
|
// Will be updated with actual token in updateClientWithResolvedAuth
|
180
|
+
const resolvedBaseURL = this.getBaseURL();
|
497
181
|
this.anthropic = new Anthropic({
|
498
182
|
apiKey: '', // Empty string, will be replaced when auth is resolved
|
499
|
-
baseURL:
|
183
|
+
baseURL: resolvedBaseURL,
|
500
184
|
dangerouslyAllowBrowser: true,
|
501
185
|
});
|
502
186
|
}
|
@@ -509,8 +193,7 @@ ${llxprtPrompts}`;
|
|
509
193
|
catch (error) {
|
510
194
|
this.logger.debug(() => `Failed to persist model to SettingsService: ${error}`);
|
511
195
|
}
|
512
|
-
//
|
513
|
-
this.currentModel = modelId;
|
196
|
+
// No local caching - always look up from SettingsService
|
514
197
|
}
|
515
198
|
getCurrentModel() {
|
516
199
|
// Try to get from SettingsService first (source of truth)
|
@@ -524,11 +207,11 @@ ${llxprtPrompts}`;
|
|
524
207
|
catch (error) {
|
525
208
|
this.logger.debug(() => `Failed to get model from SettingsService: ${error}`);
|
526
209
|
}
|
527
|
-
//
|
528
|
-
return this.
|
210
|
+
// Always return from getDefaultModel, no caching
|
211
|
+
return this.getDefaultModel();
|
529
212
|
}
|
530
213
|
getDefaultModel() {
|
531
|
-
// Return
|
214
|
+
// Return hardcoded default - do NOT call getModel() to avoid circular dependency
|
532
215
|
return 'claude-sonnet-4-20250514';
|
533
216
|
}
|
534
217
|
/**
|
@@ -550,54 +233,6 @@ ${llxprtPrompts}`;
|
|
550
233
|
return 'claude-sonnet-4-latest';
|
551
234
|
}
|
552
235
|
}
|
553
|
-
/**
|
554
|
-
* Resolves a model ID that may contain "-latest" to the actual model ID.
|
555
|
-
* Caches the result to avoid frequent API calls.
|
556
|
-
*/
|
557
|
-
async resolveLatestModel(modelId) {
|
558
|
-
// If it's not a latest alias, return as-is
|
559
|
-
if (!modelId.endsWith('-latest')) {
|
560
|
-
return modelId;
|
561
|
-
}
|
562
|
-
// Check cache
|
563
|
-
const now = Date.now();
|
564
|
-
if (this.modelCache &&
|
565
|
-
now - this.modelCache.timestamp < this.modelCacheTTL) {
|
566
|
-
// Find the corresponding model from cache
|
567
|
-
const model = this.modelCache.models.find((m) => m.id === modelId);
|
568
|
-
if (model) {
|
569
|
-
// The latest aliases are synthetic, find the real model
|
570
|
-
const tier = modelId.includes('opus') ? 'opus' : 'sonnet';
|
571
|
-
const realModel = this.modelCache.models
|
572
|
-
.filter((m) => m.id.startsWith(`claude-${tier}-4-`) && !m.id.endsWith('-latest'))
|
573
|
-
.sort((a, b) => b.id.localeCompare(a.id))[0];
|
574
|
-
return realModel ? realModel.id : modelId;
|
575
|
-
}
|
576
|
-
}
|
577
|
-
try {
|
578
|
-
// Ensure client has proper auth before calling getModels
|
579
|
-
await this.updateClientWithResolvedAuth();
|
580
|
-
// Fetch fresh models
|
581
|
-
const models = await this.getModels();
|
582
|
-
this.modelCache = { models, timestamp: now };
|
583
|
-
// Find the real model for this latest alias
|
584
|
-
const tier = modelId.includes('opus') ? 'opus' : 'sonnet';
|
585
|
-
const realModel = models
|
586
|
-
.filter((m) => m.id.startsWith(`claude-${tier}-4-`) && !m.id.endsWith('-latest'))
|
587
|
-
.sort((a, b) => b.id.localeCompare(a.id))[0];
|
588
|
-
return realModel ? realModel.id : modelId;
|
589
|
-
}
|
590
|
-
catch (_error) {
|
591
|
-
// If we can't fetch models, just use simple fallback like Claude Code does
|
592
|
-
this.logger.debug(() => 'Failed to fetch models for latest resolution, using fallback');
|
593
|
-
if (modelId.includes('opus')) {
|
594
|
-
return 'opus';
|
595
|
-
}
|
596
|
-
else {
|
597
|
-
return 'sonnet'; // Default to sonnet like Claude Code
|
598
|
-
}
|
599
|
-
}
|
600
|
-
}
|
601
236
|
getMaxTokensForModel(modelId) {
|
602
237
|
// Handle latest aliases explicitly
|
603
238
|
if (modelId === 'claude-opus-4-latest' ||
|
@@ -632,88 +267,6 @@ ${llxprtPrompts}`;
|
|
632
267
|
// Default for Claude 3.x models
|
633
268
|
return 200000;
|
634
269
|
}
|
635
|
-
isRetryableError(error) {
|
636
|
-
if (!(error instanceof Error))
|
637
|
-
return false;
|
638
|
-
const errorMessage = error.message.toLowerCase();
|
639
|
-
if (error.message.includes('rate_limit_error'))
|
640
|
-
return true;
|
641
|
-
// Check for Anthropic-specific error patterns
|
642
|
-
if (error.message.includes('Anthropic API error:')) {
|
643
|
-
// Extract the actual error content
|
644
|
-
const match = error.message.match(/{"type":"error","error":({.*})}/);
|
645
|
-
if (match) {
|
646
|
-
try {
|
647
|
-
const errorData = JSON.parse(match[1]);
|
648
|
-
const errorType = errorData.type?.toLowerCase() || '';
|
649
|
-
const errorMsg = errorData.message?.toLowerCase() || '';
|
650
|
-
return this.retryableErrorMessages.some((retryable) => errorType.includes(retryable) || errorMsg.includes(retryable));
|
651
|
-
}
|
652
|
-
catch {
|
653
|
-
// If parsing fails, fall back to string matching
|
654
|
-
}
|
655
|
-
}
|
656
|
-
}
|
657
|
-
// Direct error message checking
|
658
|
-
return this.retryableErrorMessages.some((msg) => errorMessage.includes(msg));
|
659
|
-
}
|
660
|
-
/**
|
661
|
-
* Validates and potentially fixes the message history to ensure proper tool_use/tool_result pairing.
|
662
|
-
* This prevents the "tool_use ids were found without tool_result blocks" error after a failed request.
|
663
|
-
*/
|
664
|
-
validateAndFixMessages(messages) {
|
665
|
-
const fixedMessages = [];
|
666
|
-
let pendingToolCalls = [];
|
667
|
-
for (let i = 0; i < messages.length; i++) {
|
668
|
-
const msg = messages[i];
|
669
|
-
if (msg.role === 'assistant' && msg.tool_calls) {
|
670
|
-
// Track tool calls from assistant
|
671
|
-
fixedMessages.push(msg);
|
672
|
-
pendingToolCalls = msg.tool_calls.map((tc) => ({
|
673
|
-
id: tc.id,
|
674
|
-
name: tc.function.name,
|
675
|
-
}));
|
676
|
-
}
|
677
|
-
else if (msg.role === 'tool' && pendingToolCalls.length > 0) {
|
678
|
-
// Match tool results with pending tool calls
|
679
|
-
fixedMessages.push(msg);
|
680
|
-
// Remove the matched tool call
|
681
|
-
pendingToolCalls = pendingToolCalls.filter((tc) => tc.id !== msg.tool_call_id);
|
682
|
-
}
|
683
|
-
else if (msg.role === 'assistant' ||
|
684
|
-
msg.role === 'user' ||
|
685
|
-
msg.role === 'system') {
|
686
|
-
// If we have pending tool calls and encounter a non-tool message,
|
687
|
-
// we need to add dummy tool results to maintain consistency
|
688
|
-
if (pendingToolCalls.length > 0 && msg.role !== 'system') {
|
689
|
-
// Add dummy tool results for unmatched tool calls
|
690
|
-
for (const toolCall of pendingToolCalls) {
|
691
|
-
fixedMessages.push({
|
692
|
-
role: 'tool',
|
693
|
-
tool_call_id: toolCall.id,
|
694
|
-
content: 'Error: Tool execution was interrupted. Please retry.',
|
695
|
-
});
|
696
|
-
}
|
697
|
-
pendingToolCalls = [];
|
698
|
-
}
|
699
|
-
fixedMessages.push(msg);
|
700
|
-
}
|
701
|
-
else {
|
702
|
-
fixedMessages.push(msg);
|
703
|
-
}
|
704
|
-
}
|
705
|
-
// Handle any remaining pending tool calls at the end
|
706
|
-
if (pendingToolCalls.length > 0) {
|
707
|
-
for (const toolCall of pendingToolCalls) {
|
708
|
-
fixedMessages.push({
|
709
|
-
role: 'tool',
|
710
|
-
tool_call_id: toolCall.id,
|
711
|
-
content: 'Error: Tool execution was interrupted. Please retry.',
|
712
|
-
});
|
713
|
-
}
|
714
|
-
}
|
715
|
-
return fixedMessages;
|
716
|
-
}
|
717
270
|
/**
|
718
271
|
* Anthropic always requires payment (API key or OAuth)
|
719
272
|
*/
|
@@ -737,11 +290,31 @@ ${llxprtPrompts}`;
|
|
737
290
|
* @param params Parameters to merge with existing, or undefined to clear all
|
738
291
|
*/
|
739
292
|
setModelParams(params) {
|
293
|
+
const settingsService = getSettingsService();
|
740
294
|
if (params === undefined) {
|
741
|
-
|
295
|
+
// Clear all model params
|
296
|
+
settingsService.setProviderSetting(this.name, 'temperature', undefined);
|
297
|
+
settingsService.setProviderSetting(this.name, 'max_tokens', undefined);
|
298
|
+
settingsService.setProviderSetting(this.name, 'top_p', undefined);
|
299
|
+
settingsService.setProviderSetting(this.name, 'top_k', undefined);
|
742
300
|
}
|
743
301
|
else {
|
744
|
-
|
302
|
+
// Set each param individually
|
303
|
+
if ('temperature' in params) {
|
304
|
+
settingsService.setProviderSetting(this.name, 'temperature', params.temperature);
|
305
|
+
}
|
306
|
+
if ('max_tokens' in params) {
|
307
|
+
settingsService.setProviderSetting(this.name, 'max_tokens', params.max_tokens);
|
308
|
+
}
|
309
|
+
if ('top_p' in params) {
|
310
|
+
settingsService.setProviderSetting(this.name, 'top_p', params.top_p);
|
311
|
+
}
|
312
|
+
if ('top_k' in params) {
|
313
|
+
settingsService.setProviderSetting(this.name, 'top_k', params.top_k);
|
314
|
+
}
|
315
|
+
if ('stop_sequences' in params) {
|
316
|
+
settingsService.setProviderSetting(this.name, 'stop_sequences', params.stop_sequences);
|
317
|
+
}
|
745
318
|
}
|
746
319
|
}
|
747
320
|
/**
|
@@ -749,7 +322,31 @@ ${llxprtPrompts}`;
|
|
749
322
|
* @returns Current parameters or undefined if not set
|
750
323
|
*/
|
751
324
|
getModelParams() {
|
752
|
-
|
325
|
+
// Always get from SettingsService
|
326
|
+
const settingsService = getSettingsService();
|
327
|
+
const providerSettings = settingsService.getProviderSettings(this.name);
|
328
|
+
if (!providerSettings) {
|
329
|
+
return undefined;
|
330
|
+
}
|
331
|
+
const params = {};
|
332
|
+
if (providerSettings.temperature !== undefined)
|
333
|
+
params.temperature = providerSettings.temperature;
|
334
|
+
if (providerSettings.max_tokens !== undefined)
|
335
|
+
params.max_tokens = providerSettings.max_tokens;
|
336
|
+
if (providerSettings.top_p !== undefined)
|
337
|
+
params.top_p = providerSettings.top_p;
|
338
|
+
if (providerSettings.top_k !== undefined)
|
339
|
+
params.top_k = providerSettings.top_k;
|
340
|
+
if (providerSettings.stop_sequences !== undefined)
|
341
|
+
params.stop_sequences = providerSettings.stop_sequences;
|
342
|
+
return Object.keys(params).length > 0 ? params : undefined;
|
343
|
+
}
|
344
|
+
/**
|
345
|
+
* Override clearAuthCache to also clear cached auth key
|
346
|
+
*/
|
347
|
+
clearAuthCache() {
|
348
|
+
super.clearAuthCache();
|
349
|
+
this._cachedAuthKey = undefined;
|
753
350
|
}
|
754
351
|
/**
|
755
352
|
* Check if the provider is authenticated using any available method
|
@@ -758,5 +355,421 @@ ${llxprtPrompts}`;
|
|
758
355
|
async isAuthenticated() {
|
759
356
|
return super.isAuthenticated();
|
760
357
|
}
|
358
|
+
/**
|
359
|
+
* Detect the appropriate tool format for the current model/configuration
|
360
|
+
* @returns The detected tool format
|
361
|
+
*/
|
362
|
+
detectToolFormat() {
|
363
|
+
try {
|
364
|
+
const settingsService = getSettingsService();
|
365
|
+
// First check SettingsService for toolFormat override in provider settings
|
366
|
+
// Note: This is synchronous access to cached settings, not async
|
367
|
+
const currentSettings = settingsService['settings'];
|
368
|
+
const providerSettings = currentSettings?.providers?.[this.name];
|
369
|
+
const toolFormatOverride = providerSettings?.toolFormat;
|
370
|
+
// If explicitly set to a specific format (not 'auto'), use it
|
371
|
+
if (toolFormatOverride && toolFormatOverride !== 'auto') {
|
372
|
+
return toolFormatOverride;
|
373
|
+
}
|
374
|
+
// Auto-detect based on model name if set to 'auto' or not set
|
375
|
+
const modelName = this.getCurrentModel().toLowerCase();
|
376
|
+
// Check for GLM-4.5 models (glm-4.5, glm-4-5)
|
377
|
+
if (modelName.includes('glm-4.5') || modelName.includes('glm-4-5')) {
|
378
|
+
return 'qwen';
|
379
|
+
}
|
380
|
+
// Check for qwen models
|
381
|
+
if (modelName.includes('qwen')) {
|
382
|
+
return 'qwen';
|
383
|
+
}
|
384
|
+
// Default to 'anthropic' format
|
385
|
+
return 'anthropic';
|
386
|
+
}
|
387
|
+
catch (error) {
|
388
|
+
this.logger.debug(() => `Failed to detect tool format from SettingsService: ${error}`);
|
389
|
+
// Fallback detection without SettingsService
|
390
|
+
const modelName = this.getCurrentModel().toLowerCase();
|
391
|
+
if (modelName.includes('glm-4.5') || modelName.includes('glm-4-5')) {
|
392
|
+
return 'qwen';
|
393
|
+
}
|
394
|
+
if (modelName.includes('qwen')) {
|
395
|
+
return 'qwen';
|
396
|
+
}
|
397
|
+
return 'anthropic';
|
398
|
+
}
|
399
|
+
}
|
400
|
+
getToolFormat() {
|
401
|
+
// Use the same detection logic as detectToolFormat()
|
402
|
+
return this.detectToolFormat();
|
403
|
+
}
|
404
|
+
/**
|
405
|
+
* Normalize tool IDs from various formats to Anthropic format
|
406
|
+
* Handles IDs from OpenAI (call_xxx), Anthropic (toolu_xxx), and history (hist_tool_xxx)
|
407
|
+
*/
|
408
|
+
normalizeToAnthropicToolId(id) {
|
409
|
+
// If already in Anthropic format, return as-is
|
410
|
+
if (id.startsWith('toolu_')) {
|
411
|
+
return id;
|
412
|
+
}
|
413
|
+
// For history format, extract the UUID and add Anthropic prefix
|
414
|
+
if (id.startsWith('hist_tool_')) {
|
415
|
+
const uuid = id.substring('hist_tool_'.length);
|
416
|
+
return 'toolu_' + uuid;
|
417
|
+
}
|
418
|
+
// For OpenAI format, extract the UUID and add Anthropic prefix
|
419
|
+
if (id.startsWith('call_')) {
|
420
|
+
const uuid = id.substring('call_'.length);
|
421
|
+
return 'toolu_' + uuid;
|
422
|
+
}
|
423
|
+
// Unknown format - assume it's a raw UUID
|
424
|
+
return 'toolu_' + id;
|
425
|
+
}
|
426
|
+
/**
|
427
|
+
* Normalize tool IDs from Anthropic format to history format
|
428
|
+
*/
|
429
|
+
normalizeToHistoryToolId(id) {
|
430
|
+
// If already in history format, return as-is
|
431
|
+
if (id.startsWith('hist_tool_')) {
|
432
|
+
return id;
|
433
|
+
}
|
434
|
+
// For Anthropic format, extract the UUID and add history prefix
|
435
|
+
if (id.startsWith('toolu_')) {
|
436
|
+
const uuid = id.substring('toolu_'.length);
|
437
|
+
return 'hist_tool_' + uuid;
|
438
|
+
}
|
439
|
+
// For OpenAI format, extract the UUID and add history prefix
|
440
|
+
if (id.startsWith('call_')) {
|
441
|
+
const uuid = id.substring('call_'.length);
|
442
|
+
return 'hist_tool_' + uuid;
|
443
|
+
}
|
444
|
+
// Unknown format - assume it's a raw UUID
|
445
|
+
return 'hist_tool_' + id;
|
446
|
+
}
|
447
|
+
/**
|
448
|
+
* Generate chat completion with IContent interface
|
449
|
+
* Convert IContent directly to Anthropic API format
|
450
|
+
*/
|
451
|
+
async *generateChatCompletion(content, tools) {
|
452
|
+
// Convert IContent directly to Anthropic API format (no IMessage!)
|
453
|
+
const anthropicMessages = [];
|
454
|
+
// Extract system message if present
|
455
|
+
// let systemMessage: string | undefined;
|
456
|
+
// Filter out orphaned tool responses at the beginning of the conversation
|
457
|
+
// TODO: Investigate post-0.2.2 - These shouldn't be truly orphaned since the same
|
458
|
+
// history works with OpenAI/Cerebras. Likely Anthropic has stricter formatting
|
459
|
+
// requirements for tool responses that we're not fully meeting yet.
|
460
|
+
let startIndex = 0;
|
461
|
+
while (startIndex < content.length &&
|
462
|
+
content[startIndex].speaker === 'tool') {
|
463
|
+
this.logger.debug(() => `Skipping orphaned tool response at beginning of conversation`);
|
464
|
+
startIndex++;
|
465
|
+
}
|
466
|
+
const filteredContent = content.slice(startIndex);
|
467
|
+
// Group consecutive tool responses together for Anthropic API
|
468
|
+
let pendingToolResults = [];
|
469
|
+
const flushToolResults = () => {
|
470
|
+
if (pendingToolResults.length > 0) {
|
471
|
+
anthropicMessages.push({
|
472
|
+
role: 'user',
|
473
|
+
content: pendingToolResults,
|
474
|
+
});
|
475
|
+
pendingToolResults = [];
|
476
|
+
}
|
477
|
+
};
|
478
|
+
for (const c of filteredContent) {
|
479
|
+
if (c.speaker === 'human') {
|
480
|
+
// Flush any pending tool results before adding a human message
|
481
|
+
flushToolResults();
|
482
|
+
const textBlock = c.blocks.find((b) => b.type === 'text');
|
483
|
+
// Add text block as user message
|
484
|
+
anthropicMessages.push({
|
485
|
+
role: 'user',
|
486
|
+
content: textBlock?.text || '',
|
487
|
+
});
|
488
|
+
}
|
489
|
+
else if (c.speaker === 'ai') {
|
490
|
+
// Flush any pending tool results before adding an AI message
|
491
|
+
flushToolResults();
|
492
|
+
const textBlocks = c.blocks.filter((b) => b.type === 'text');
|
493
|
+
const toolCallBlocks = c.blocks.filter((b) => b.type === 'tool_call');
|
494
|
+
if (toolCallBlocks.length > 0) {
|
495
|
+
// Build content array with text and tool_use blocks
|
496
|
+
const contentArray = [];
|
497
|
+
// Add text if present
|
498
|
+
const contentText = textBlocks.map((b) => b.text).join('');
|
499
|
+
if (contentText) {
|
500
|
+
contentArray.push({ type: 'text', text: contentText });
|
501
|
+
}
|
502
|
+
// Add tool uses
|
503
|
+
for (const tc of toolCallBlocks) {
|
504
|
+
// Ensure parameters are an object, not a string
|
505
|
+
let parametersObj = tc.parameters;
|
506
|
+
if (typeof parametersObj === 'string') {
|
507
|
+
try {
|
508
|
+
parametersObj = JSON.parse(parametersObj);
|
509
|
+
}
|
510
|
+
catch (e) {
|
511
|
+
this.logger.debug(() => `Failed to parse tool parameters as JSON: ${e}`);
|
512
|
+
parametersObj = {};
|
513
|
+
}
|
514
|
+
}
|
515
|
+
contentArray.push({
|
516
|
+
type: 'tool_use',
|
517
|
+
id: this.normalizeToAnthropicToolId(tc.id),
|
518
|
+
name: tc.name,
|
519
|
+
input: parametersObj,
|
520
|
+
});
|
521
|
+
}
|
522
|
+
anthropicMessages.push({
|
523
|
+
role: 'assistant',
|
524
|
+
content: contentArray,
|
525
|
+
});
|
526
|
+
}
|
527
|
+
else {
|
528
|
+
// Text-only message
|
529
|
+
const contentText = textBlocks.map((b) => b.text).join('');
|
530
|
+
anthropicMessages.push({
|
531
|
+
role: 'assistant',
|
532
|
+
content: contentText,
|
533
|
+
});
|
534
|
+
}
|
535
|
+
}
|
536
|
+
else if (c.speaker === 'tool') {
|
537
|
+
const toolResponseBlock = c.blocks.find((b) => b.type === 'tool_response');
|
538
|
+
if (!toolResponseBlock) {
|
539
|
+
throw new Error('Tool content must have a tool_response block');
|
540
|
+
}
|
541
|
+
// Collect tool results to be grouped together
|
542
|
+
pendingToolResults.push({
|
543
|
+
type: 'tool_result',
|
544
|
+
tool_use_id: this.normalizeToAnthropicToolId(toolResponseBlock.callId),
|
545
|
+
content: JSON.stringify(toolResponseBlock.result),
|
546
|
+
});
|
547
|
+
}
|
548
|
+
else {
|
549
|
+
throw new Error(`Unknown speaker type: ${c.speaker}`);
|
550
|
+
}
|
551
|
+
}
|
552
|
+
// Flush any remaining tool results at the end
|
553
|
+
flushToolResults();
|
554
|
+
// Validate that all tool_results have corresponding tool_uses
|
555
|
+
// Anthropic requires strict pairing between tool_use and tool_result
|
556
|
+
const toolUseIds = new Set();
|
557
|
+
const toolResultIds = new Set();
|
558
|
+
for (const msg of anthropicMessages) {
|
559
|
+
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
560
|
+
for (const block of msg.content) {
|
561
|
+
if (block.type === 'tool_use') {
|
562
|
+
toolUseIds.add(block.id);
|
563
|
+
}
|
564
|
+
}
|
565
|
+
}
|
566
|
+
else if (msg.role === 'user' && Array.isArray(msg.content)) {
|
567
|
+
for (const block of msg.content) {
|
568
|
+
if (block.type === 'tool_result') {
|
569
|
+
toolResultIds.add(block.tool_use_id);
|
570
|
+
}
|
571
|
+
}
|
572
|
+
}
|
573
|
+
}
|
574
|
+
// Remove orphaned tool results (results without corresponding tool uses)
|
575
|
+
const orphanedResults = Array.from(toolResultIds).filter((id) => !toolUseIds.has(id));
|
576
|
+
if (orphanedResults.length > 0) {
|
577
|
+
this.logger.debug(() => `Found ${orphanedResults.length} orphaned tool results, removing them`);
|
578
|
+
// Filter out messages that only contain orphaned tool results
|
579
|
+
const filteredMessages = anthropicMessages.filter((msg) => {
|
580
|
+
if (msg.role === 'user' && Array.isArray(msg.content)) {
|
581
|
+
const filteredContent = msg.content.filter((block) => block.type !== 'tool_result' ||
|
582
|
+
!orphanedResults.includes(block.tool_use_id));
|
583
|
+
if (filteredContent.length === 0) {
|
584
|
+
// Remove empty user messages
|
585
|
+
return false;
|
586
|
+
}
|
587
|
+
msg.content = filteredContent;
|
588
|
+
}
|
589
|
+
return true;
|
590
|
+
});
|
591
|
+
// Replace the messages array
|
592
|
+
anthropicMessages.length = 0;
|
593
|
+
anthropicMessages.push(...filteredMessages);
|
594
|
+
}
|
595
|
+
// Ensure the conversation starts with a valid message type
|
596
|
+
// Anthropic requires the first message to be from the user
|
597
|
+
if (anthropicMessages.length > 0 && anthropicMessages[0].role !== 'user') {
|
598
|
+
// If the first message is not from the user, add a minimal user message
|
599
|
+
this.logger.debug(() => `First message is not from user, adding placeholder user message`);
|
600
|
+
anthropicMessages.unshift({
|
601
|
+
role: 'user',
|
602
|
+
content: 'Continue the conversation',
|
603
|
+
});
|
604
|
+
}
|
605
|
+
// Ensure we have at least one message
|
606
|
+
if (anthropicMessages.length === 0) {
|
607
|
+
anthropicMessages.push({
|
608
|
+
role: 'user',
|
609
|
+
content: 'Hello',
|
610
|
+
});
|
611
|
+
}
|
612
|
+
// Convert Gemini format tools directly to Anthropic format using the new method
|
613
|
+
const anthropicTools = this.toolFormatter.convertGeminiToAnthropic(tools);
|
614
|
+
// Ensure authentication
|
615
|
+
await this.updateClientWithResolvedAuth();
|
616
|
+
// Check OAuth mode
|
617
|
+
const authToken = await this.getAuthToken();
|
618
|
+
const isOAuth = authToken && authToken.startsWith('sk-ant-oat');
|
619
|
+
// Get streaming setting from ephemeral settings (default: enabled)
|
620
|
+
const streamingSetting = this.providerConfig?.getEphemeralSettings?.()?.['streaming'];
|
621
|
+
const streamingEnabled = streamingSetting !== 'disabled';
|
622
|
+
// Build request with proper typing
|
623
|
+
const currentModel = this.getCurrentModel();
|
624
|
+
// Get the system prompt for non-OAuth mode
|
625
|
+
const userMemory = this.globalConfig?.getUserMemory
|
626
|
+
? this.globalConfig.getUserMemory()
|
627
|
+
: '';
|
628
|
+
// For OAuth mode, inject core system prompt as the first human message
|
629
|
+
if (isOAuth) {
|
630
|
+
const corePrompt = await getCoreSystemPromptAsync(userMemory, currentModel, undefined);
|
631
|
+
if (corePrompt) {
|
632
|
+
anthropicMessages.unshift({
|
633
|
+
role: 'user',
|
634
|
+
content: `<system>\n${corePrompt}\n</system>\n\nUser provided conversation begins here:`,
|
635
|
+
});
|
636
|
+
}
|
637
|
+
}
|
638
|
+
const systemPrompt = !isOAuth
|
639
|
+
? await getCoreSystemPromptAsync(userMemory, currentModel, undefined)
|
640
|
+
: undefined;
|
641
|
+
const requestBody = {
|
642
|
+
model: currentModel,
|
643
|
+
messages: anthropicMessages,
|
644
|
+
max_tokens: this.getMaxTokensForModel(currentModel),
|
645
|
+
stream: streamingEnabled,
|
646
|
+
...(this.getModelParams() || {}),
|
647
|
+
...(isOAuth
|
648
|
+
? {
|
649
|
+
system: "You are Claude Code, Anthropic's official CLI for Claude.",
|
650
|
+
}
|
651
|
+
: systemPrompt
|
652
|
+
? { system: systemPrompt }
|
653
|
+
: {}),
|
654
|
+
...(anthropicTools && anthropicTools.length > 0
|
655
|
+
? { tools: anthropicTools }
|
656
|
+
: {}),
|
657
|
+
};
|
658
|
+
// Debug log the tools being sent to Anthropic
|
659
|
+
if (anthropicTools && anthropicTools.length > 0) {
|
660
|
+
this.logger.debug(() => `[AnthropicProvider] Sending tools to API:`, {
|
661
|
+
toolCount: anthropicTools.length,
|
662
|
+
toolNames: anthropicTools.map((t) => t.name),
|
663
|
+
firstTool: anthropicTools[0],
|
664
|
+
requestHasTools: 'tools' in requestBody,
|
665
|
+
});
|
666
|
+
}
|
667
|
+
// Make the API call directly with type assertion
|
668
|
+
const response = await this.anthropic.messages.create(requestBody);
|
669
|
+
if (streamingEnabled) {
|
670
|
+
// Handle streaming response - response is already a Stream when streaming is enabled
|
671
|
+
const stream = response;
|
672
|
+
let currentToolCall;
|
673
|
+
for await (const chunk of stream) {
|
674
|
+
if (chunk.type === 'content_block_start') {
|
675
|
+
if (chunk.content_block.type === 'tool_use') {
|
676
|
+
currentToolCall = {
|
677
|
+
id: chunk.content_block.id,
|
678
|
+
name: chunk.content_block.name,
|
679
|
+
input: '',
|
680
|
+
};
|
681
|
+
}
|
682
|
+
}
|
683
|
+
else if (chunk.type === 'content_block_delta') {
|
684
|
+
if (chunk.delta.type === 'text_delta') {
|
685
|
+
// Emit text immediately as IContent
|
686
|
+
yield {
|
687
|
+
speaker: 'ai',
|
688
|
+
blocks: [{ type: 'text', text: chunk.delta.text }],
|
689
|
+
};
|
690
|
+
}
|
691
|
+
else if (chunk.delta.type === 'input_json_delta' &&
|
692
|
+
currentToolCall) {
|
693
|
+
currentToolCall.input += chunk.delta.partial_json;
|
694
|
+
// Check for double-escaping patterns
|
695
|
+
const detectedFormat = this.detectToolFormat();
|
696
|
+
logDoubleEscapingInChunk(chunk.delta.partial_json, currentToolCall.name, detectedFormat);
|
697
|
+
}
|
698
|
+
}
|
699
|
+
else if (chunk.type === 'content_block_stop') {
|
700
|
+
if (currentToolCall) {
|
701
|
+
// Process tool parameters with double-escape handling
|
702
|
+
const detectedFormat = this.detectToolFormat();
|
703
|
+
const processedParameters = processToolParameters(currentToolCall.input, currentToolCall.name, detectedFormat);
|
704
|
+
yield {
|
705
|
+
speaker: 'ai',
|
706
|
+
blocks: [
|
707
|
+
{
|
708
|
+
type: 'tool_call',
|
709
|
+
id: this.normalizeToHistoryToolId(currentToolCall.id),
|
710
|
+
name: currentToolCall.name,
|
711
|
+
parameters: processedParameters,
|
712
|
+
},
|
713
|
+
],
|
714
|
+
};
|
715
|
+
currentToolCall = undefined;
|
716
|
+
}
|
717
|
+
}
|
718
|
+
else if (chunk.type === 'message_delta' && chunk.usage) {
|
719
|
+
// Emit usage metadata
|
720
|
+
yield {
|
721
|
+
speaker: 'ai',
|
722
|
+
blocks: [],
|
723
|
+
metadata: {
|
724
|
+
usage: {
|
725
|
+
promptTokens: chunk.usage.input_tokens || 0,
|
726
|
+
completionTokens: chunk.usage.output_tokens || 0,
|
727
|
+
totalTokens: (chunk.usage.input_tokens || 0) +
|
728
|
+
(chunk.usage.output_tokens || 0),
|
729
|
+
},
|
730
|
+
},
|
731
|
+
};
|
732
|
+
}
|
733
|
+
}
|
734
|
+
}
|
735
|
+
else {
|
736
|
+
// Handle non-streaming response
|
737
|
+
const message = response;
|
738
|
+
const blocks = [];
|
739
|
+
// Process content blocks
|
740
|
+
const detectedFormat = this.detectToolFormat();
|
741
|
+
for (const contentBlock of message.content) {
|
742
|
+
if (contentBlock.type === 'text') {
|
743
|
+
blocks.push({ type: 'text', text: contentBlock.text });
|
744
|
+
}
|
745
|
+
else if (contentBlock.type === 'tool_use') {
|
746
|
+
// Process tool parameters with double-escape handling
|
747
|
+
const processedParameters = processToolParameters(JSON.stringify(contentBlock.input), contentBlock.name, detectedFormat);
|
748
|
+
blocks.push({
|
749
|
+
type: 'tool_call',
|
750
|
+
id: this.normalizeToHistoryToolId(contentBlock.id),
|
751
|
+
name: contentBlock.name,
|
752
|
+
parameters: processedParameters,
|
753
|
+
});
|
754
|
+
}
|
755
|
+
}
|
756
|
+
// Build response IContent
|
757
|
+
const result = {
|
758
|
+
speaker: 'ai',
|
759
|
+
blocks,
|
760
|
+
};
|
761
|
+
// Add usage metadata if present
|
762
|
+
if (message.usage) {
|
763
|
+
result.metadata = {
|
764
|
+
usage: {
|
765
|
+
promptTokens: message.usage.input_tokens,
|
766
|
+
completionTokens: message.usage.output_tokens,
|
767
|
+
totalTokens: message.usage.input_tokens + message.usage.output_tokens,
|
768
|
+
},
|
769
|
+
};
|
770
|
+
}
|
771
|
+
yield result;
|
772
|
+
}
|
773
|
+
}
|
761
774
|
}
|
762
775
|
//# sourceMappingURL=AnthropicProvider.js.map
|