@ebowwa/coder 0.7.64 → 0.7.65

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 (101) hide show
  1. package/dist/index.js +36168 -32
  2. package/dist/interfaces/ui/terminal/cli/index.js +34253 -158
  3. package/dist/interfaces/ui/terminal/native/README.md +53 -0
  4. package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
  5. package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
  6. package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
  7. package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
  8. package/dist/interfaces/ui/terminal/native/index.js +43 -0
  9. package/dist/interfaces/ui/terminal/native/index.node +0 -0
  10. package/dist/interfaces/ui/terminal/native/package.json +34 -0
  11. package/dist/native/README.md +53 -0
  12. package/dist/native/claude_code_native.darwin-x64.node +0 -0
  13. package/dist/native/claude_code_native.dylib +0 -0
  14. package/dist/native/index.d.ts +0 -480
  15. package/dist/native/index.darwin-arm64.node +0 -0
  16. package/dist/native/index.js +43 -1625
  17. package/dist/native/index.node +0 -0
  18. package/dist/native/package.json +34 -0
  19. package/native/index.darwin-arm64.node +0 -0
  20. package/native/index.js +33 -19
  21. package/package.json +3 -2
  22. package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
  23. package/packages/src/core/agent-loop/compaction.ts +6 -2
  24. package/packages/src/core/agent-loop/index.ts +2 -0
  25. package/packages/src/core/agent-loop/loop-state.ts +1 -1
  26. package/packages/src/core/agent-loop/turn-executor.ts +4 -0
  27. package/packages/src/core/agent-loop/types.ts +4 -0
  28. package/packages/src/core/api-client-impl.ts +283 -173
  29. package/packages/src/core/cognitive-security/hooks.ts +2 -1
  30. package/packages/src/core/config/todo +7 -0
  31. package/packages/src/core/context/__tests__/integration.test.ts +334 -0
  32. package/packages/src/core/context/compaction.ts +170 -0
  33. package/packages/src/core/context/constants.ts +58 -0
  34. package/packages/src/core/context/extraction.ts +85 -0
  35. package/packages/src/core/context/index.ts +66 -0
  36. package/packages/src/core/context/summarization.ts +251 -0
  37. package/packages/src/core/context/token-estimation.ts +98 -0
  38. package/packages/src/core/context/types.ts +59 -0
  39. package/packages/src/core/models.ts +81 -4
  40. package/packages/src/core/normalizers/todo +5 -1
  41. package/packages/src/core/providers/README.md +230 -0
  42. package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
  43. package/packages/src/core/providers/index.ts +419 -0
  44. package/packages/src/core/providers/types.ts +132 -0
  45. package/packages/src/core/retry.ts +10 -0
  46. package/packages/src/ecosystem/tools/index.ts +174 -0
  47. package/packages/src/index.ts +23 -2
  48. package/packages/src/interfaces/ui/index.ts +17 -20
  49. package/packages/src/interfaces/ui/spinner.ts +2 -2
  50. package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
  51. package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
  52. package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
  53. package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
  54. package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
  55. package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
  56. package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
  57. package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
  58. package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +393 -0
  59. package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
  60. package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
  61. package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
  62. package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
  63. package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
  64. package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
  65. package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
  66. package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
  67. package/packages/src/native/index.ts +404 -27
  68. package/packages/src/native/tui_v2_types.ts +39 -0
  69. package/packages/src/teammates/coordination.test.ts +279 -0
  70. package/packages/src/teammates/coordination.ts +646 -0
  71. package/packages/src/teammates/index.ts +95 -25
  72. package/packages/src/teammates/integration.test.ts +272 -0
  73. package/packages/src/teammates/runner.test.ts +235 -0
  74. package/packages/src/teammates/runner.ts +750 -0
  75. package/packages/src/teammates/schemas.ts +673 -0
  76. package/packages/src/types/index.ts +1 -0
  77. package/packages/src/core/context-compaction.ts +0 -578
  78. package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
  79. package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
  80. package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
  81. package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
  82. package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
  83. package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
  84. package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
  85. package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
  86. package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
  87. package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
  88. package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
  89. package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
  90. package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
  91. package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
  92. package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
  93. package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
  94. package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
  95. package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
  96. package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
  97. package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
  98. package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
  99. package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
  100. package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
  101. package/packages/src/interfaces/ui/terminal/tui/useNativeInput.ts +0 -239
