diff --git a/apps/tui/src/components/ServerScreen.tsx b/apps/tui/src/components/ServerScreen.tsx index 91d71a2e..1b54ccac 100644 --- a/apps/tui/src/components/ServerScreen.tsx +++ b/apps/tui/src/components/ServerScreen.tsx @@ -1,5 +1,5 @@ -import { NETWORK } from "@ccflare/core"; import { Config } from "@ccflare/config"; +import { NETWORK } from "@ccflare/core"; import { Box, Text, useInput } from "ink"; interface ServerScreenProps { diff --git a/apps/tui/src/main.ts b/apps/tui/src/main.ts index 4c7c89ea..35c0ff62 100644 --- a/apps/tui/src/main.ts +++ b/apps/tui/src/main.ts @@ -189,6 +189,7 @@ Examples: "opus-4": CLAUDE_MODEL_IDS.OPUS_4, "sonnet-4": CLAUDE_MODEL_IDS.SONNET_4, "opus-4.1": CLAUDE_MODEL_IDS.OPUS_4_1, + "sonnet-4.5": CLAUDE_MODEL_IDS.SONNET_4_5, }; const fullModel = modelMap[parsed.setModel]; diff --git a/packages/core/src/models.ts b/packages/core/src/models.ts index 61d402d4..8c242593 100644 --- a/packages/core/src/models.ts +++ b/packages/core/src/models.ts @@ -11,6 +11,7 @@ export const CLAUDE_MODEL_IDS = { // Claude 4 models SONNET_4: "claude-sonnet-4-20250514", + SONNET_4_5: "claude-sonnet-4-5-20250929", OPUS_4: "claude-opus-4-20250514", OPUS_4_1: "claude-opus-4-1-20250805", @@ -24,6 +25,7 @@ export const MODEL_DISPLAY_NAMES: Record = { [CLAUDE_MODEL_IDS.HAIKU_3_5]: "Claude Haiku 3.5", [CLAUDE_MODEL_IDS.SONNET_3_5]: "Claude Sonnet 3.5 v2", [CLAUDE_MODEL_IDS.SONNET_4]: "Claude Sonnet 4", + [CLAUDE_MODEL_IDS.SONNET_4_5]: "Claude Sonnet 4.5", [CLAUDE_MODEL_IDS.OPUS_4]: "Claude Opus 4", [CLAUDE_MODEL_IDS.OPUS_4_1]: "Claude Opus 4.1", [CLAUDE_MODEL_IDS.OPUS_3]: "Claude Opus 3", @@ -35,6 +37,7 @@ export const MODEL_SHORT_NAMES: Record = { [CLAUDE_MODEL_IDS.HAIKU_3_5]: "claude-3.5-haiku", [CLAUDE_MODEL_IDS.SONNET_3_5]: "claude-3.5-sonnet", [CLAUDE_MODEL_IDS.SONNET_4]: "claude-sonnet-4", + [CLAUDE_MODEL_IDS.SONNET_4_5]: "claude-sonnet-4.5", [CLAUDE_MODEL_IDS.OPUS_4]: "claude-opus-4", [CLAUDE_MODEL_IDS.OPUS_4_1]: "claude-opus-4.1", [CLAUDE_MODEL_IDS.OPUS_3]: "claude-3-opus", diff --git a/packages/core/src/pricing.ts b/packages/core/src/pricing.ts index cfe7f75b..70171cf7 100644 --- a/packages/core/src/pricing.ts +++ b/packages/core/src/pricing.ts @@ -66,6 +66,16 @@ const BUNDLED_PRICING: ApiResponse = { cache_write: 3.75, }, }, + [CLAUDE_MODEL_IDS.SONNET_4_5]: { + id: CLAUDE_MODEL_IDS.SONNET_4_5, + name: MODEL_DISPLAY_NAMES[CLAUDE_MODEL_IDS.SONNET_4_5], + cost: { + input: 3, + output: 15, + cache_read: 0.3, + cache_write: 3.75, + }, + }, [CLAUDE_MODEL_IDS.OPUS_4]: { id: CLAUDE_MODEL_IDS.OPUS_4, name: MODEL_DISPLAY_NAMES[CLAUDE_MODEL_IDS.OPUS_4], diff --git a/packages/dashboard-web/src/components/charts/ModelPerformanceComparison.tsx b/packages/dashboard-web/src/components/charts/ModelPerformanceComparison.tsx index 03a49156..5400b0f4 100644 --- a/packages/dashboard-web/src/components/charts/ModelPerformanceComparison.tsx +++ b/packages/dashboard-web/src/components/charts/ModelPerformanceComparison.tsx @@ -40,6 +40,9 @@ const MODEL_COLORS: Record = { "claude-3.5-haiku": COLORS.success, "claude-3-opus": COLORS.blue, "claude-opus-4": COLORS.pink, + "claude-opus-4.1": COLORS.indigo, + "claude-sonnet-4": COLORS.cyan, + "claude-sonnet-4.5": COLORS.purple, }; function getModelColor(model: string): string { diff --git a/packages/dashboard-web/src/components/charts/ModelTokenSpeedChart.tsx b/packages/dashboard-web/src/components/charts/ModelTokenSpeedChart.tsx index 4a68622f..9fec577c 100644 --- a/packages/dashboard-web/src/components/charts/ModelTokenSpeedChart.tsx +++ b/packages/dashboard-web/src/components/charts/ModelTokenSpeedChart.tsx @@ -34,7 +34,9 @@ const MODEL_COLORS: Record = { "claude-3.5-haiku": COLORS.success, "claude-3-opus": COLORS.blue, "claude-opus-4": COLORS.pink, - // Add more models as needed + "claude-opus-4.1": COLORS.indigo, + "claude-sonnet-4": COLORS.cyan, + "claude-sonnet-4.5": COLORS.purple, }; function getModelColor(model: string): string { diff --git a/packages/dashboard-web/src/components/charts/MultiModelChart.tsx b/packages/dashboard-web/src/components/charts/MultiModelChart.tsx index 9d384c7b..a393b19d 100644 --- a/packages/dashboard-web/src/components/charts/MultiModelChart.tsx +++ b/packages/dashboard-web/src/components/charts/MultiModelChart.tsx @@ -53,6 +53,9 @@ const MODEL_COLORS: Record = { "claude-3.5-haiku": COLORS.success, "claude-3-opus": COLORS.blue, "claude-opus-4": COLORS.pink, + "claude-opus-4.1": COLORS.indigo, + "claude-sonnet-4": COLORS.cyan, + "claude-sonnet-4.5": COLORS.purple, }; function getModelColor(model: string, index: number): string { diff --git a/packages/dashboard-web/src/components/conversation/Message.tsx b/packages/dashboard-web/src/components/conversation/Message.tsx index 0e9e919f..5db16d93 100644 --- a/packages/dashboard-web/src/components/conversation/Message.tsx +++ b/packages/dashboard-web/src/components/conversation/Message.tsx @@ -43,7 +43,8 @@ function MessageComponent({ ); const thinkingText = typeof thinkingBlock?.thinking === "string" ? thinkingBlock.thinking : ""; - const hasThinking = thinkingText && cleanLineNumbers(thinkingText).trim().length > 0; + const hasThinking = + thinkingText && cleanLineNumbers(thinkingText).trim().length > 0; const cleanedContent = typeof content === "string" ? cleanLineNumbers(content).trim() : ""; const hasTools = tools?.length || 0; diff --git a/packages/providers/src/providers/anthropic/provider.ts b/packages/providers/src/providers/anthropic/provider.ts index b4ee000d..0b6842b9 100644 --- a/packages/providers/src/providers/anthropic/provider.ts +++ b/packages/providers/src/providers/anthropic/provider.ts @@ -116,6 +116,9 @@ export class AnthropicProvider extends BaseProvider { newHeaders.set("x-api-key", apiKey); } + // Add anthropic-beta header for extended context support + newHeaders.set("anthropic-beta", "context-1m-2025-08-07"); + // Remove host header newHeaders.delete("host"); diff --git a/packages/types/src/agent.ts b/packages/types/src/agent.ts index 4cfa135b..5442a43d 100644 --- a/packages/types/src/agent.ts +++ b/packages/types/src/agent.ts @@ -42,6 +42,7 @@ export const ALLOWED_MODELS = [ CLAUDE_MODEL_IDS.OPUS_4, CLAUDE_MODEL_IDS.OPUS_4_1, CLAUDE_MODEL_IDS.SONNET_4, + CLAUDE_MODEL_IDS.SONNET_4_5, ] as const; export type AllowedModel = (typeof ALLOWED_MODELS)[number]; diff --git a/packages/ui-common/src/parsers/parse-conversation.ts b/packages/ui-common/src/parsers/parse-conversation.ts index 2ff17885..0c809599 100644 --- a/packages/ui-common/src/parsers/parse-conversation.ts +++ b/packages/ui-common/src/parsers/parse-conversation.ts @@ -45,7 +45,7 @@ export function parseRequestMessages(body: string | null): MessageData[] { for (const item of msg.content) { if (item.type === "text") { - // Filter out system reminders + // Filter out system reminders let text = normalizeText(item.text || ""); if (text.includes("")) { text = text @@ -72,11 +72,18 @@ export function parseRequestMessages(body: string | null): MessageData[] { name: item.name, input: item.input, }); - } else if (item.type === "tool_result") { + } else if (item.type === "tool_result") { const resultContent = Array.isArray((item as any).content) - ? ((item as any).content as Array<{ type: string; text?: string }>) - .map((c) => normalizeText(typeof c.text === "string" ? c.text : "")) - .join("") + ? ( + (item as any).content as Array<{ + type: string; + text?: string; + }> + ) + .map((c) => + normalizeText(typeof c.text === "string" ? c.text : ""), + ) + .join("") : typeof (item as any).content === "string" ? normalizeText((item as any).content as string) : ""; @@ -89,7 +96,7 @@ export function parseRequestMessages(body: string | null): MessageData[] { tool_use_id: (item as any).tool_use_id, content: resultContent, }); - } else if (item.type === "thinking") { + } else if (item.type === "thinking") { const thinking = normalizeText((item as any).thinking || ""); if (thinking) { message.contentBlocks?.push({ @@ -221,54 +228,61 @@ export function parseAssistantMessage(body: string | null): MessageData | null { if (!isStreaming) { try { const parsed = JSON.parse(body); - if (parsed.content) { - if (typeof parsed.content === "string") { - currentContent = normalizeText(parsed.content); - } else if (Array.isArray(parsed.content)) { - for (const item of parsed.content) { - if (item.type === "text" && item.text) { - const norm = normalizeText(item.text); - currentContent += norm; - message.contentBlocks?.push({ - type: ContentBlockType.Text, - text: norm, - }); - } else if (item.type === "tool_use") { - message.tools?.push({ - id: item.id, - name: item.name || "unknown", - input: item.input, - }); - message.contentBlocks?.push({ - type: ContentBlockType.ToolUse, - ...item, - }); - } else if (item.type === "thinking") { - const thinking = normalizeText((item as any).thinking || ""); - if (thinking) { - message.contentBlocks?.push({ - type: ContentBlockType.Thinking, - thinking, - }); - } - } else if (item.type === "tool_result") { - const resultContent = Array.isArray((item as any).content) - ? ((item as any).content as Array<{ type: string; text?: string }>) - .map((c) => normalizeText(typeof c.text === "string" ? c.text : "")) - .join("") - : typeof (item as any).content === "string" - ? normalizeText((item as any).content as string) - : ""; - message.toolResults?.push({ - tool_use_id: (item as any).tool_use_id || "", - content: resultContent, - }); + if (parsed.content) { + if (typeof parsed.content === "string") { + currentContent = normalizeText(parsed.content); + } else if (Array.isArray(parsed.content)) { + for (const item of parsed.content) { + if (item.type === "text" && item.text) { + const norm = normalizeText(item.text); + currentContent += norm; + message.contentBlocks?.push({ + type: ContentBlockType.Text, + text: norm, + }); + } else if (item.type === "tool_use") { + message.tools?.push({ + id: item.id, + name: item.name || "unknown", + input: item.input, + }); + message.contentBlocks?.push({ + type: ContentBlockType.ToolUse, + ...item, + }); + } else if (item.type === "thinking") { + const thinking = normalizeText((item as any).thinking || ""); + if (thinking) { message.contentBlocks?.push({ - type: ContentBlockType.ToolResult, - tool_use_id: (item as any).tool_use_id, - content: resultContent, + type: ContentBlockType.Thinking, + thinking, }); } + } else if (item.type === "tool_result") { + const resultContent = Array.isArray((item as any).content) + ? ( + (item as any).content as Array<{ + type: string; + text?: string; + }> + ) + .map((c) => + normalizeText(typeof c.text === "string" ? c.text : ""), + ) + .join("") + : typeof (item as any).content === "string" + ? normalizeText((item as any).content as string) + : ""; + message.toolResults?.push({ + tool_use_id: (item as any).tool_use_id || "", + content: resultContent, + }); + message.contentBlocks?.push({ + type: ContentBlockType.ToolResult, + tool_use_id: (item as any).tool_use_id, + content: resultContent, + }); + } } } } diff --git a/packages/ui-common/src/utils/normalize-text.ts b/packages/ui-common/src/utils/normalize-text.ts index b3a4c21a..d5e000fc 100644 --- a/packages/ui-common/src/utils/normalize-text.ts +++ b/packages/ui-common/src/utils/normalize-text.ts @@ -3,47 +3,49 @@ * and optionally strip a single pair of wrapping quotes. */ export function normalizeText(input: unknown): string { - let s = typeof input === "string" ? input : ""; - if (!s) return ""; + let s = typeof input === "string" ? input : ""; + if (!s) return ""; - // 1) If it's a JSON-encoded string (e.g., "...\n..."), try to parse directly - if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { - try { - s = JSON.parse(s); - } catch { - // If JSON.parse fails, fall back to manual unquoting - s = s.slice(1, -1); - } - } else if (/\\[nrt"\\]/.test(s)) { - // 2) If it contains escaped sequences, decode them via JSON.parse wrapper - try { - const literal = - '"' + - s - .replace(/\\/g, "\\\\") - .replace(/\n/g, "\\n") - .replace(/\r/g, "\\r") - .replace(/\t/g, "\\t") - .replace(/"/g, "\\\"") + - '"'; - s = JSON.parse(literal); - } catch { - // Ignore if decoding fails - } - } + // 1) If it's a JSON-encoded string (e.g., "...\n..."), try to parse directly + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + try { + s = JSON.parse(s); + } catch { + // If JSON.parse fails, fall back to manual unquoting + s = s.slice(1, -1); + } + } else if (/\\[nrt"\\]/.test(s)) { + // 2) If it contains escaped sequences, decode them via JSON.parse wrapper + try { + const literal = + '"' + + s + .replace(/\\/g, "\\\\") + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/\t/g, "\\t") + .replace(/"/g, '\\"') + + '"'; + s = JSON.parse(literal); + } catch { + // Ignore if decoding fails + } + } - // 3) Heuristic: repair mojibake (UTF-8 mis-decoded as Latin-1) - if (/[ÃÂâ]/.test(s)) { - try { - const bytes = new Uint8Array(Array.from(s, (ch) => ch.charCodeAt(0) & 0xff)); - const recoded = new TextDecoder("utf-8", { fatal: false }).decode(bytes); - if (recoded && recoded !== s) { - s = recoded; - } - } catch { - // Ignore decoding errors - } - } + // 3) Heuristic: repair mojibake (UTF-8 mis-decoded as Latin-1) + if (/[ÃÂâ]/.test(s)) { + try { + const bytes = new Uint8Array( + Array.from(s, (ch) => ch.charCodeAt(0) & 0xff), + ); + const recoded = new TextDecoder("utf-8", { fatal: false }).decode(bytes); + if (recoded && recoded !== s) { + s = recoded; + } + } catch { + // Ignore decoding errors + } + } - return s; + return s; } diff --git a/packages/ui-constants/src/index.ts b/packages/ui-constants/src/index.ts index e0531d1b..7c267e27 100644 --- a/packages/ui-constants/src/index.ts +++ b/packages/ui-constants/src/index.ts @@ -7,6 +7,8 @@ export const COLORS = { blue: "#3b82f6", purple: "#8b5cf6", pink: "#ec4899", + indigo: "#6366f1", + cyan: "#06b6d4", } as const; // Chart color sequence for multi-series charts