Fix provider context budgeting and ChatGPT subscription limits

This commit is contained in:
2026-09-13 23:01:03 -05:00
parent ba56d83fc6
commit 6f33c97763
9 changed files with 302 additions and 81 deletions
+67 -31
View File
@@ -81,11 +81,11 @@ use crate::ai::provider::ProviderConfig;
#[cfg(not(target_family = "wasm"))]
use crate::ai::remote_logging::{self, RemoteLogLevel, RemoteLogRecord};
use crate::ai::runtime::{
prepare_provider_run, provider_runtime_for_request, OrchestrationModelOption,
PreparedProviderRun, ProviderActionContext, ProviderRunBlock, ProviderRunCoordinator,
ProviderRunProfile, ProviderRunProjection, ProviderRunResponseProjector,
ProviderToolExecutionRef, ProviderToolLifecycleOutcome, RuntimeResponseConfig,
BASE_PROVIDER_PROFILE, CLI_MONITOR_PROVIDER_PROFILE,
prepare_provider_run, provider_context_window_tokens, provider_runtime_for_request,
OrchestrationModelOption, PreparedProviderRun, ProviderActionContext, ProviderRunBlock,
ProviderRunCoordinator, ProviderRunProfile, ProviderRunProjection,
ProviderRunResponseProjector, ProviderToolExecutionRef, ProviderToolLifecycleOutcome,
RuntimeResponseConfig, BASE_PROVIDER_PROFILE, CLI_MONITOR_PROVIDER_PROFILE,
};
use crate::ai::AIRequestUsageModel;
use crate::cloud_object::model::persistence::CloudModel;
@@ -112,7 +112,7 @@ use crate::workspaces::user_workspaces::UserWorkspaces;
const PROGRESSIVE_SUMMARY_TRIGGER_USAGE: f32 = 0.70;
const PROGRESSIVE_SUMMARY_RETAINED_USAGE: f32 = 0.50;
const PROGRESSIVE_SUMMARY_MIN_RECENT_MESSAGES: usize = 20;
const PROGRESSIVE_SUMMARY_MIN_RECENT_MESSAGES: usize = 1;
const PROGRESSIVE_SUMMARY_OUTPUT_TOKENS: u32 = 8_000;
const PROGRESSIVE_SUMMARY_CONTEXT_SAFETY_TOKENS: u32 = 2_000;
const PROGRESSIVE_SUMMARY_START_TIMEOUT: Duration = Duration::from_secs(120);
@@ -199,7 +199,7 @@ fn progressive_summary_candidate(
}
fn estimated_text_tokens(text: &str) -> u32 {
u32::try_from(text.chars().count().div_ceil(4)).unwrap_or(u32::MAX)
u32::try_from(text.len().div_ceil(3)).unwrap_or(u32::MAX)
}
fn estimated_message_tokens(message: &ConversationMessage) -> u32 {
@@ -259,6 +259,8 @@ fn progressive_summary_split_point(
for index in (0..messages.len()).rev() {
let message_tokens = estimated_message_tokens(&messages[index]);
let retained_count = messages.len() - index;
// Keep the latest message, then retain only as much history as fits.
// A fixed message-count floor can retain many oversized file reads.
if retained_count > PROGRESSIVE_SUMMARY_MIN_RECENT_MESSAGES
&& retained_tokens.saturating_add(message_tokens) > retained_token_budget
{
@@ -278,6 +280,17 @@ fn progressive_summary_split_point(
split_point -= 1;
}
// A single large tool batch can exceed the entire retained budget. Summarize
// the complete batch rather than keeping an oversized call/result pair forever.
// Its original results remain available in the tool-result archive.
if messages
.last()
.is_some_and(|message| content_contains_tool_result(&message.content))
&& estimated_history_tokens(&messages[split_point..]) > retained_token_budget
{
return messages.len();
}
split_point
}
@@ -5678,13 +5691,19 @@ impl BlocklistAIController {
self.fail_restored_provider_run(conversation_id, error, ctx);
return;
}
let base_provider_config =
ResponseStream::resolve_provider_config(snapshot.base_request.model.as_str(), ctx);
// A restored checkpoint may contain an older, larger catalog budget.
// Reconcile it before any resumed model call and persist the corrected limit.
snapshot.response_config.max_context_tokens = provider_context_window_tokens(
&base_provider_config,
snapshot.base_request.model.as_str(),
snapshot.response_config.max_context_tokens,
);
if let Err(error) = self.persist_provider_run_snapshot(conversation_id, &snapshot, ctx) {
self.fail_restored_provider_run(conversation_id, error, ctx);
return;
}
let base_provider_config =
ResponseStream::resolve_provider_config(snapshot.base_request.model.as_str(), ctx);
let cli_provider_config = snapshot
.cli_monitor_request
.as_ref()
@@ -6583,6 +6602,7 @@ impl BlocklistAIController {
fn begin_active_provider_progressive_summary(
&mut self,
conversation_id: AIConversationId,
force: bool,
ctx: &mut ModelContext<Self>,
) -> bool {
if self
@@ -6623,28 +6643,38 @@ impl BlocklistAIController {
return false;
};
(
conversation
.current_context_tokens()
.min(active_context_limit),
conversation.current_context_tokens(),
conversation.has_pending_progressive_summary(),
)
};
let transcript = active_run.coordinator.run().transcript();
let (estimated_context_tokens, persistent_context_tokens) = active_run
.coordinator
.estimated_context_tokens()
.unwrap_or_else(|| {
(
current_context_tokens,
current_context_tokens.saturating_sub(estimated_history_tokens(transcript)),
)
});
let current_context_tokens = current_context_tokens.max(estimated_context_tokens);
// Trigger and retain against the space history can actually use. Fixed
// prompts and a large output reservation cannot be reduced by summarizing.
let history_capacity = active_context_limit.saturating_sub(persistent_context_tokens);
let current_history_tokens =
current_context_tokens.saturating_sub(persistent_context_tokens);
if summary_pending
|| last_failure_context_tokens == Some(current_context_tokens)
|| current_context_tokens as f32 / (active_context_limit as f32)
< PROGRESSIVE_SUMMARY_TRIGGER_USAGE
|| (!force
&& current_history_tokens as f32 / (history_capacity.max(1) as f32)
< PROGRESSIVE_SUMMARY_TRIGGER_USAGE)
{
return false;
}
let transcript = active_run.coordinator.run().transcript();
let transcript_tokens = estimated_history_tokens(transcript);
let persistent_context_tokens = current_context_tokens.saturating_sub(transcript_tokens);
let retained_total_budget =
(active_context_limit as f32 * PROGRESSIVE_SUMMARY_RETAINED_USAGE) as u32;
let retained_history_budget = retained_total_budget
.saturating_sub(persistent_context_tokens)
.saturating_sub(PROGRESSIVE_SUMMARY_OUTPUT_TOKENS);
let retained_history_budget =
(history_capacity as f32 * PROGRESSIVE_SUMMARY_RETAINED_USAGE) as u32;
let retained_history_budget =
retained_history_budget.saturating_sub(PROGRESSIVE_SUMMARY_OUTPUT_TOKENS);
let split_point = progressive_summary_split_point(transcript, retained_history_budget);
if split_point == 0 {
return false;
@@ -6851,6 +6881,16 @@ impl BlocklistAIController {
let mut summary_applied = false;
let mut recorded_usage = None;
match result {
Ok((summary, usage, summarizer_model_id))
if estimated_history_tokens(&progressive_summary_messages(summary.clone()))
>= estimated_history_tokens(&plan.summarized_prefix) =>
{
log::warn!(
"[progressive-summary] Summary did not reduce context for conversation {:?}",
conversation_id,
);
recorded_usage = Some((usage, summarizer_model_id));
}
Ok((summary, usage, summarizer_model_id)) => {
let Some(run) = self
.active_provider_runs
@@ -6925,10 +6965,6 @@ impl BlocklistAIController {
}
}
let failure_context_tokens = BlocklistAIHistoryModel::as_ref(ctx)
.conversation(&conversation_id)
.map(AIConversation::current_context_tokens)
.unwrap_or(plan.trigger_context_tokens);
if let Some(run) = self
.active_provider_runs
.get_mut(&conversation_id)
@@ -6938,7 +6974,7 @@ impl BlocklistAIController {
None
} else {
// Fail open and suppress another request at this unchanged context boundary.
Some(failure_context_tokens)
Some(plan.trigger_context_tokens)
};
}
if let Some((usage, summarizer_model_id)) = recorded_usage {
@@ -6998,7 +7034,7 @@ impl BlocklistAIController {
if self.apply_ready_active_provider_progressive_summary(conversation_id, ctx) {
return;
}
self.begin_active_provider_progressive_summary(conversation_id, ctx);
self.begin_active_provider_progressive_summary(conversation_id, false, ctx);
if matches!(
self.active_provider_progressive_summaries
.get(&conversation_id),
@@ -7471,7 +7507,7 @@ impl BlocklistAIController {
);
return;
}
if !self.begin_active_provider_progressive_summary(conversation_id, ctx) {
if !self.begin_active_provider_progressive_summary(conversation_id, true, ctx) {
self.fail_active_provider_run(
conversation_id,
"provider context overflow could not start compaction".to_string(),
+78
View File
@@ -3734,3 +3734,81 @@ fn optimistic_cli_subagent_completion_with_in_flight_stream_reports_success() {
});
});
}
#[test]
fn progressive_summary_compacts_short_histories_to_the_token_budget() {
let messages = vec![
ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text("large project context".repeat(1_000)),
},
ConversationMessage {
role: MessageRole::Assistant,
content: MessageContent::Text("Working on the requested change".to_string()),
},
ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text("Please continue".to_string()),
},
];
assert_eq!(super::progressive_summary_split_point(&messages, 100), 1);
}
#[test]
fn progressive_summary_includes_an_oversized_final_tool_batch() {
let messages = vec![
ConversationMessage {
role: MessageRole::Assistant,
content: MessageContent::ToolUse {
tool_use_id: "read".to_string(),
name: "read_file".to_string(),
input: serde_json::json!({"path": "large.rs"}),
},
},
ConversationMessage {
role: MessageRole::User,
content: MessageContent::ToolResult {
tool_use_id: "read".to_string(),
content: "file contents".repeat(1_000),
is_error: false,
},
},
];
assert_eq!(super::progressive_summary_split_point(&messages, 100), 2);
assert_eq!(
super::progressive_summary_split_point(&messages, 100_000),
0
);
}
#[test]
fn progressive_summary_keeps_retained_tool_results_paired() {
let messages = vec![
ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text("old project context".repeat(1_000)),
},
ConversationMessage {
role: MessageRole::Assistant,
content: MessageContent::ToolUse {
tool_use_id: "read".to_string(),
name: "read_file".to_string(),
input: serde_json::json!({"path": "small.rs"}),
},
},
ConversationMessage {
role: MessageRole::User,
content: MessageContent::ToolResult {
tool_use_id: "read".to_string(),
content: "file contents".to_string(),
is_error: false,
},
},
ConversationMessage {
role: MessageRole::Assistant,
content: MessageContent::Text("I found the issue".to_string()),
},
];
let budget = super::estimated_history_tokens(&messages[2..]);
assert_eq!(super::progressive_summary_split_point(&messages, budget), 1);
}