Skip to content

feat(async-context-compression): add manual invoke ability (closes #80)#92

Open
Fu-Jie wants to merge 2 commits into
mainfrom
trae/issue-80-manual-compress
Open

feat(async-context-compression): add manual invoke ability (closes #80)#92
Fu-Jie wants to merge 2 commits into
mainfrom
trae/issue-80-manual-compress

Conversation

@Fu-Jie

@Fu-Jie Fu-Jie commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Implements manual invoke ability for async-context-compression requested in #80.

Adds a public manual_compress() method on the Filter that bypasses threshold/hysteresis guards and reuses the existing compression pipeline (_load_full_chat_messages / _calculate_target_compressed_count / _get_chat_lock / _generate_summary_async / _save_summary). An HTTP endpoint POST /api/v1/filters/async-context-compression/compress is registered on the Open WebUI app for programmatic invocation.

Changes

  • plugins/filters/async-context-compression/async_context_compression.py (+453/-1)
    • New async def manual_compress(chat_id, model, user_data, ...) method
    • New manual_compress_min_messages valve (default 4)
    • 9 new i18n keys x 12 languages
    • Threshold/force gating, idempotency via existing chat lock
    • Module-level route registration on webui_app
  • plugins/filters/async-context-compression/README.md (+89/-1)
  • plugins/filters/async-context-compression/README_CN.md (+89/-1)

Use Cases

  • Cloned chat lost compression state -> manually re-compress
  • Imported history chat -> compress before submitting
  • Switching to a smaller-context model -> manually compress to avoid overflow

Test Plan

  • Automatic trigger logic unaffected (inlet/outlet paths unchanged)
  • manual_compress() returns correct compression result
  • Idempotency: repeated calls do not double-compress
  • Below-threshold chat returns skipped unless force=True
  • Empty/missing chat returns skipped with chat_not_found_or_empty

Usage

curl -X POST "http://localhost:8080/api/v1/filters/async-context-compression/compress" \
  -H "Authorization: Bearer $OPENWEBUI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"chat_id": "abc123", "force": true}'

Closes #80

Add an HTTP endpoint POST /api/v1/filters/async-context-compression/compress
that triggers context compression for a chat without first sending a request
to the model. This solves cloned chats losing compression, imported chat JSON
with no summary, and switching to a smaller-context model mid-chat.

The endpoint reuses the exact same compression pipeline as the automatic
outlet trigger (threshold check, hysteresis guard, atomic-group alignment,
optimistic lock), so it is idempotent and safe to call repeatedly. An optional
force flag bypasses threshold/hysteresis guards for debugging/rescue.

- Filter.manual_compress(): new method reusing _generate_summary_async
- register_manual_compress_endpoint(): idempotent route registration on webui_app
- Bump version 1.6.5 -> 1.6.6
- Document the endpoint in README.md and README_CN.md
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

✅ Plugin Version Check / 插件版本检查

版本更新检测通过!PR 包含版本变化和更新说明。

Version check passed! PR contains version changes and update description.


版本变化 / Version Changes

Plugin Updates

  • Async Context Compression: v1.6.5 → v1.6.7 | 📖 README

This comment was generated automatically. / 此评论由自动生成。

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

此拉取请求为异步上下文压缩过滤器新增了手动触发压缩的 HTTP 端点(POST /api/v1/filters/async-context-compression/compress),并同步更新了中英文文档。审查意见指出了一些需要改进的问题:首先,FastAPI 路由中未正确导入和使用 Depends(bearer_scheme),导致 Bearer Token 无法自动提取而退回到匿名上下文;其次,在 manual_compress 中存在重复读取数据库记录的开销;此外,直接调用包含同步数据库查询的 _get_model_thresholds 违反了异步 I/O 规范,应使用 asyncio.to_thread 包装;最后,接口返回的 status 判断逻辑不够准确,应通过对比压缩计数是否增加来确定是否成功生成了新摘要。

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +5401 to +5402
from fastapi import HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

为了在 FastAPI 路由中正确使用 Depends(bearer_scheme) 依赖注入,需要从 fastapi 导入 Depends

Suggested change
from fastapi import HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer

Comment on lines +5443 to +5446
async def _manual_compress_endpoint(
payload: Dict[str, Any],
credentials: Optional[HTTPAuthorizationCredentials] = None,
) -> Dict[str, Any]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

在 FastAPI 中,如果不使用 Depends(bearer_scheme)credentials 参数将无法从 HTTP 请求的 Authorization 头部自动提取 Bearer Token,而是会被当作普通的请求体或查询参数解析,导致其值始终为 None。这会使得 _resolve_user 总是退回到匿名上下文(Anonymous)。

建议使用 Depends(bearer_scheme) 来正确注入凭证。

Suggested change
async def _manual_compress_endpoint(
payload: Dict[str, Any],
credentials: Optional[HTTPAuthorizationCredentials] = None,
) -> Dict[str, Any]:
async def _manual_compress_endpoint(
payload: Dict[str, Any],
credentials: Optional[HTTPAuthorizationCredentials] = Depends(bearer_scheme),
) -> Dict[str, Any]:

Comment on lines +3992 to +4012
if not any(self._is_summary_message(m) for m in summary_messages):
existing_record = await self._load_summary_record(chat_id)
if existing_record and existing_record.compressed_message_count > 0:
boundary = min(
existing_record.compressed_message_count, len(summary_messages)
)
injected_summary_msg = self._build_summary_message(
existing_record.summary,
lang,
existing_record.compressed_message_count,
)
summary_messages = (
summary_messages[:boundary]
+ [injected_summary_msg]
+ summary_messages[boundary:]
)

previous_record = await self._load_summary_record(chat_id)
previous_compressed_count = (
previous_record.compressed_message_count if previous_record else 0
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这里存在重复读取数据库的操作。existing_record 在第 3993 行已经通过 _load_summary_record 加载过一次,而在第 4009 行又重新调用 _load_summary_record 加载并赋值给 previous_record。我们可以先加载 existing_record,然后根据需要进行注入,最后直接将 existing_record 赋值给 previous_record,从而避免不必要的数据库 I/O 开销。

        existing_record = await self._load_summary_record(chat_id)
        if not any(self._is_summary_message(m) for m in summary_messages):
            if existing_record and existing_record.compressed_message_count > 0:
                boundary = min(
                    existing_record.compressed_message_count, len(summary_messages)
                )
                injected_summary_msg = self._build_summary_message(
                    existing_record.summary,
                    lang,
                    existing_record.compressed_message_count,
                )
                summary_messages = (
                    summary_messages[:boundary]
                    + [injected_summary_msg]
                    + summary_messages[boundary:]
                )

        previous_record = existing_record
        previous_compressed_count = (
            previous_record.compressed_message_count if previous_record else 0
        )

Comment on lines +4022 to +4029
thresholds = (
self._get_model_thresholds(effective_model_id)
if effective_model_id
else {
"compression_threshold_tokens": self.valves.compression_threshold_tokens,
"max_context_tokens": self.valves.max_context_tokens,
}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

根据项目规范(Non-Negotiable Rules 第 5 条:Async I/O only — wrap sync calls with asyncio.to_thread),在异步上下文中执行同步 I/O 操作时,应当使用 asyncio.to_thread 进行包装。_get_model_thresholds 内部调用了同步的数据库查询 _call_db_sync,直接在 async def manual_compress 中调用会阻塞事件循环。建议使用 asyncio.to_thread 异步执行该方法。

Suggested change
thresholds = (
self._get_model_thresholds(effective_model_id)
if effective_model_id
else {
"compression_threshold_tokens": self.valves.compression_threshold_tokens,
"max_context_tokens": self.valves.max_context_tokens,
}
)
thresholds = (
await asyncio.to_thread(self._get_model_thresholds, effective_model_id)
if effective_model_id
else {
"compression_threshold_tokens": self.valves.compression_threshold_tokens,
"max_context_tokens": self.valves.max_context_tokens,
}
)
References
  1. Async I/O only — wrap sync calls with asyncio.to_thread (link)

Comment on lines +4162 to +4172
return {
"status": "compressed" if new_record else "skipped",
"chat_id": chat_id,
"message_count": len(messages),
"current_tokens": current_tokens,
"summary_tokens": new_summary_tokens,
"threshold": compression_threshold_tokens,
"previous_compressed_count": previous_compressed_count,
"compressed_count": new_compressed_count,
"forced": force,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这里的 status 判断逻辑不够准确。如果该会话之前已经存在压缩记录(previous_record 不为 None),且本次调用由于某种原因(例如在 _generate_summary_async 内部被跳过或静默失败)并未实际生成新的摘要,new_record 依然不为 None。这会导致接口返回 "status": "compressed",而实际上并没有发生新的压缩,这与文档中 compressed 表示“生成了新摘要并已持久化”的定义不符。

建议将判断条件改为 new_compressed_count > previous_compressed_count,以确保只有在压缩边界实际推进时才返回 "status": "compressed"

Suggested change
return {
"status": "compressed" if new_record else "skipped",
"chat_id": chat_id,
"message_count": len(messages),
"current_tokens": current_tokens,
"summary_tokens": new_summary_tokens,
"threshold": compression_threshold_tokens,
"previous_compressed_count": previous_compressed_count,
"compressed_count": new_compressed_count,
"forced": force,
}
return {
"status": "compressed" if new_compressed_count > previous_compressed_count else "skipped",
"chat_id": chat_id,
"message_count": len(messages),
"current_tokens": current_tokens,
"summary_tokens": new_summary_tokens,
"threshold": compression_threshold_tokens,
"previous_compressed_count": previous_compressed_count,
"compressed_count": new_compressed_count,
"forced": force,
}

…act command (closes #80)

The HTTP endpoint approach (POST /api/v1/filters/async-context-compression/compress)
is unworkable in practice: Open WebUI filters run inside the main process without
an independent HTTP service, app.add_api_route pollutes the host app's route
namespace, and users cannot invoke the endpoint from the Open WebUI frontend.

Replace it with an inlet command-recognition approach: when the latest user
message is /compact, /compress, or /summary (case-insensitive), the filter
triggers compression via manual_compress() and replaces the command text with
a system notice so the LLM relays the outcome verbatim. The command is never
sent to the LLM as a literal prompt.

- Remove all HTTP endpoint code: _MANUAL_ROUTE_*, register_manual_compress_endpoint,
  _manual_compress_endpoint, _resolve_user, _get_manual_filter_instance,
  _MANUAL_FILTER_INSTANCE, app.add_api_route, import-time registration
- Add _try_handle_manual_command() in inlet flow (called after chat_id is resolved)
- Support /compact, /compress, /summary (case-insensitive)
- Support force via trailing ! (e.g. /compact!) or force argument (e.g. /compact force)
- Keep manual_compress() core method (reuses _generate_summary_async pipeline)
- Add manual_compress_min_messages valve (default 4, bypassed by force)
- Add 3 new i18n keys x 12 languages (manual_compressed, manual_skipped, manual_error)
- Bump version 1.6.6 -> 1.6.7
- Update README.md and README_CN.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Async Context Compression Manual Invoke Ability

1 participant