@vybestack/llxprt-code-core 0.1.23 → 0.2.2-nightly.250908.fb8099b7

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.
Files changed (149) hide show
  1. package/README.md +21 -17
  2. package/dist/src/adapters/IStreamAdapter.d.ts +3 -3
  3. package/dist/src/auth/oauth-errors.d.ts +173 -0
  4. package/dist/src/auth/oauth-errors.js +461 -0
  5. package/dist/src/auth/oauth-errors.js.map +1 -0
  6. package/dist/src/auth/precedence.d.ts +1 -5
  7. package/dist/src/auth/precedence.js +28 -48
  8. package/dist/src/auth/precedence.js.map +1 -1
  9. package/dist/src/auth/token-store.js +2 -2
  10. package/dist/src/auth/token-store.js.map +1 -1
  11. package/dist/src/auth/types.d.ts +4 -4
  12. package/dist/src/code_assist/codeAssist.js +19 -6
  13. package/dist/src/code_assist/codeAssist.js.map +1 -1
  14. package/dist/src/code_assist/oauth2.d.ts +7 -0
  15. package/dist/src/code_assist/oauth2.js +82 -32
  16. package/dist/src/code_assist/oauth2.js.map +1 -1
  17. package/dist/src/code_assist/server.js +15 -4
  18. package/dist/src/code_assist/server.js.map +1 -1
  19. package/dist/src/code_assist/setup.js +9 -0
  20. package/dist/src/code_assist/setup.js.map +1 -1
  21. package/dist/src/config/index.d.ts +7 -0
  22. package/dist/src/config/index.js +8 -0
  23. package/dist/src/config/index.js.map +1 -0
  24. package/dist/src/core/client.d.ts +15 -20
  25. package/dist/src/core/client.js +98 -124
  26. package/dist/src/core/client.js.map +1 -1
  27. package/dist/src/core/compression-config.d.ts +10 -0
  28. package/dist/src/core/compression-config.js +17 -0
  29. package/dist/src/core/compression-config.js.map +1 -0
  30. package/dist/src/core/coreToolScheduler.js +50 -15
  31. package/dist/src/core/coreToolScheduler.js.map +1 -1
  32. package/dist/src/core/geminiChat.d.ts +68 -9
  33. package/dist/src/core/geminiChat.js +940 -405
  34. package/dist/src/core/geminiChat.js.map +1 -1
  35. package/dist/src/core/nonInteractiveToolExecutor.js +70 -19
  36. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  37. package/dist/src/core/prompts.js +35 -25
  38. package/dist/src/core/prompts.js.map +1 -1
  39. package/dist/src/core/turn.d.ts +1 -0
  40. package/dist/src/core/turn.js +8 -6
  41. package/dist/src/core/turn.js.map +1 -1
  42. package/dist/src/ide/ide-client.d.ts +1 -1
  43. package/dist/src/ide/ide-client.js +12 -6
  44. package/dist/src/ide/ide-client.js.map +1 -1
  45. package/dist/src/index.d.ts +4 -2
  46. package/dist/src/index.js +5 -2
  47. package/dist/src/index.js.map +1 -1
  48. package/dist/src/prompt-config/TemplateEngine.js +17 -0
  49. package/dist/src/prompt-config/TemplateEngine.js.map +1 -1
  50. package/dist/src/prompt-config/defaults/core-defaults.js +39 -32
  51. package/dist/src/prompt-config/defaults/core-defaults.js.map +1 -1
  52. package/dist/src/prompt-config/defaults/core.md +2 -0
  53. package/dist/src/prompt-config/defaults/provider-defaults.js +34 -27
  54. package/dist/src/prompt-config/defaults/provider-defaults.js.map +1 -1
  55. package/dist/src/prompt-config/defaults/providers/gemini/core.md +270 -0
  56. package/dist/src/prompt-config/defaults/providers/gemini/models/gemini-2.5-flash/core.md +12 -0
  57. package/dist/src/prompt-config/defaults/providers/gemini/models/gemini-2.5-flash/gemini-2-5-flash/core.md +12 -0
  58. package/dist/src/prompt-config/types.d.ts +2 -0
  59. package/dist/src/providers/BaseProvider.d.ts +39 -13
  60. package/dist/src/providers/BaseProvider.js +102 -28
  61. package/dist/src/providers/BaseProvider.js.map +1 -1
  62. package/dist/src/providers/IProvider.d.ts +17 -3
  63. package/dist/src/providers/LoggingProviderWrapper.d.ts +10 -3
  64. package/dist/src/providers/LoggingProviderWrapper.js +33 -27
  65. package/dist/src/providers/LoggingProviderWrapper.js.map +1 -1
  66. package/dist/src/providers/ProviderContentGenerator.d.ts +2 -2
  67. package/dist/src/providers/ProviderContentGenerator.js +9 -6
  68. package/dist/src/providers/ProviderContentGenerator.js.map +1 -1
  69. package/dist/src/providers/ProviderManager.d.ts +4 -0
  70. package/dist/src/providers/ProviderManager.js +6 -0
  71. package/dist/src/providers/ProviderManager.js.map +1 -1
  72. package/dist/src/providers/anthropic/AnthropicProvider.d.ts +34 -21
  73. package/dist/src/providers/anthropic/AnthropicProvider.js +505 -492
  74. package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
  75. package/dist/src/providers/gemini/GeminiProvider.d.ts +23 -9
  76. package/dist/src/providers/gemini/GeminiProvider.js +344 -515
  77. package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
  78. package/dist/src/providers/openai/ConversationCache.d.ts +3 -3
  79. package/dist/src/providers/openai/IChatGenerateParams.d.ts +9 -4
  80. package/dist/src/providers/openai/OpenAIProvider.d.ts +46 -96
  81. package/dist/src/providers/openai/OpenAIProvider.js +532 -1393
  82. package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
  83. package/dist/src/providers/openai/buildResponsesRequest.d.ts +3 -3
  84. package/dist/src/providers/openai/buildResponsesRequest.js +67 -37
  85. package/dist/src/providers/openai/buildResponsesRequest.js.map +1 -1
  86. package/dist/src/providers/openai/estimateRemoteTokens.d.ts +2 -2
  87. package/dist/src/providers/openai/estimateRemoteTokens.js +21 -8
  88. package/dist/src/providers/openai/estimateRemoteTokens.js.map +1 -1
  89. package/dist/src/providers/openai/parseResponsesStream.d.ts +6 -2
  90. package/dist/src/providers/openai/parseResponsesStream.js +99 -391
  91. package/dist/src/providers/openai/parseResponsesStream.js.map +1 -1
  92. package/dist/src/providers/openai/syntheticToolResponses.d.ts +5 -5
  93. package/dist/src/providers/openai/syntheticToolResponses.js +102 -91
  94. package/dist/src/providers/openai/syntheticToolResponses.js.map +1 -1
  95. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.d.ts +89 -0
  96. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js +451 -0
  97. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js.map +1 -0
  98. package/dist/src/providers/openai-responses/index.d.ts +1 -0
  99. package/dist/src/providers/openai-responses/index.js +2 -0
  100. package/dist/src/providers/openai-responses/index.js.map +1 -0
  101. package/dist/src/providers/tokenizers/OpenAITokenizer.js +3 -3
  102. package/dist/src/providers/tokenizers/OpenAITokenizer.js.map +1 -1
  103. package/dist/src/providers/types.d.ts +1 -1
  104. package/dist/src/services/ClipboardService.d.ts +19 -0
  105. package/dist/src/services/ClipboardService.js +66 -0
  106. package/dist/src/services/ClipboardService.js.map +1 -0
  107. package/dist/src/services/history/ContentConverters.d.ts +43 -0
  108. package/dist/src/services/history/ContentConverters.js +325 -0
  109. package/dist/src/services/history/ContentConverters.js.map +1 -0
  110. package/dist/src/{providers/IMessage.d.ts → services/history/HistoryEvents.d.ts} +16 -22
  111. package/dist/src/{providers/IMessage.js → services/history/HistoryEvents.js} +1 -1
  112. package/dist/src/services/history/HistoryEvents.js.map +1 -0
  113. package/dist/src/services/history/HistoryService.d.ts +220 -0
  114. package/dist/src/services/history/HistoryService.js +673 -0
  115. package/dist/src/services/history/HistoryService.js.map +1 -0
  116. package/dist/src/services/history/IContent.d.ts +183 -0
  117. package/dist/src/services/history/IContent.js +104 -0
  118. package/dist/src/services/history/IContent.js.map +1 -0
  119. package/dist/src/services/index.d.ts +1 -0
  120. package/dist/src/services/index.js +1 -0
  121. package/dist/src/services/index.js.map +1 -1
  122. package/dist/src/telemetry/types.d.ts +16 -4
  123. package/dist/src/telemetry/types.js.map +1 -1
  124. package/dist/src/tools/IToolFormatter.d.ts +2 -2
  125. package/dist/src/tools/ToolFormatter.d.ts +42 -4
  126. package/dist/src/tools/ToolFormatter.js +159 -37
  127. package/dist/src/tools/ToolFormatter.js.map +1 -1
  128. package/dist/src/tools/doubleEscapeUtils.d.ts +57 -0
  129. package/dist/src/tools/doubleEscapeUtils.js +241 -0
  130. package/dist/src/tools/doubleEscapeUtils.js.map +1 -0
  131. package/dist/src/tools/read-file.d.ts +6 -1
  132. package/dist/src/tools/read-file.js +25 -11
  133. package/dist/src/tools/read-file.js.map +1 -1
  134. package/dist/src/tools/todo-schemas.d.ts +4 -4
  135. package/dist/src/tools/tools.js +13 -0
  136. package/dist/src/tools/tools.js.map +1 -1
  137. package/dist/src/tools/write-file.d.ts +6 -1
  138. package/dist/src/tools/write-file.js +48 -26
  139. package/dist/src/tools/write-file.js.map +1 -1
  140. package/dist/src/types/modelParams.d.ts +8 -0
  141. package/dist/src/utils/bfsFileSearch.js +2 -6
  142. package/dist/src/utils/bfsFileSearch.js.map +1 -1
  143. package/dist/src/utils/schemaValidator.js +16 -1
  144. package/dist/src/utils/schemaValidator.js.map +1 -1
  145. package/package.json +8 -7
  146. package/dist/src/providers/IMessage.js.map +0 -1
  147. package/dist/src/providers/adapters/GeminiCompatibleWrapper.d.ts +0 -69
  148. package/dist/src/providers/adapters/GeminiCompatibleWrapper.js +0 -577
  149. 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 { ContentGeneratorRole } from '../ContentGeneratorRole.js';
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
- baseURL;
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
- else {
95
- // Regular API key auth
96
- if (this.anthropic.apiKey !== resolvedToken) {
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: this.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?.(apiKey);
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: this.baseURL,
171
+ baseURL: resolvedBaseURL,
487
172
  dangerouslyAllowBrowser: true,
488
173
  });
489
174
  }
490
175
  setBaseUrl(baseUrl) {
491
- // If no baseUrl is provided, clear to default (undefined)
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: this.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
- // Keep local cache for performance
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
- // Fall back to cached value or default
528
- return this.currentModel || this.getDefaultModel();
210
+ // Always return from getDefaultModel, no caching
211
+ return this.getDefaultModel();
529
212
  }
530
213
  getDefaultModel() {
531
- // Return the default model for this provider
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
- this.modelParams = undefined;
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
- this.modelParams = { ...this.modelParams, ...params };
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
- return this.modelParams;
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