fix(proxy): reduce unnecessary Copilot premium interaction consumption

- Fix request classification: treat messages containing tool_result as
  agent continuation instead of user-initiated, preventing false premium
  charges on every tool call
- Add subagent detection via __SUBAGENT_MARKER__ and metadata._agent_
  fallback, setting x-interaction-type=conversation-subagent
- Add deterministic x-interaction-id derived from session ID to group
  requests into a single billing interaction
- Add orphan tool_result sanitization to prevent upstream API errors
  that could cause retries and duplicate billing
- Reorder pipeline: classify (on original body) → sanitize → merge →
  warmup, ensuring classification sees raw tool_result semantics
- Enable warmup downgrade by default with gpt-5-mini model
- Enhance session ID extraction priority chain for Copilot cache keys
- Detect infinite whitespace bug in streaming tool call arguments
This commit is contained in:
Jason
2026-04-14 10:12:45 +08:00
parent c01338ac33
commit 0739b60341
7 changed files with 684 additions and 75 deletions
+42 -4
View File
@@ -81,13 +81,50 @@ pub fn transform_claude_request_for_api_format(
provider: &Provider,
api_format: &str,
) -> Result<serde_json::Value, ProxyError> {
match api_format {
"openai_responses" => {
let cache_key = provider
// Copilot 场景:优先从 metadata.user_id 提取 session ID 作为 cache key
// 格式: "uuid_sessionId" → 提取 "_" 后面的部分作为 session 标识
// 同一会话的请求共享 cache key,提升 Copilot 缓存命中率
let is_copilot = provider
.meta
.as_ref()
.and_then(|m| m.provider_type.as_deref())
== Some("github_copilot")
|| provider
.settings_config
.get("baseUrl")
.and_then(|v| v.as_str())
.is_some_and(|u| u.contains("githubcopilot.com"));
let session_cache_key: Option<String> = if is_copilot {
let metadata = body.get("metadata");
// Session 提取优先级(与 forwarder 和 session.rs 统一):
// 1. metadata.user_id 中的 _session_ 后缀
// 2. metadata.session_id(直接字段)
metadata
.and_then(|m| m.get("user_id"))
.and_then(|v| v.as_str())
.and_then(super::super::session::parse_session_from_user_id)
.or_else(|| {
metadata
.and_then(|m| m.get("session_id"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
})
} else {
None
};
let cache_key = session_cache_key
.as_deref()
.or_else(|| {
provider
.meta
.as_ref()
.and_then(|m| m.prompt_cache_key.as_deref())
.unwrap_or(&provider.id);
})
.unwrap_or(&provider.id);
match api_format {
"openai_responses" => {
// Codex OAuth (ChatGPT Plus/Pro 反代) 需要在请求体里强制 store: false
// + include: ["reasoning.encrypted_content"],由 transform 层统一处理。
let is_codex_oauth = provider
@@ -455,6 +492,7 @@ impl ProviderAdapter for ClaudeAdapter {
HeaderName::from_static("x-interaction-type"),
HeaderValue::from_static("conversation-agent"),
),
// x-interaction-id 由 forwarder 按需注入(仅在有 session 时)
(
HeaderName::from_static("x-vscode-user-agent-library-version"),
HeaderValue::from_static("electron-fetch"),