feat: 实现日历提醒完整功能(操作执行、通知服务重构、归档)

- 新增 ReminderActionExecutor 处理取消/稍后提醒操作
- 新增 ReminderOutboxStore 本地存储待处理操作
- 重构 LocalNotificationService 支持聚合提醒和交互操作
- 新增 event_color_resolver 工具类统一颜色解析
- 新增 CalendarService.archiveEvent 归档方法
- 增强 ModelTracking 支持缓存命中、推理token和成本追踪
- 添加 qwen3.5-35b-a3b 模型配置
- 更新 AndroidManifest 全屏intent权限
- 补充相关单元测试和文档
This commit is contained in:
qzl
2026-03-18 19:12:47 +08:00
parent 257cb0f5d5
commit 00f37d7e19
35 changed files with 2676 additions and 244 deletions
+76 -7
View File
@@ -85,9 +85,15 @@ class LiteLLMService:
selected_tier = tier
break
cached_token_rate = (
selected_tier.cache_hit_cost_per_token
if selected_tier.cache_hit_cost_per_token > 0
else selected_tier.input_cost_per_token
)
return float(
uncached_prompt_tokens * selected_tier.input_cost_per_token
+ normalized_cached_tokens * selected_tier.cache_hit_cost_per_token
+ normalized_cached_tokens * cached_token_rate
+ normalized_completion_tokens * selected_tier.output_cost_per_token
)
@@ -95,23 +101,86 @@ class LiteLLMService:
self,
*,
model: str,
usage_summary: dict[str, int] | None,
usage_summary: dict[str, Any] | None,
) -> dict[str, Any]:
summary = usage_summary or {}
input_tokens = max(int(summary.get("input_tokens", 0) or 0), 0)
output_tokens = max(int(summary.get("output_tokens", 0) or 0), 0)
total_tokens = max(
int(summary.get("total_tokens", input_tokens + output_tokens) or 0), 0
)
latency_ms = max(int(summary.get("latency_ms", 0) or 0), 0)
cached_prompt_tokens = max(int(summary.get("cached_prompt_tokens", 0) or 0), 0)
cost = self.calculate_cost(
model=model,
prompt_tokens=input_tokens,
completion_tokens=output_tokens,
cached_prompt_tokens=cached_prompt_tokens,
prompt_cache_hit_tokens = max(
int(summary.get("prompt_cache_hit_tokens", cached_prompt_tokens) or 0), 0
)
prompt_cache_miss_tokens = max(
int(
summary.get(
"prompt_cache_miss_tokens",
max(input_tokens - prompt_cache_hit_tokens, 0),
)
or 0
),
0,
)
reasoning_tokens = max(int(summary.get("reasoning_tokens", 0) or 0), 0)
direct_cost_raw = summary.get("direct_cost")
direct_cost_observed = bool(int(summary.get("direct_cost_observed", 0) or 0))
direct_cost_complete = bool(int(summary.get("direct_cost_complete", 0) or 0))
model_call_records = max(int(summary.get("model_call_records", 0) or 0), 0)
usage_records = max(int(summary.get("usage_records", 0) or 0), 0)
usage_complete = model_call_records == 0 or model_call_records == usage_records
direct_cost = self._coerce_non_negative_float(direct_cost_raw)
if (
usage_complete
and direct_cost_observed
and direct_cost_complete
and direct_cost is not None
):
cost = direct_cost
cost_source = "provider"
else:
cost = self.calculate_cost(
model=model,
prompt_tokens=input_tokens,
completion_tokens=output_tokens,
cached_prompt_tokens=cached_prompt_tokens,
)
cost_source = (
"incomplete_usage_fallback"
if not usage_complete
else (
"catalog_fallback_incomplete_provider_cost"
if direct_cost_observed and not direct_cost_complete
else "catalog_fallback"
)
)
return {
"model": model,
"inputTokens": input_tokens,
"outputTokens": output_tokens,
"totalTokens": total_tokens,
"cachedPromptTokens": cached_prompt_tokens,
"promptCacheHitTokens": prompt_cache_hit_tokens,
"promptCacheMissTokens": prompt_cache_miss_tokens,
"reasoningTokens": reasoning_tokens,
"cost": cost,
"costSource": cost_source,
"usageComplete": usage_complete,
"latencyMs": latency_ms,
}
@staticmethod
def _coerce_non_negative_float(value: Any) -> float | None:
if value is None:
return None
try:
parsed = float(value)
except (TypeError, ValueError):
return None
if parsed < 0:
return None
return parsed