Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "codeg"
version = "0.21.7"
version = "0.21.8"
description = "Agent Code Generation App"
authors = ["feitao"]
edition = "2021"
Expand Down
8 changes: 4 additions & 4 deletions src-tauri/src/acp/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: &[],
Expand Down Expand Up @@ -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(
Expand Down
68 changes: 29 additions & 39 deletions src-tauri/src/parsers/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Regex> = 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
Expand Down Expand Up @@ -275,38 +266,34 @@ fn is_synthetic_assistant(value: &serde_json::Value) -> bool {
.unwrap_or(false)
}

fn parse_model_capacity_suffix(model: &str) -> Option<u64> {
let captures = model_capacity_suffix_regex().captures(model.trim())?;
let value = captures.get(1)?.as_str().parse::<f64>().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<u64> {
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);
}
Expand Down Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions src-tauri/src/parsers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Regex> = 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<u64> {
let captures = model_capacity_suffix_regex().captures(model.trim())?;
let value = captures.get(1)?.as_str().parse::<f64>().ok()?;
Expand Down Expand Up @@ -489,7 +506,15 @@ pub fn infer_context_window_max_tokens(model: Option<&str>) -> Option<u64> {
.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") {
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 3 additions & 3 deletions src/components/settings/acp-agent-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10693,7 +10693,7 @@ supports_websockets = true`}
event.target.value
)
}}
placeholder="claude-opus-4-8"
placeholder="claude-opus-5"
/>
</div>
<div className="space-y-1.5">
Expand Down Expand Up @@ -10750,7 +10750,7 @@ supports_websockets = true`}
event.target.value
)
}}
placeholder="claude-opus-4-8"
placeholder="claude-opus-5"
/>
</div>
</div>
Expand All @@ -10775,7 +10775,7 @@ supports_websockets = true`}
event.target.value
)
}}
placeholder="my-gateway/claude-opus-4-8"
placeholder="my-gateway/claude-opus-5"
/>
</div>
<div className="space-y-1.5">
Expand Down
6 changes: 3 additions & 3 deletions src/components/settings/add-model-provider-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ export function AddModelProviderDialog({
reasoning: e.target.value,
}))
}
placeholder="claude-opus-4-8"
placeholder="claude-opus-5"
/>
</div>
<div className="space-y-1.5">
Expand Down Expand Up @@ -295,7 +295,7 @@ export function AddModelProviderDialog({
opus: e.target.value,
}))
}
placeholder="claude-opus-4-8"
placeholder="claude-opus-5"
/>
</div>
<div className="space-y-1.5 md:col-span-2">
Expand All @@ -310,7 +310,7 @@ export function AddModelProviderDialog({
customOption: e.target.value,
}))
}
placeholder="my-gateway/claude-opus-4-8"
placeholder="my-gateway/claude-opus-5"
/>
</div>
<div className="space-y-1.5">
Expand Down
6 changes: 3 additions & 3 deletions src/components/settings/edit-model-provider-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ export function EditModelProviderDialog({
reasoning: e.target.value,
}))
}
placeholder="claude-opus-4-8"
placeholder="claude-opus-5"
/>
</div>
<div className="space-y-1.5">
Expand Down Expand Up @@ -314,7 +314,7 @@ export function EditModelProviderDialog({
opus: e.target.value,
}))
}
placeholder="claude-opus-4-8"
placeholder="claude-opus-5"
/>
</div>
<div className="space-y-1.5 md:col-span-2">
Expand All @@ -329,7 +329,7 @@ export function EditModelProviderDialog({
customOption: e.target.value,
}))
}
placeholder="my-gateway/claude-opus-4-8"
placeholder="my-gateway/claude-opus-5"
/>
</div>
<div className="space-y-1.5">
Expand Down
2 changes: 1 addition & 1 deletion src/components/settings/pi-config-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -911,7 +911,7 @@ export function PiConfigPanel({
<Input
value={model}
onChange={(event) => setModel(event.target.value)}
placeholder="claude-sonnet-4-20250514"
placeholder="claude-sonnet-5"
spellCheck={false}
disabled={savingCreds || loadingCreds}
/>
Expand Down
6 changes: 3 additions & 3 deletions src/lib/adapters/ai-elements-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading