diff --git a/package.json b/package.json index f6338f992..dc34c3358 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codeg", "private": true, - "version": "0.21.7", + "version": "0.21.8", "packageManager": "pnpm@11.9.0", "scripts": { "dev": "next dev --turbopack", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fac8069b3..ee01c432f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1014,7 +1014,7 @@ checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "codeg" -version = "0.21.7" +version = "0.21.8" dependencies = [ "aes-gcm", "agent-client-protocol-schema", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d0db85d19..a68821f79 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codeg" -version = "0.21.7" +version = "0.21.8" description = "Agent Code Generation App" authors = ["feitao"] edition = "2021" diff --git a/src-tauri/src/acp/registry.rs b/src-tauri/src/acp/registry.rs index 353a6c07b..f22f84509 100644 --- a/src-tauri/src/acp/registry.rs +++ b/src-tauri/src/acp/registry.rs @@ -194,8 +194,8 @@ pub fn get_agent_meta(agent_type: AgentType) -> AcpAgentMeta { name: "Claude Code", description: "ACP wrapper for Anthropic's Claude", distribution: AgentDistribution::Npx { - version: "0.61.0", - package: "@agentclientprotocol/claude-agent-acp@0.61.0", + version: "0.62.0", + package: "@agentclientprotocol/claude-agent-acp@0.62.0", cmd: "claude-agent-acp", args: &[], env: &[], @@ -628,8 +628,8 @@ mod tests { fn registry_pins_current_acp_agent_versions() { assert_npx_version( AgentType::ClaudeCode, - "0.61.0", - "@agentclientprotocol/claude-agent-acp@0.61.0", + "0.62.0", + "@agentclientprotocol/claude-agent-acp@0.62.0", Some("22.0.0"), ); assert_npx_version( diff --git a/src-tauri/src/parsers/claude.rs b/src-tauri/src/parsers/claude.rs index bc1699430..5b0469aac 100644 --- a/src-tauri/src/parsers/claude.rs +++ b/src-tauri/src/parsers/claude.rs @@ -34,15 +34,6 @@ fn system_tag_regex() -> &'static Regex { }) } -/// Regex that matches an optional model capacity suffix like `[1M]` / `[500k]`. -fn model_capacity_suffix_regex() -> &'static Regex { - static RE: OnceLock = OnceLock::new(); - RE.get_or_init(|| { - Regex::new(r"(?i)\[\s*([0-9]+(?:\.[0-9]+)?)\s*([km])\s*\]\s*$") - .expect("valid model capacity regex") - }) -} - /// Sentinel prefixing the structured lifecycle payload the parser writes into /// an async-launch ack's `output_preview` (see /// `ClaudeRecordAccumulator::finalize_background_lifecycle`). The frontend @@ -275,38 +266,34 @@ fn is_synthetic_assistant(value: &serde_json::Value) -> bool { .unwrap_or(false) } -fn parse_model_capacity_suffix(model: &str) -> Option { - let captures = model_capacity_suffix_regex().captures(model.trim())?; - let value = captures.get(1)?.as_str().parse::().ok()?; - if !value.is_finite() || value <= 0.0 { - return None; - } - - let unit = captures - .get(2) - .map(|m| m.as_str().to_ascii_lowercase()) - .unwrap_or_default(); - let multiplier = match unit.as_str() { - "m" => 1_000_000.0, - "k" => 1_000.0, - _ => return None, - }; - - Some((value * multiplier) as u64) -} - +/// Context window to display for a *file-parsed* Claude Code session, which +/// carries no authoritative window of its own (live sessions get one from the +/// ACP `usage_update.size` and never reach here). +/// +/// Deliberately defaults higher than [`super::infer_context_window_max_tokens`] +/// does for the same `claude-*` model, because the two read different inputs: +/// Claude Code's transcripts record the *resolved* model with the 1M marker +/// stripped — a session launched as `claude-opus-5[1m]` is written to the +/// JSONL as plain `claude-opus-5` — so neither the bracket spelling nor the +/// `-1m` id spelling survives to be detected, and any per-session guess is +/// unavoidable. Assuming the 1M lane keeps the meter from over-reporting +/// pressure ~5x on the extended-context models most sessions run; the cost is +/// under-reporting for a session that really was on the 200K lane. Other +/// agents' transcripts keep the id verbatim, so that path can detect the lane +/// and defaults to 200K instead. Change one of these without the other and +/// they silently disagree again. fn claude_context_window_max_tokens_for_model(model: Option<&str>) -> Option { let model = model?.trim(); if model.is_empty() { return None; } - // If user/model config contains an explicit capacity suffix, prefer it. - if let Some(suffixed_limit) = parse_model_capacity_suffix(model) { + // A capacity suffix that did survive (hand-written config, non-CLI writer) + // is authoritative. + if let Some(suffixed_limit) = super::parse_model_capacity_suffix(model) { return Some(suffixed_limit); } - // Claude models default to 1M when no explicit capacity is provided. if model.to_ascii_lowercase().starts_with("claude") { return Some(1_000_000); } @@ -1996,24 +1983,27 @@ mod tests { } #[test] - fn parses_model_capacity_suffix() { + fn honours_surviving_capacity_suffix() { assert_eq!( - parse_model_capacity_suffix("claude-sonnet-4-6[1.5M]"), - Some(1_500_000) - ); - assert_eq!( - parse_model_capacity_suffix("claude-opus-4-6 [500k]"), + claude_context_window_max_tokens_for_model(Some("claude-opus-4-6 [500k]")), Some(500_000) ); - assert_eq!(parse_model_capacity_suffix("claude-sonnet-4-6"), None); } #[test] fn defaults_context_limit_for_claude_models() { + // Claude Code strips the 1M marker when it writes the transcript, so a + // bare id has to be assumed to be the extended lane here — unlike the + // shared inference other agents' parsers use. See the doc comment on + // `claude_context_window_max_tokens_for_model`. assert_eq!( claude_context_window_max_tokens_for_model(Some("claude-sonnet-4-6")), Some(1_000_000) ); + assert_eq!( + super::super::infer_context_window_max_tokens(Some("claude-sonnet-4-6")), + Some(200_000) + ); assert_eq!( claude_context_window_max_tokens_for_model(Some("custom-model-x")), None diff --git a/src-tauri/src/parsers/mod.rs b/src-tauri/src/parsers/mod.rs index d6ae02756..354156ef2 100644 --- a/src-tauri/src/parsers/mod.rs +++ b/src-tauri/src/parsers/mod.rs @@ -449,6 +449,23 @@ fn model_capacity_suffix_regex() -> &'static Regex { }) } +/// Matches the SDK's *id* spelling of Anthropic's 1M-context lane, where `1m` +/// is its own delimited token (`claude-opus-4-6-1m`). `\b1m\b` is the same +/// test claude-agent-acp's `inferContextWindowFromModel` applies, and it +/// deliberately does not match embedded runs like `10m`. +fn claude_one_million_id_regex() -> &'static Regex { + static RE: OnceLock = OnceLock::new(); + RE.get_or_init(|| Regex::new(r"(?i)\b1m\b").expect("valid claude 1m id regex")) +} + +/// Whether `model` names Anthropic's 1M-context lane through the SDK's id +/// spelling (`claude-opus-4-6-1m`). The CLI's *display* spelling +/// (`claude-sonnet-5[1m]`) carries the same meaning but is handled by +/// [`parse_model_capacity_suffix`], which reads the bracketed number directly. +fn is_claude_one_million_context_id(model: &str) -> bool { + claude_one_million_id_regex().is_match(model) +} + fn parse_model_capacity_suffix(model: &str) -> Option { let captures = model_capacity_suffix_regex().captures(model.trim())?; let value = captures.get(1)?.as_str().parse::().ok()?; @@ -489,7 +506,15 @@ pub fn infer_context_window_max_tokens(model: Option<&str>) -> Option { .trim() .to_ascii_lowercase(); + // Anthropic's default lane is 200K; the 1M lane is opt-in and shows up in + // the model id itself. Agents other than Claude Code record the id the way + // their backend named it, so the marker survives here — unlike Claude + // Code's own transcripts, where it is stripped (see + // `claude::claude_context_window_max_tokens_for_model`). if normalized.starts_with("claude") { + if is_claude_one_million_context_id(&normalized) { + return Some(1_000_000); + } return Some(200_000); } if normalized.starts_with("gemini") { @@ -1208,6 +1233,21 @@ mod tests { infer_context_window_max_tokens(Some("claude-sonnet-4-6 [1.5M]")), Some(1_500_000) ); + // The 1M lane also travels as a bare id token (`-1m`), which is how the + // SDK — and therefore every agent that records the resolved id — spells + // it. `\b1m\b` must not fire on an embedded run like `10m`. + assert_eq!( + infer_context_window_max_tokens(Some("claude-opus-4-6-1m")), + Some(1_000_000) + ); + assert_eq!( + infer_context_window_max_tokens(Some("my-gateway/claude-opus-4-6-1m")), + Some(1_000_000) + ); + assert_eq!( + infer_context_window_max_tokens(Some("claude-opus-4-6-10m-preview")), + Some(200_000) + ); // Grok context windows per x.ai docs: grok-4.5 = 500K, grok-4.3 / // grok-4.20 = 1M, the coding/build models = 256K (grok-code-fast-1 // despite "fast"), the general -fast variants = 2M, and any unknown diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5dee830e4..e7a47ebc4 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "codeg", - "version": "0.21.7", + "version": "0.21.8", "identifier": "app.codeg", "build": { "beforeDevCommand": "pnpm tauri:before-dev", diff --git a/src/components/settings/acp-agent-settings.tsx b/src/components/settings/acp-agent-settings.tsx index ce29468b0..b5d500f26 100644 --- a/src/components/settings/acp-agent-settings.tsx +++ b/src/components/settings/acp-agent-settings.tsx @@ -10693,7 +10693,7 @@ supports_websockets = true`} event.target.value ) }} - placeholder="claude-opus-4-8" + placeholder="claude-opus-5" />
@@ -10750,7 +10750,7 @@ supports_websockets = true`} event.target.value ) }} - placeholder="claude-opus-4-8" + placeholder="claude-opus-5" />
@@ -10775,7 +10775,7 @@ supports_websockets = true`} event.target.value ) }} - placeholder="my-gateway/claude-opus-4-8" + placeholder="my-gateway/claude-opus-5" />
diff --git a/src/components/settings/add-model-provider-dialog.tsx b/src/components/settings/add-model-provider-dialog.tsx index 0f4defdd8..ffd96f204 100644 --- a/src/components/settings/add-model-provider-dialog.tsx +++ b/src/components/settings/add-model-provider-dialog.tsx @@ -250,7 +250,7 @@ export function AddModelProviderDialog({ reasoning: e.target.value, })) } - placeholder="claude-opus-4-8" + placeholder="claude-opus-5" />
@@ -295,7 +295,7 @@ export function AddModelProviderDialog({ opus: e.target.value, })) } - placeholder="claude-opus-4-8" + placeholder="claude-opus-5" />
@@ -310,7 +310,7 @@ export function AddModelProviderDialog({ customOption: e.target.value, })) } - placeholder="my-gateway/claude-opus-4-8" + placeholder="my-gateway/claude-opus-5" />
diff --git a/src/components/settings/edit-model-provider-dialog.tsx b/src/components/settings/edit-model-provider-dialog.tsx index 459928da6..9798dd256 100644 --- a/src/components/settings/edit-model-provider-dialog.tsx +++ b/src/components/settings/edit-model-provider-dialog.tsx @@ -269,7 +269,7 @@ export function EditModelProviderDialog({ reasoning: e.target.value, })) } - placeholder="claude-opus-4-8" + placeholder="claude-opus-5" />
@@ -314,7 +314,7 @@ export function EditModelProviderDialog({ opus: e.target.value, })) } - placeholder="claude-opus-4-8" + placeholder="claude-opus-5" />
@@ -329,7 +329,7 @@ export function EditModelProviderDialog({ customOption: e.target.value, })) } - placeholder="my-gateway/claude-opus-4-8" + placeholder="my-gateway/claude-opus-5" />
diff --git a/src/components/settings/pi-config-panel.tsx b/src/components/settings/pi-config-panel.tsx index b48b9cfdf..5094fcbfc 100644 --- a/src/components/settings/pi-config-panel.tsx +++ b/src/components/settings/pi-config-panel.tsx @@ -911,7 +911,7 @@ export function PiConfigPanel({ setModel(event.target.value)} - placeholder="claude-sonnet-4-20250514" + placeholder="claude-sonnet-5" spellCheck={false} disabled={savingCreds || loadingCreds} /> diff --git a/src/lib/adapters/ai-elements-adapter.test.ts b/src/lib/adapters/ai-elements-adapter.test.ts index ef776ac44..c8c71bee3 100644 --- a/src/lib/adapters/ai-elements-adapter.test.ts +++ b/src/lib/adapters/ai-elements-adapter.test.ts @@ -170,9 +170,9 @@ describe("groupConsecutiveToolCalls", () => { state: "output-available", meta: { contextCompaction: true, tokensBefore: 51777, tokensAfter: 4616 }, } - expect( - groupConsecutiveToolCalls([compaction]).map((p) => p.type) - ).toEqual(["tool-call"]) + expect(groupConsecutiveToolCalls([compaction]).map((p) => p.type)).toEqual([ + "tool-call", + ]) // It also breaks a surrounding run instead of folding in. expect( groupConsecutiveToolCalls([poll("read"), compaction, poll("read")]).map(