@@ -1,5 +1,10 @@
1
1
  /**
2
2
  * API Client - SSE streaming for LLM APIs
3
+ *
4
+ * Supports multiple providers:
5
+ * - Zhipu (Z.AI / GLM models) - OpenAI format
6
+ * - MiniMax (M2.5) - Anthropic format
7
+ * - OpenAI (future)
3
8
  */
4
9
 
5
10
  import type {
@@ -33,6 +38,30 @@ import {
33
38
  DEFAULT_MODEL,
34
39
  supportsExtendedThinking,
35
40
  } from "./models.js";
41
+ import {
42
+ resolveProvider,
43
+ getProviderForModel,
44
+ recordProviderSuccess,
45
+ recordProviderFailure,
46
+ type ProviderName,
47
+ type ProviderConfig,
48
+ } from "./providers/index.js";
49
+
50
+ /**
51
+ * Convert Anthropic-style tools to OpenAI-style tools
52
+ * Anthropic: { name, description, input_schema }
53
+ * OpenAI: { type: "function", function: { name, description, parameters } }
54
+ */
55
+ function convertToolsToOpenAIFormat(tools: APITool[]): unknown[] {
56
+ return tools.map((tool) => ({
57
+ type: "function",
58
+ function: {
59
+ name: tool.name,
60
+ description: tool.description,
61
+ parameters: tool.input_schema,
62
+ },
63
+ }));
64
+ }
36
65
 
37
66
  export interface StreamOptions {
38
67
  apiKey: string;
@@ -50,6 +79,8 @@ export interface StreamOptions {
50
79
  /** Called when redacted thinking is received (data is base64) */
51
80
  onRedactedThinking?: (data: string) => void;
52
81
  onToolUse?: (toolUse: { id: string; name: string; input: unknown }) => void;
82
+ /** Called when a retry is about to start - UI should reset streaming state */
83
+ onRetryStart?: () => void;
53
84
  signal?: AbortSignal;
54
85
  }
55
86
 
@@ -197,153 +228,55 @@ export function calculateCacheMetrics(usage: UsageMetrics): CacheMetrics {
197
228
  }
198
229
 
199
230
  /**
200
- * Create a streaming message request to Anthropic API
231
+ * Callbacks to emit during streaming (passed in, not buffered)
201
232
  */
202
- export async function createMessageStream(
203
- messages: Message[],
204
- options: StreamOptions
205
- ): Promise<StreamResult> {
206
- const {
207
- apiKey,
208
- model = "claude-sonnet-4-6",
209
- maxTokens = 4096,
210
- tools,
211
- systemPrompt,
212
- cacheConfig = DEFAULT_CACHE_CONFIG,
213
- thinking,
214
- extendedThinking,
215
- onToken,
216
- onThinking,
217
- onRedactedThinking,
218
- onToolUse,
219
- signal,
220
- } = options;
221
-
222
- const startTime = Date.now();
223
- let ttft = 0;
224
- let firstToken = true;
225
- let totalThinkingTokens = 0;
226
-
227
- // Build cached messages
228
- const cachedMessages = buildCachedMessages(messages, cacheConfig);
229
-
230
- // Build system prompt with cache control
231
- const cachedSystemPrompt = buildSystemPrompt(systemPrompt, cacheConfig);
232
-
233
- // Build request
234
- const request: APIRequest = {
235
- model,
236
- max_tokens: maxTokens,
237
- messages: cachedMessages.map((m) => ({
238
- role: m.role,
239
- content: m.content,
240
- })),
241
- stream: true,
242
- };
243
-
244
- // Add system prompt if provided
245
- if (cachedSystemPrompt) {
246
- request.system = cachedSystemPrompt;
247
- }
248
-
249
- // Add tools if provided (with optional caching)
250
- if (tools && tools.length > 0) {
251
- request.tools = tools;
252
- }
253
-
254
- // Determine API endpoint (support custom base URL for GLM, etc.)
255
- const baseUrl = process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com";
256
- const apiEndpoint = `${baseUrl}/v1/messages`;
257
-
258
- // Build headers
259
- const headers: Record<string, string> = {
260
- "Content-Type": "application/json",
261
- "x-api-key": apiKey,
262
- "anthropic-version": "2023-06-01",
263
- };
264
-
265
- // Determine thinking configuration
266
- const shouldUseExtendedThinking =
267
- (extendedThinking?.enabled ?? false) ||
268
- (thinking && thinking.type !== "disabled");
269
-
270
- if (shouldUseExtendedThinking && supportsExtendedThinking(model)) {
271
- // Calculate budget tokens
272
- let budgetTokens: number;
273
-
274
- if (extendedThinking?.budgetTokens) {
275
- budgetTokens = extendedThinking.budgetTokens;
276
- } else if (thinking?.type === "enabled") {
277
- budgetTokens = thinking.budget_tokens;
278
- } else {
279
- // Use effort level to determine budget
280
- const effort = extendedThinking?.effort || "medium";
281
- budgetTokens = calculateBudgetTokens(
282
- {
283
- enabled: true,
284
- effort,
285
- modelMultiplier: model.includes("opus") ? 2 : 1,
286
- },
287
- model
288
- );
289
- }
290
-
291
- // Clamp budget to valid range
292
- budgetTokens = Math.max(1024, Math.min(budgetTokens, 100000));
293
-
294
- request.thinking = {
295
- type: "enabled",
296
- budget_tokens: budgetTokens,
297
- };
298
-
299
- // Add beta headers for extended thinking features
300
- const betaFeatures: string[] = ["extended-thinking-2025-01-24"];
301
-
302
- // Add interleaved thinking support if enabled
303
- if (extendedThinking?.interleaved !== false) {
304
- betaFeatures.push("interleaved-thinking-2025-01-24");
305
- }
306
-
307
- headers["anthropic-beta"] = betaFeatures.join(",");
308
- } else {
309
- // Default beta header
310
- headers["anthropic-beta"] = "max-tokens-3-5-sonnet-2024-07-15";
311
- }
312
-
313
- // Make API request with retry logic
314
- const retryOptions: RetryOptions = {
315
- maxRetries: 3,
316
- baseDelayMs: 1000,
317
- maxDelayMs: 30000,
318
- retryableStatusCodes: [429, 500, 502, 503, 504, 529],
319
- onRetry: (attempt, error, delayMs) => {
320
- console.log(`\x1b[33mAPI retry ${attempt}/3 after ${delayMs}ms: ${error.message}\x1b[0m`);
321
- },
322
- };
233
+ interface StreamCallbacks {
234
+ onToken?: (text: string) => void;
235
+ onThinking?: (thinking: string) => void;
236
+ onRedactedThinking?: (data: string) => void;
237
+ onToolUse?: (toolUse: { id: string; name: string; input: unknown }) => void;
238
+ onRetryStart?: () => void;
239
+ }
323
240
 
324
- const response = await withRetry(
325
- async () => {
326
- const res = await fetch(apiEndpoint, {
327
- method: "POST",
328
- headers,
329
- body: JSON.stringify(request),
330
- signal,
331
- });
332
-
333
- // Throw for retryable status codes so withRetry can handle them
334
- if (!res.ok && retryOptions.retryableStatusCodes?.includes(res.status)) {
335
- const errorText = await res.text();
336
- throw new Error(`API error: ${res.status} - ${errorText}`);
337
- }
241
+ /**
242
+ * Internal result from a single stream attempt
243
+ */
244
+ interface StreamAttemptResult {
245
+ message: APIResponse | null;
246
+ content: ContentBlock[];
247
+ usage: UsageMetrics;
248
+ thinkingTokens: number;
249
+ ttftMs: number;
250
+ }
338
251
 
339
- return res;
340
- },
341
- retryOptions
342
- );
252
+ /**
253
+ * Execute a single streaming API attempt
254
+ * Emits callbacks in real-time for streaming display
255
+ */
256
+ async function executeStreamAttempt(
257
+ request: APIRequest,
258
+ headers: Record<string, string>,
259
+ apiEndpoint: string,
260
+ signal: AbortSignal | undefined,
261
+ model: string,
262
+ retryableStatusCodes: number[],
263
+ startTime: number,
264
+ callbacks: StreamCallbacks
265
+ ): Promise<StreamAttemptResult> {
266
+ const response = await fetch(apiEndpoint, {
267
+ method: "POST",
268
+ headers,
269
+ body: JSON.stringify(request),
270
+ signal,
271
+ });
343
272
 
273
+ // Throw for retryable status codes so withRetry can handle them
344
274
  if (!response.ok) {
345
- const error = await response.text();
346
- throw new Error(`API error: ${response.status} - ${error}`);
275
+ const errorText = await response.text();
276
+ if (retryableStatusCodes.includes(response.status)) {
277
+ throw new Error(`API error: ${response.status} - ${errorText}`);
278
+ }
279
+ throw new Error(`API error: ${response.status} - ${errorText}`);
347
280
  }
348
281
 
349
282
  if (!response.body) {
@@ -363,7 +296,9 @@ export async function createMessageStream(
363
296
  let currentToolUseBlock: ToolUseBlock | null = null;
364
297
  let toolUseInput = "";
365
298
 
366
- const buffer = "";
299
+ let ttft = 0;
300
+ let firstToken = true;
301
+ let totalThinkingTokens = 0;
367
302
 
368
303
  try {
369
304
  let buffer = "";
@@ -440,7 +375,7 @@ export async function createMessageStream(
440
375
  if (delta.type === "text_delta" && currentTextBlock) {
441
376
  const text = delta.text as string;
442
377
  currentTextBlock.text += text;
443
- onToken?.(text);
378
+ callbacks.onToken?.(text); // Emit in real-time
444
379
 
445
380
  if (firstToken) {
446
381
  ttft = Date.now() - startTime;
@@ -449,14 +384,13 @@ export async function createMessageStream(
449
384
  } else if (delta.type === "thinking_delta" && currentThinkingBlock) {
450
385
  const thinking = delta.thinking as string;
451
386
  currentThinkingBlock.thinking += thinking;
452
- onThinking?.(thinking);
453
- totalThinkingTokens += Math.ceil(thinking.length / 4); // Rough estimate
387
+ callbacks.onThinking?.(thinking); // Emit in real-time
388
+ totalThinkingTokens += Math.ceil(thinking.length / 4);
454
389
  } else if (delta.type === "redacted_thinking_delta" && currentRedactedThinkingBlock) {
455
- // Handle redacted thinking deltas
456
390
  const redactedData = delta.data as string;
457
391
  currentRedactedThinkingBlock.data += redactedData;
458
- onRedactedThinking?.(redactedData);
459
- totalThinkingTokens += Math.ceil(redactedData.length / 4); // Rough estimate
392
+ callbacks.onRedactedThinking?.(redactedData); // Emit in real-time
393
+ totalThinkingTokens += Math.ceil(redactedData.length / 4);
460
394
  } else if (delta.type === "input_json_delta" && currentToolUseBlock) {
461
395
  toolUseInput += delta.partial_json as string;
462
396
  }
@@ -464,8 +398,6 @@ export async function createMessageStream(
464
398
  }
465
399
 
466
400
  case "content_block_stop": {
467
- // content_block_stop event has { index: number }, not the block itself
468
- // We need to check which current block is active and push it
469
401
  if (currentTextBlock !== null) {
470
402
  currentContent.push(currentTextBlock);
471
403
  currentTextBlock = null;
@@ -474,7 +406,6 @@ export async function createMessageStream(
474
406
  currentThinkingBlock = null;
475
407
  } else if (currentRedactedThinkingBlock !== null) {
476
408
  currentContent.push(currentRedactedThinkingBlock);
477
- onRedactedThinking?.(currentRedactedThinkingBlock.data);
478
409
  currentRedactedThinkingBlock = null;
479
410
  } else if (currentToolUseBlock !== null) {
480
411
  try {
@@ -483,11 +414,11 @@ export async function createMessageStream(
483
414
  currentToolUseBlock.input = {};
484
415
  }
485
416
  currentContent.push(currentToolUseBlock);
486
- onToolUse?.({
417
+ callbacks.onToolUse?.({
487
418
  id: currentToolUseBlock.id,
488
419
  name: currentToolUseBlock.name,
489
420
  input: currentToolUseBlock.input,
490
- });
421
+ }); // Emit in real-time
491
422
  currentToolUseBlock = null;
492
423
  toolUseInput = "";
493
424
  }
@@ -506,13 +437,10 @@ export async function createMessageStream(
506
437
  }
507
438
 
508
439
  case "message_stop":
509
- // Message complete
510
440
  break;
511
441
 
512
442
  // OpenAI/Z.AI compatible format (for GLM-5, etc.)
513
- // OpenAI streaming sends chunks with choices array
514
443
  default: {
515
- // Check for OpenAI format: { choices: [{ delta: { content: "..." } }], usage: {...} }
516
444
  if (event.choices && Array.isArray(event.choices)) {
517
445
  const choice = event.choices[0] as { delta?: { content?: string }; finish_reason?: string } | undefined;
518
446
  if (choice?.delta?.content) {
@@ -522,13 +450,12 @@ export async function createMessageStream(
522
450
  } else {
523
451
  currentTextBlock = { type: "text", text };
524
452
  }
525
- onToken?.(text);
453
+ callbacks.onToken?.(text); // Emit in real-time
526
454
  if (firstToken) {
527
455
  ttft = Date.now() - startTime;
528
456
  firstToken = false;
529
457
  }
530
458
  }
531
- // Check for finish
532
459
  if (choice?.finish_reason) {
533
460
  if (currentTextBlock) {
534
461
  currentContent.push(currentTextBlock);
@@ -550,7 +477,6 @@ export async function createMessageStream(
550
477
  }
551
478
  }
552
479
  }
553
- // OpenAI usage format (often in final chunk)
554
480
  if (event.usage) {
555
481
  const openaiUsage = event.usage as { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number };
556
482
  usage.input_tokens = openaiUsage.prompt_tokens || 0;
@@ -560,11 +486,13 @@ export async function createMessageStream(
560
486
  }
561
487
  }
562
488
  } catch (err: unknown) {
563
- // Log the parse error with more detail
489
+ // Only rethrow if it's an API error, not a JSON parse error
490
+ if (err instanceof Error && err.message.startsWith("API error:")) {
491
+ throw err;
492
+ }
564
493
  if (process.env.DEBUG_API === '1') {
565
494
  console.error('\x1b[91m[DEBUG] JSON parse error:', err);
566
495
  console.error('\x1b[91m[DEBUG] Error parsing SSE data:', data.substring(0, 200));
567
- console.error('\x1b[91m[DEBUG] Original buffer:', buffer.substring(0, 500));
568
496
  }
569
497
  }
570
498
  }
@@ -573,8 +501,8 @@ export async function createMessageStream(
573
501
  reader.releaseLock();
574
502
  }
575
503
 
504
+ // Handle "No message received" case - this is retryable
576
505
  if (!message) {
577
- // If we received content via OpenAI format but no message_start, create a message
578
506
  if (currentContent.length > 0) {
579
507
  message = {
580
508
  id: `msg-${Date.now()}`,
@@ -587,31 +515,213 @@ export async function createMessageStream(
587
515
  usage: { input_tokens: 0, output_tokens: 0 },
588
516
  };
589
517
  } else {
590
- // Debug: Log what we did receive
591
- if (process.env.DEBUG_API === '1') {
592
- console.log('\x1b[91m[DEBUG] No message_start event received. Buffer:\x1b[0m', buffer.substring(0, 500));
593
- }
518
+ // This is a transient error - throw to trigger retry
594
519
  throw new Error("No message received from API");
595
520
  }
596
521
  }
597
522
 
598
- message.content = currentContent;
523
+ return {
524
+ message,
525
+ content: currentContent,
526
+ usage,
527
+ thinkingTokens: totalThinkingTokens,
528
+ ttftMs: ttft,
529
+ };
530
+ }
531
+
532
+ /**
533
+ * Create a streaming message request to Anthropic API
534
+ * Full retry support including stream parsing errors
535
+ */
536
+ export async function createMessageStream(
537
+ messages: Message[],
538
+ options: StreamOptions
539
+ ): Promise<StreamResult> {
540
+ const {
541
+ apiKey,
542
+ model = "claude-sonnet-4-6",
543
+ maxTokens = 4096,
544
+ tools,
545
+ systemPrompt,
546
+ cacheConfig = DEFAULT_CACHE_CONFIG,
547
+ thinking,
548
+ extendedThinking,
549
+ onToken,
550
+ onThinking,
551
+ onRedactedThinking,
552
+ onToolUse,
553
+ onRetryStart,
554
+ signal,
555
+ } = options;
556
+
557
+ const startTime = Date.now();
558
+
559
+ // Build cached messages
560
+ const cachedMessages = buildCachedMessages(messages, cacheConfig);
561
+
562
+ // Build system prompt with cache control
563
+ const cachedSystemPrompt = buildSystemPrompt(systemPrompt, cacheConfig);
564
+
565
+ // Build request
566
+ const request: APIRequest = {
567
+ model,
568
+ max_tokens: maxTokens,
569
+ messages: cachedMessages.map((m) => ({
570
+ role: m.role,
571
+ content: m.content,
572
+ })),
573
+ stream: true,
574
+ };
575
+
576
+ if (cachedSystemPrompt) {
577
+ request.system = cachedSystemPrompt;
578
+ }
579
+
580
+ // Tools will be set after determining API format (for format conversion)
581
+
582
+ // Resolve provider based on model name
583
+ const providerInfo = resolveProvider(model);
584
+
585
+ // Determine API endpoint and headers based on provider
586
+ let apiEndpoint: string;
587
+ let headers: Record<string, string>;
588
+ let apiFormat: "anthropic" | "openai";
589
+
590
+ if (providerInfo) {
591
+ // Use provider-specific configuration
592
+ apiEndpoint = providerInfo.endpoint;
593
+ apiFormat = providerInfo.config.format;
594
+
595
+ if (apiFormat === "anthropic") {
596
+ // Anthropic/MiniMax format
597
+ headers = {
598
+ "Content-Type": "application/json",
599
+ [providerInfo.config.authHeader]: providerInfo.apiKey,
600
+ "anthropic-version": "2023-06-01",
601
+ };
602
+ } else {
603
+ // OpenAI/Zhipu format
604
+ headers = {
605
+ "Content-Type": "application/json",
606
+ [providerInfo.config.authHeader]: `Bearer ${providerInfo.apiKey}`,
607
+ };
608
+ }
609
+ } else {
610
+ // Fallback to environment-based configuration (legacy)
611
+ const baseUrl = process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com";
612
+ apiEndpoint = `${baseUrl}/v1/messages`;
613
+ apiFormat = "anthropic";
614
+
615
+ headers = {
616
+ "Content-Type": "application/json",
617
+ "x-api-key": apiKey,
618
+ "anthropic-version": "2023-06-01",
619
+ };
620
+ }
621
+
622
+ // Set tools with format conversion if needed
623
+ if (tools && tools.length > 0) {
624
+ if (apiFormat === "openai") {
625
+ // Convert Anthropic-style tools to OpenAI format
626
+ // Cast needed because OpenAI format differs from APITool
627
+ (request as unknown as Record<string, unknown>).tools = convertToolsToOpenAIFormat(tools);
628
+ } else {
629
+ // Keep Anthropic format as-is
630
+ request.tools = tools;
631
+ }
632
+ }
633
+
634
+ const shouldUseExtendedThinking =
635
+ (extendedThinking?.enabled ?? false) ||
636
+ (thinking && thinking.type !== "disabled");
637
+
638
+ if (shouldUseExtendedThinking && supportsExtendedThinking(model)) {
639
+ let budgetTokens: number;
640
+
641
+ if (extendedThinking?.budgetTokens) {
642
+ budgetTokens = extendedThinking.budgetTokens;
643
+ } else if (thinking?.type === "enabled") {
644
+ budgetTokens = thinking.budget_tokens;
645
+ } else {
646
+ const effort = extendedThinking?.effort || "medium";
647
+ budgetTokens = calculateBudgetTokens(
648
+ { enabled: true, effort, modelMultiplier: model.includes("opus") ? 2 : 1 },
649
+ model
650
+ );
651
+ }
652
+
653
+ budgetTokens = Math.max(1024, Math.min(budgetTokens, 100000));
654
+
655
+ request.thinking = { type: "enabled", budget_tokens: budgetTokens };
656
+
657
+ const betaFeatures: string[] = ["extended-thinking-2025-01-24"];
658
+ if (extendedThinking?.interleaved !== false) {
659
+ betaFeatures.push("interleaved-thinking-2025-01-24");
660
+ }
661
+ headers["anthropic-beta"] = betaFeatures.join(",");
662
+ } else if (apiFormat === "anthropic") {
663
+ headers["anthropic-beta"] = "max-tokens-3-5-sonnet-2024-07-15";
664
+ }
665
+
666
+ // Retry options - now covers entire stream parsing
667
+ const retryOptions: RetryOptions = {
668
+ maxRetries: 10,
669
+ baseDelayMs: 1000,
670
+ maxDelayMs: 60000,
671
+ retryableStatusCodes: [429, 500, 502, 503, 504, 529],
672
+ onRetry: (attempt, error, delayMs) => {
673
+ console.log(`\x1b[33mAPI retry ${attempt}/10 after ${delayMs}ms: ${error.message}\x1b[0m`);
674
+ // Notify UI to reset streaming state before retry
675
+ onRetryStart?.();
676
+ // Track provider failure on retry
677
+ const providerName = getProviderForModel(model);
678
+ if (providerName) {
679
+ recordProviderFailure(providerName);
680
+ }
681
+ },
682
+ };
683
+
684
+ // Execute with retry - wraps entire fetch + stream parsing
685
+ // Callbacks are emitted in real-time during streaming
686
+ const result = await withRetry(
687
+ () => executeStreamAttempt(
688
+ request,
689
+ headers,
690
+ apiEndpoint,
691
+ signal,
692
+ model,
693
+ retryOptions.retryableStatusCodes ?? [],
694
+ startTime,
695
+ { onToken, onThinking, onRedactedThinking, onToolUse }
696
+ ),
697
+ retryOptions
698
+ );
699
+
700
+ // Build final message
701
+ const message = result.message!;
702
+ message.content = result.content;
599
703
 
600
704
  // Calculate cost and cache metrics
601
- const { costUSD, estimatedSavingsUSD } = calculateCost(model, usage);
602
- const cacheMetrics = calculateCacheMetrics(usage);
705
+ const { costUSD, estimatedSavingsUSD } = calculateCost(model, result.usage);
706
+ const cacheMetrics = calculateCacheMetrics(result.usage);
603
707
  cacheMetrics.estimatedSavingsUSD = estimatedSavingsUSD;
604
708
 
605
709
  const durationMs = Date.now() - startTime;
606
710
 
711
+ // Track provider health on success
712
+ const providerName = getProviderForModel(model);
713
+ if (providerName) {
714
+ recordProviderSuccess(providerName, durationMs);
715
+ }
716
+
607
717
  return {
608
718
  message,
609
- usage,
719
+ usage: result.usage,
610
720
  cacheMetrics,
611
721
  costUSD,
612
722
  durationMs,
613
- ttftMs: ttft || durationMs,
614
- thinkingTokens: totalThinkingTokens,
723
+ ttftMs: result.ttftMs || durationMs,
724
+ thinkingTokens: result.thinkingTokens,
615
725
  };
616
726
  }
617
727
 
@@ -493,7 +493,8 @@ export class CognitiveSecurityHooks {
493
493
  if (this.config.logEvents) {
494
494
  const prefix = action === "deny" ? "\x1b[31m[Security]\x1b[0m" : "\x1b[90m[Security]\x1b[0m";
495
495
  const toolStr = tool ? ` ${tool}:` : "";
496
- console.log(`${prefix}${toolStr} ${reason}`);
496
+ // Use console.error to avoid interfering with TUI (stdout is used by renderer)
497
+ console.error(`${prefix}${toolStr} ${reason}`);
497
498
  }
498
499
  }
499
500
 
@@ -0,0 +1,7 @@
1
+ mv
2
+ - /Users/ebowwa/Desktop/codespaces/packages/src/coder/packages/src/core/claude-md.ts
3
+ - /Users/ebowwa/Desktop/codespaces/packages/src/coder/packages/src/core/config-loader.ts
4
+ - /Users/ebowwa/Desktop/codespaces/packages/src/coder/packages/src/core/models.ts
5
+ - /Users/ebowwa/Desktop/codespaces/packages/src/coder/packages/src/core/permissions.ts
6
+
7
+ here aqi