codemaxxing 0.1.8 → 0.1.9

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/src/agent.ts CHANGED
@@ -1,18 +1,63 @@
1
1
  import OpenAI from "openai";
2
+ import Anthropic from "@anthropic-ai/sdk";
2
3
  import type {
3
4
  ChatCompletionMessageParam,
4
5
  ChatCompletionTool,
5
6
  ChatCompletionChunk,
6
7
  } from "openai/resources/chat/completions";
7
- import { FILE_TOOLS, executeTool } from "./tools/files.js";
8
+ import { FILE_TOOLS, executeTool, generateDiff, getExistingContent } from "./tools/files.js";
8
9
  import { buildProjectContext, getSystemPrompt } from "./utils/context.js";
9
10
  import { isGitRepo, autoCommit } from "./utils/git.js";
10
- import { createSession, saveMessage, updateTokenEstimate, loadMessages } from "./utils/sessions.js";
11
+ import { createSession, saveMessage, updateTokenEstimate, updateSessionCost, loadMessages } from "./utils/sessions.js";
11
12
  import type { ProviderConfig } from "./config.js";
12
13
 
13
14
  // Tools that can modify your project — require approval
14
15
  const DANGEROUS_TOOLS = new Set(["write_file", "run_command"]);
15
16
 
17
+ // Cost per 1M tokens (input/output) for common models
18
+ const MODEL_COSTS: Record<string, { input: number; output: number }> = {
19
+ // OpenAI
20
+ "gpt-4o": { input: 2.5, output: 10 },
21
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
22
+ "gpt-4-turbo": { input: 10, output: 30 },
23
+ "gpt-4": { input: 30, output: 60 },
24
+ "gpt-3.5-turbo": { input: 0.5, output: 1.5 },
25
+ "o1": { input: 15, output: 60 },
26
+ "o1-mini": { input: 3, output: 12 },
27
+ "o3-mini": { input: 1.1, output: 4.4 },
28
+ // Anthropic
29
+ "claude-3-5-sonnet-20241022": { input: 3, output: 15 },
30
+ "claude-3-5-sonnet": { input: 3, output: 15 },
31
+ "claude-sonnet-4-20250514": { input: 3, output: 15 },
32
+ "claude-3-5-haiku-20241022": { input: 0.8, output: 4 },
33
+ "claude-3-opus-20240229": { input: 15, output: 75 },
34
+ "claude-3-haiku-20240307": { input: 0.25, output: 1.25 },
35
+ // Qwen (typically free/cheap on local, but OpenRouter pricing)
36
+ "qwen/qwen-2.5-coder-32b-instruct": { input: 0.2, output: 0.2 },
37
+ "qwen/qwen-2.5-72b-instruct": { input: 0.35, output: 0.4 },
38
+ // DeepSeek
39
+ "deepseek/deepseek-chat": { input: 0.14, output: 0.28 },
40
+ "deepseek/deepseek-coder": { input: 0.14, output: 0.28 },
41
+ // Llama
42
+ "meta-llama/llama-3.1-70b-instruct": { input: 0.52, output: 0.75 },
43
+ "meta-llama/llama-3.1-8b-instruct": { input: 0.055, output: 0.055 },
44
+ // Google
45
+ "google/gemini-pro-1.5": { input: 1.25, output: 5 },
46
+ "google/gemini-flash-1.5": { input: 0.075, output: 0.3 },
47
+ };
48
+
49
+ function getModelCost(model: string): { input: number; output: number } {
50
+ // Direct match
51
+ if (MODEL_COSTS[model]) return MODEL_COSTS[model];
52
+ // Partial match (model name contains a known key)
53
+ const lower = model.toLowerCase();
54
+ for (const [key, cost] of Object.entries(MODEL_COSTS)) {
55
+ if (lower.includes(key) || key.includes(lower)) return cost;
56
+ }
57
+ // Default: $0 (local/unknown models)
58
+ return { input: 0, output: 0 };
59
+ }
60
+
16
61
  export interface AgentOptions {
17
62
  provider: ProviderConfig;
18
63
  cwd: string;
@@ -22,8 +67,10 @@ export interface AgentOptions {
22
67
  onToolCall?: (name: string, args: Record<string, unknown>) => void;
23
68
  onToolResult?: (name: string, result: string) => void;
24
69
  onThinking?: (text: string) => void;
25
- onToolApproval?: (name: string, args: Record<string, unknown>) => Promise<"yes" | "no" | "always">;
70
+ onToolApproval?: (name: string, args: Record<string, unknown>, diff?: string) => Promise<"yes" | "no" | "always">;
26
71
  onGitCommit?: (message: string) => void;
72
+ onContextCompressed?: (oldTokens: number, newTokens: number) => void;
73
+ contextCompressionThreshold?: number;
27
74
  }
28
75
 
29
76
  interface AssembledToolCall {
@@ -34,6 +81,8 @@ interface AssembledToolCall {
34
81
 
35
82
  export class CodingAgent {
36
83
  private client: OpenAI;
84
+ private anthropicClient: Anthropic | null = null;
85
+ private providerType: "openai" | "anthropic";
37
86
  private messages: ChatCompletionMessageParam[] = [];
38
87
  private tools: ChatCompletionTool[] = FILE_TOOLS;
39
88
  private cwd: string;
@@ -45,17 +94,33 @@ export class CodingAgent {
45
94
  private autoCommitEnabled: boolean = false;
46
95
  private repoMap: string = "";
47
96
  private sessionId: string = "";
97
+ private totalPromptTokens: number = 0;
98
+ private totalCompletionTokens: number = 0;
99
+ private totalCost: number = 0;
100
+ private systemPrompt: string = "";
101
+ private compressionThreshold: number;
48
102
 
49
103
  constructor(private options: AgentOptions) {
104
+ this.providerType = options.provider.type || "openai";
50
105
  this.client = new OpenAI({
51
106
  baseURL: options.provider.baseUrl,
52
107
  apiKey: options.provider.apiKey,
53
108
  });
109
+ if (this.providerType === "anthropic") {
110
+ this.anthropicClient = new Anthropic({
111
+ apiKey: options.provider.apiKey,
112
+ });
113
+ }
54
114
  this.cwd = options.cwd;
55
115
  this.maxTokens = options.maxTokens;
56
116
  this.autoApprove = options.autoApprove;
57
117
  this.model = options.provider.model;
118
+ // Default model for Anthropic
119
+ if (this.providerType === "anthropic" && (this.model === "auto" || !this.model)) {
120
+ this.model = "claude-sonnet-4-20250514";
121
+ }
58
122
  this.gitEnabled = isGitRepo(this.cwd);
123
+ this.compressionThreshold = options.contextCompressionThreshold ?? 80000;
59
124
  }
60
125
 
61
126
  /**
@@ -63,10 +128,10 @@ export class CodingAgent {
63
128
  */
64
129
  async init(): Promise<void> {
65
130
  const context = await buildProjectContext(this.cwd);
66
- const systemPrompt = await getSystemPrompt(context);
131
+ this.systemPrompt = await getSystemPrompt(context);
67
132
 
68
133
  this.messages = [
69
- { role: "system", content: systemPrompt },
134
+ { role: "system", content: this.systemPrompt },
70
135
  ];
71
136
 
72
137
  // Create a new session
@@ -116,6 +181,13 @@ export class CodingAgent {
116
181
  this.messages.push(userMsg);
117
182
  saveMessage(this.sessionId, userMsg);
118
183
 
184
+ // Check if context needs compression before sending
185
+ await this.maybeCompressContext();
186
+
187
+ if (this.providerType === "anthropic" && this.anthropicClient) {
188
+ return this.chatAnthropic(userMessage);
189
+ }
190
+
119
191
  let iterations = 0;
120
192
  const MAX_ITERATIONS = 20;
121
193
 
@@ -128,6 +200,7 @@ export class CodingAgent {
128
200
  tools: this.tools,
129
201
  max_tokens: this.maxTokens,
130
202
  stream: true,
203
+ stream_options: { include_usage: true },
131
204
  });
132
205
 
133
206
  // Accumulate the streamed response
@@ -135,8 +208,15 @@ export class CodingAgent {
135
208
  let thinkingText = "";
136
209
  let inThinking = false;
137
210
  const toolCalls: Map<number, AssembledToolCall> = new Map();
211
+ let chunkPromptTokens = 0;
212
+ let chunkCompletionTokens = 0;
138
213
 
139
214
  for await (const chunk of stream) {
215
+ // Capture usage from the final chunk
216
+ if ((chunk as any).usage) {
217
+ chunkPromptTokens = (chunk as any).usage.prompt_tokens ?? 0;
218
+ chunkCompletionTokens = (chunk as any).usage.completion_tokens ?? 0;
219
+ }
140
220
  const delta = chunk.choices?.[0]?.delta;
141
221
  if (!delta) continue;
142
222
 
@@ -195,6 +275,16 @@ export class CodingAgent {
195
275
  this.messages.push(assistantMessage);
196
276
  saveMessage(this.sessionId, assistantMessage);
197
277
 
278
+ // Track token usage and cost
279
+ if (chunkPromptTokens > 0 || chunkCompletionTokens > 0) {
280
+ this.totalPromptTokens += chunkPromptTokens;
281
+ this.totalCompletionTokens += chunkCompletionTokens;
282
+ const costs = getModelCost(this.model);
283
+ this.totalCost = (this.totalPromptTokens / 1_000_000) * costs.input +
284
+ (this.totalCompletionTokens / 1_000_000) * costs.output;
285
+ updateSessionCost(this.sessionId, this.totalPromptTokens, this.totalCompletionTokens, this.totalCost);
286
+ }
287
+
198
288
  // If no tool calls, we're done — return the text
199
289
  if (toolCalls.size === 0) {
200
290
  updateTokenEstimate(this.sessionId, this.estimateTokens());
@@ -215,7 +305,15 @@ export class CodingAgent {
215
305
  // Check approval for dangerous tools
216
306
  if (DANGEROUS_TOOLS.has(toolCall.name) && !this.autoApprove && !this.alwaysApproved.has(toolCall.name)) {
217
307
  if (this.options.onToolApproval) {
218
- const decision = await this.options.onToolApproval(toolCall.name, args);
308
+ // Generate diff for write_file if file already exists
309
+ let diff: string | undefined;
310
+ if (toolCall.name === "write_file" && args.path && args.content) {
311
+ const existing = getExistingContent(String(args.path), this.cwd);
312
+ if (existing !== null) {
313
+ diff = generateDiff(existing, String(args.content), String(args.path));
314
+ }
315
+ }
316
+ const decision = await this.options.onToolApproval(toolCall.name, args, diff);
219
317
  if (decision === "no") {
220
318
  const denied = `Tool call "${toolCall.name}" was denied by the user.`;
221
319
  this.options.onToolResult?.(toolCall.name, denied);
@@ -262,6 +360,198 @@ export class CodingAgent {
262
360
  return "Max iterations reached. The agent may be stuck in a loop.";
263
361
  }
264
362
 
363
+ /**
364
+ * Convert OpenAI-format tools to Anthropic tool format
365
+ */
366
+ private getAnthropicTools(): Anthropic.Tool[] {
367
+ return this.tools.map((t) => ({
368
+ name: t.function.name,
369
+ description: t.function.description ?? "",
370
+ input_schema: (t.function.parameters as Anthropic.Tool.InputSchema) ?? { type: "object" as const, properties: {} },
371
+ }));
372
+ }
373
+
374
+ /**
375
+ * Convert messages to Anthropic format (separate system from conversation)
376
+ */
377
+ private getAnthropicMessages(): Anthropic.MessageParam[] {
378
+ const msgs: Anthropic.MessageParam[] = [];
379
+ for (const msg of this.messages) {
380
+ if (msg.role === "system") continue; // system handled separately
381
+ if (msg.role === "user") {
382
+ msgs.push({ role: "user", content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content) });
383
+ } else if (msg.role === "assistant") {
384
+ const content: Anthropic.ContentBlockParam[] = [];
385
+ if (msg.content) {
386
+ content.push({ type: "text", text: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content) });
387
+ }
388
+ if ("tool_calls" in msg && Array.isArray((msg as any).tool_calls)) {
389
+ for (const tc of (msg as any).tool_calls) {
390
+ let input: Record<string, unknown> = {};
391
+ try { input = JSON.parse(tc.function.arguments); } catch {}
392
+ content.push({
393
+ type: "tool_use",
394
+ id: tc.id,
395
+ name: tc.function.name,
396
+ input,
397
+ });
398
+ }
399
+ }
400
+ if (content.length > 0) {
401
+ msgs.push({ role: "assistant", content });
402
+ }
403
+ } else if (msg.role === "tool") {
404
+ const toolCallId = (msg as any).tool_call_id;
405
+ const resultContent = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
406
+ // Anthropic expects tool results as user messages with tool_result content
407
+ msgs.push({
408
+ role: "user",
409
+ content: [{
410
+ type: "tool_result",
411
+ tool_use_id: toolCallId,
412
+ content: resultContent,
413
+ }],
414
+ });
415
+ }
416
+ }
417
+ return msgs;
418
+ }
419
+
420
+ /**
421
+ * Anthropic-native streaming chat
422
+ */
423
+ private async chatAnthropic(_userMessage: string): Promise<string> {
424
+ const client = this.anthropicClient!;
425
+ let iterations = 0;
426
+ const MAX_ITERATIONS = 20;
427
+
428
+ while (iterations < MAX_ITERATIONS) {
429
+ iterations++;
430
+
431
+ const anthropicMessages = this.getAnthropicMessages();
432
+ const anthropicTools = this.getAnthropicTools();
433
+
434
+ const stream = client.messages.stream({
435
+ model: this.model,
436
+ max_tokens: this.maxTokens,
437
+ system: this.systemPrompt,
438
+ messages: anthropicMessages,
439
+ tools: anthropicTools,
440
+ });
441
+
442
+ let contentText = "";
443
+ const toolCalls: Array<{ id: string; name: string; input: Record<string, unknown> }> = [];
444
+ let currentToolId = "";
445
+ let currentToolName = "";
446
+ let currentToolInput = "";
447
+
448
+ stream.on("text", (text) => {
449
+ contentText += text;
450
+ this.options.onToken?.(text);
451
+ });
452
+
453
+ const finalMessage = await stream.finalMessage();
454
+
455
+ // Track usage
456
+ if (finalMessage.usage) {
457
+ const promptTokens = finalMessage.usage.input_tokens;
458
+ const completionTokens = finalMessage.usage.output_tokens;
459
+ this.totalPromptTokens += promptTokens;
460
+ this.totalCompletionTokens += completionTokens;
461
+ const costs = getModelCost(this.model);
462
+ this.totalCost = (this.totalPromptTokens / 1_000_000) * costs.input +
463
+ (this.totalCompletionTokens / 1_000_000) * costs.output;
464
+ updateSessionCost(this.sessionId, this.totalPromptTokens, this.totalCompletionTokens, this.totalCost);
465
+ }
466
+
467
+ // Extract tool uses from content blocks
468
+ for (const block of finalMessage.content) {
469
+ if (block.type === "tool_use") {
470
+ toolCalls.push({
471
+ id: block.id,
472
+ name: block.name,
473
+ input: block.input as Record<string, unknown>,
474
+ });
475
+ }
476
+ }
477
+
478
+ // Build OpenAI-format assistant message for session storage
479
+ const assistantMessage: any = { role: "assistant", content: contentText || null };
480
+ if (toolCalls.length > 0) {
481
+ assistantMessage.tool_calls = toolCalls.map((tc) => ({
482
+ id: tc.id,
483
+ type: "function" as const,
484
+ function: { name: tc.name, arguments: JSON.stringify(tc.input) },
485
+ }));
486
+ }
487
+ this.messages.push(assistantMessage);
488
+ saveMessage(this.sessionId, assistantMessage);
489
+
490
+ // If no tool calls, we're done
491
+ if (toolCalls.length === 0) {
492
+ updateTokenEstimate(this.sessionId, this.estimateTokens());
493
+ return contentText || "(empty response)";
494
+ }
495
+
496
+ // Process tool calls
497
+ for (const toolCall of toolCalls) {
498
+ const args = toolCall.input;
499
+ this.options.onToolCall?.(toolCall.name, args);
500
+
501
+ // Check approval for dangerous tools
502
+ if (DANGEROUS_TOOLS.has(toolCall.name) && !this.autoApprove && !this.alwaysApproved.has(toolCall.name)) {
503
+ if (this.options.onToolApproval) {
504
+ let diff: string | undefined;
505
+ if (toolCall.name === "write_file" && args.path && args.content) {
506
+ const existing = getExistingContent(String(args.path), this.cwd);
507
+ if (existing !== null) {
508
+ diff = generateDiff(existing, String(args.content), String(args.path));
509
+ }
510
+ }
511
+ const decision = await this.options.onToolApproval(toolCall.name, args, diff);
512
+ if (decision === "no") {
513
+ const denied = `Tool call "${toolCall.name}" was denied by the user.`;
514
+ this.options.onToolResult?.(toolCall.name, denied);
515
+ const deniedMsg: ChatCompletionMessageParam = {
516
+ role: "tool",
517
+ tool_call_id: toolCall.id,
518
+ content: denied,
519
+ };
520
+ this.messages.push(deniedMsg);
521
+ saveMessage(this.sessionId, deniedMsg);
522
+ continue;
523
+ }
524
+ if (decision === "always") {
525
+ this.alwaysApproved.add(toolCall.name);
526
+ }
527
+ }
528
+ }
529
+
530
+ const result = await executeTool(toolCall.name, args, this.cwd);
531
+ this.options.onToolResult?.(toolCall.name, result);
532
+
533
+ // Auto-commit after successful write_file
534
+ if (this.gitEnabled && this.autoCommitEnabled && toolCall.name === "write_file" && result.startsWith("✅")) {
535
+ const path = String(args.path ?? "unknown");
536
+ const committed = autoCommit(this.cwd, path, "write");
537
+ if (committed) {
538
+ this.options.onGitCommit?.(`write ${path}`);
539
+ }
540
+ }
541
+
542
+ const toolMsg: ChatCompletionMessageParam = {
543
+ role: "tool",
544
+ tool_call_id: toolCall.id,
545
+ content: result,
546
+ };
547
+ this.messages.push(toolMsg);
548
+ saveMessage(this.sessionId, toolMsg);
549
+ }
550
+ }
551
+
552
+ return "Max iterations reached. The agent may be stuck in a loop.";
553
+ }
554
+
265
555
  /**
266
556
  * Switch to a different model mid-session
267
557
  */
@@ -315,6 +605,90 @@ export class CodingAgent {
315
605
  return Math.ceil(chars / 4);
316
606
  }
317
607
 
608
+ /**
609
+ * Check if context needs compression and compress if threshold exceeded
610
+ */
611
+ private async maybeCompressContext(): Promise<void> {
612
+ const currentTokens = this.estimateTokens();
613
+ if (currentTokens < this.compressionThreshold) return;
614
+
615
+ // Keep: system prompt (index 0) + last 10 messages
616
+ const keepCount = 10;
617
+ if (this.messages.length <= keepCount + 1) return; // Not enough to compress
618
+
619
+ const systemMsg = this.messages[0];
620
+ const middleMessages = this.messages.slice(1, this.messages.length - keepCount);
621
+ const recentMessages = this.messages.slice(this.messages.length - keepCount);
622
+
623
+ if (middleMessages.length === 0) return;
624
+
625
+ // Build a summary of the middle messages
626
+ const summaryParts: string[] = [];
627
+ for (const msg of middleMessages) {
628
+ if (msg.role === "user" && typeof msg.content === "string") {
629
+ summaryParts.push(`User: ${msg.content.slice(0, 200)}`);
630
+ } else if (msg.role === "assistant" && typeof msg.content === "string" && msg.content) {
631
+ summaryParts.push(`Assistant: ${msg.content.slice(0, 200)}`);
632
+ } else if (msg.role === "tool") {
633
+ // Skip tool messages in summary to save tokens
634
+ }
635
+ }
636
+
637
+ // Use the active model to summarize
638
+ const summaryPrompt = `Summarize this conversation history in 2-3 concise paragraphs. Focus on: what was discussed, what files were modified, what decisions were made, and any important context for continuing the conversation.\n\n${summaryParts.join("\n")}`;
639
+
640
+ try {
641
+ let summary: string;
642
+ if (this.providerType === "anthropic" && this.anthropicClient) {
643
+ const response = await this.anthropicClient.messages.create({
644
+ model: this.model,
645
+ max_tokens: 500,
646
+ messages: [{ role: "user", content: summaryPrompt }],
647
+ });
648
+ summary = response.content
649
+ .filter((b): b is Anthropic.TextBlock => b.type === "text")
650
+ .map((b) => b.text)
651
+ .join("");
652
+ } else {
653
+ const response = await this.client.chat.completions.create({
654
+ model: this.model,
655
+ max_tokens: 500,
656
+ messages: [{ role: "user", content: summaryPrompt }],
657
+ });
658
+ summary = response.choices[0]?.message?.content ?? "Previous conversation context.";
659
+ }
660
+
661
+ const compressedMsg: ChatCompletionMessageParam = {
662
+ role: "assistant",
663
+ content: `[Context compressed: ${summary}]`,
664
+ };
665
+
666
+ const oldTokens = currentTokens;
667
+ this.messages = [systemMsg, compressedMsg, ...recentMessages];
668
+ const newTokens = this.estimateTokens();
669
+
670
+ this.options.onContextCompressed?.(oldTokens, newTokens);
671
+ } catch {
672
+ // If summarization fails, just truncate without summary
673
+ const compressedMsg: ChatCompletionMessageParam = {
674
+ role: "assistant",
675
+ content: "[Context compressed: Earlier conversation history was removed to stay within token limits.]",
676
+ };
677
+ const oldTokens = currentTokens;
678
+ this.messages = [systemMsg, compressedMsg, ...recentMessages];
679
+ const newTokens = this.estimateTokens();
680
+ this.options.onContextCompressed?.(oldTokens, newTokens);
681
+ }
682
+ }
683
+
684
+ getCostInfo(): { promptTokens: number; completionTokens: number; totalCost: number } {
685
+ return {
686
+ promptTokens: this.totalPromptTokens,
687
+ completionTokens: this.totalCompletionTokens,
688
+ totalCost: this.totalCost,
689
+ };
690
+ }
691
+
318
692
  reset(): void {
319
693
  const systemMsg = this.messages[0];
320
694
  this.messages = [systemMsg];
package/src/config.ts CHANGED
@@ -7,6 +7,7 @@ export interface ProviderConfig {
7
7
  baseUrl: string;
8
8
  apiKey: string;
9
9
  model: string;
10
+ type?: "openai" | "anthropic";
10
11
  }
11
12
 
12
13
  export interface ProviderProfile extends ProviderConfig {
@@ -20,6 +21,7 @@ export interface CodemaxxingConfig {
20
21
  autoApprove: boolean;
21
22
  contextFiles: number;
22
23
  maxTokens: number;
24
+ contextCompressionThreshold?: number;
23
25
  };
24
26
  }
25
27
 
@@ -135,6 +137,14 @@ export function applyOverrides(config: CodemaxxingConfig, args: CLIArgs): Codema
135
137
  if (args.provider && config.providers?.[args.provider]) {
136
138
  const profile = config.providers[args.provider];
137
139
  result.provider = { ...profile };
140
+ // Also check auth store for this provider
141
+ const authCred = getCredential(args.provider);
142
+ if (authCred) {
143
+ result.provider.baseUrl = authCred.baseUrl;
144
+ result.provider.apiKey = authCred.apiKey;
145
+ }
146
+ // Detect provider type
147
+ result.provider.type = detectProviderType(args.provider, result.provider.baseUrl);
138
148
  }
139
149
 
140
150
  // CLI flags override everything
@@ -142,6 +152,11 @@ export function applyOverrides(config: CodemaxxingConfig, args: CLIArgs): Codema
142
152
  if (args.apiKey) result.provider.apiKey = args.apiKey;
143
153
  if (args.baseUrl) result.provider.baseUrl = args.baseUrl;
144
154
 
155
+ // Auto-detect type from baseUrl if not set
156
+ if (!result.provider.type && result.provider.baseUrl) {
157
+ result.provider.type = detectProviderType(args.provider || "", result.provider.baseUrl);
158
+ }
159
+
145
160
  return result;
146
161
  }
147
162
 
@@ -218,7 +233,7 @@ export async function listModels(baseUrl: string, apiKey: string): Promise<strin
218
233
  export function resolveProvider(
219
234
  providerId: string,
220
235
  cliArgs: CLIArgs
221
- ): { baseUrl: string; apiKey: string; model: string } | null {
236
+ ): ProviderConfig | null {
222
237
  // Check auth store first
223
238
  const authCred = getCredential(providerId);
224
239
  if (authCred) {
@@ -226,6 +241,7 @@ export function resolveProvider(
226
241
  baseUrl: authCred.baseUrl,
227
242
  apiKey: authCred.apiKey,
228
243
  model: cliArgs.model || "auto",
244
+ type: detectProviderType(providerId, authCred.baseUrl),
229
245
  };
230
246
  }
231
247
 
@@ -237,8 +253,19 @@ export function resolveProvider(
237
253
  baseUrl: provider.baseUrl,
238
254
  apiKey: cliArgs.apiKey || provider.apiKey,
239
255
  model: cliArgs.model || provider.model,
256
+ type: detectProviderType(providerId, provider.baseUrl),
240
257
  };
241
258
  }
242
259
 
243
260
  return null;
244
261
  }
262
+
263
+ /**
264
+ * Detect provider type from ID or base URL
265
+ */
266
+ function detectProviderType(providerId: string, baseUrl: string): "openai" | "anthropic" {
267
+ if (providerId === "anthropic" || baseUrl.includes("anthropic.com")) {
268
+ return "anthropic";
269
+ }
270
+ return "openai";
271
+ }