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: 2 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ ai:
# HITL 审计仅在未显式配置 provider/base_url/api_key 时继承主渠道 headers;
# 独立审计身份如需请求头,请在 hitl.audit_model.headers 中单独配置。
# 空 headers 与未配置等价;所有自定义头值在设置接口中均按敏感值脱敏。
context_window: 0 # 0=未知;保存通道时按服务元数据自动检测
context_profile: auto # 默认自动优化;识别本地服务后保存为 local_auto;custom 可使用手动运行时参数
# Pi 路径模型推理:DeepSeek/OpenAI 为 thinking / reasoning_effort;Claude 4.6+ 为 adaptive + output_config.effort(仅显式配置 effort 时下发);3.7 为 enabled+budget_tokens:10000(文档示例),effort 不映射,自定义预算用 extra_request_fields
reasoning:
mode: on # auto | on | off;off:OpenAI/Claude 不附加推理字段,DeepSeek 发送 thinking.type=disabled(其默认开启思考)
Expand Down
12 changes: 12 additions & 0 deletions docs/en-US/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,22 @@ ai:
| `base_url/api_key/model` | Required. Base URL usually includes a version path such as `/v1`. |
| `max_total_tokens` | Shared context budget for compression, attack-chain generation, multi-agent summaries, and similar paths. |
| `max_completion_tokens` | Per-response output cap; default is used when empty. |
| `context_window` | Runtime context window, or 0 when unknown. Channel saves can fill the detected value and retain a smaller configured window. |
| `context_profile` | Background policy, default `auto`. Recognized local backends use `local_auto`. Configuration files also accept manual `local`, explicit `economy` savings, and `custom` runtime settings. |
| `reasoning` | Default reasoning fields for the channel. Gateway support varies; try `mode: off` first when a provider rejects requests. |

The chat page reads saved channels into the “AI Channel” selector. A non-empty request `aiChannelId` selects a channel for that run/session without sending API credentials through the prompt path. Empty `aiChannelId` follows `ai.default_channel`.

### Automatic input savings

DeepSeek endpoints, explicit DeepSeek reasoning profiles and model names containing DeepSeek enable input savings automatically. With at least 16 tools, discovery declares six catalog entries, core tools and explicitly pinned tools. JSON tool results lose formatting whitespace. When an authorized local reader is available, results over 8,000 bytes become previews backed by the full output. Provider-reported reasoning tokens are shown separately within completion usage.

Channel saves and connection tests probe metadata from Ollama, llama.cpp, LM Studio, vLLM and oMLX. oMLX is identified by `owned_by: omlx`. Its matching model or alias in `/v1/models/status` supplies the preferred window, with the model list as a fallback when status access is unavailable. Generic compatible responses and private IP addresses alone do not select local optimization. Failed probes allow the channel to be saved.

The local input budget is capped at 60% of the window and 131,072 tokens, with output room reserved. An unknown window uses 32,768 tokens. Local policies tighten tool discovery and result limits, and use the `max_tokens` output field. New channels reporting only an architecture maximum retain an unknown runtime window. Changing a model or endpoint clears the previous automatic classification. These policies are applied in the background.

Compare complete tasks using input, cache hits, output and call counts to measure savings. Tool discovery and evidence readback may add calls. Explicit configuration-file overrides remain supported.

### Disabling reasoning by provider and model

Use `reasoning.mode: off` (or **Reasoning → Off** in chat) to disable reasoning. The request format depends on the channel:
Expand Down
10 changes: 9 additions & 1 deletion docs/en-US/token-usage-monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@

Pi reports usage; Go accumulates completed model turns and emits `agent_usage_summary` into the existing database and timeline. Local statistics are not a provider billing statement and may not cover every auxiliary HTTP request.

Prompt tokens are Pi input + cacheRead + cacheWrite. Completion tokens are output, and total is prompt + completion. Cache details are already included and must not be added again. Model-call counts reflect observed turns. Missing reasoning/cache details mean unreported data, not proven zero use.
Prompt tokens are Pi input + cacheRead + cacheWrite. Completion tokens are output, and total is prompt + completion. Cache details are already included and must not be added again. The usage view shows provider-reported reasoning tokens as a subset of completion tokens. Model-call counts reflect observed turns. Missing reasoning/cache details mean unreported data, not proven zero use. Durable assessments retain their own conservative budget ledger.

The dashboard and Settings usage view retain period, model and project filters. APIs are `GET /api/usage/tokens` and `GET /api/conversations/:id/token-usage`. Existing usage rows and historical timeline backfill remain compatible.

Streaming updates carry deltas; complete messages are committed at message end. Implementation: `internal/multiagent/pi_runner.go` and `internal/database/model_token_usage.go`. See [Pi AI](https://github.com/earendil-works/pi/tree/v1.5.2/packages/ai).

## DeepSeek

In the channel’s reasoning settings, use `off` for simple Q&A and format conversions or pair `auto` with `low` to keep reasoning on at a lower effort. The official API defaults to `high` when effort is unspecified and maps `medium` to `high`. The channel sends its per-request output limit as `max_tokens`, shared by reasoning and the visible answer. Compare output tokens, reasoning tokens and the cache hit rate for the same tasks at different effort levels. [DeepSeek Thinking Mode](https://api-docs.deepseek.com/guides/thinking_mode/) · [Chat Completions](https://api-docs.deepseek.com/api/create-chat-completion/)

The official API automatically caches reusable request prefixes. Pi orders MCP tool definitions by name so the same catalog introduces fewer prefix changes across requests. The provider reports the actual cache hits. [DeepSeek Context Caching](https://api-docs.deepseek.com/guides/kv_cache/)

Input savings activate in the background for the official DeepSeek endpoint, explicit `deepseek` / `deepseek_compat` reasoning profiles, and model names containing `deepseek` (case-insensitive). At 16 tools, native discovery keeps six catalog entries plus core tools and explicit always-visible tools available. Other builtins load on demand. With authorized file access, results over 8,000 bytes become head/tail previews backed by the full output. Tighter configured limits take precedence. Valid JSON tool results lose formatting whitespace, including when replaying older sessions. Strings and numeric lexemes retain their original text. Existing configuration-file overrides remain supported. Compare complete tasks, including discovery/readback calls and evidence quality, to measure actual savings.
12 changes: 12 additions & 0 deletions docs/zh-CN/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,20 @@ ai:
| `base_url/api_key/model` | 必填。Base URL 通常需要包含版本路径,如 OpenAI/兼容网关的 `/v1`。 |
| `max_total_tokens` | 上下文压缩、攻击链构建、多代理摘要等共用的总预算。 |
| `max_completion_tokens` | 单次模型输出上限;未填时使用默认值。 |
| `context_window` | 运行时上下文窗口,0 表示未知。保存通道时可自动填入服务报告的窗口,已填写的更小窗口优先。 |
| `context_profile` | 后台策略,默认 `auto`。识别本地服务后记录 `local_auto`。配置文件支持手动 `local`、显式节省 `economy` 和使用运行时参数的 `custom`。 |
| `reasoning` | 该通道的默认推理扩展字段。不同网关支持差异较大,异常时先尝试 `mode: off`。 |

### 自动输入优化

DeepSeek 官方地址、明确的 DeepSeek 推理线路或包含 DeepSeek 的模型名会自动启用输入节省。达到 16 个工具时使用按需发现,默认常驻前 6 个工具、核心工具及显式常驻清单。JSON 回包精简格式空白。有授权的本地读取工具时,超过 8,000 字节的结果保存全文并返回可回读的预览。推理 Token 单列统计且计入输出总量。

保存通道和测试连接会探测 Ollama、llama.cpp、LM Studio、vLLM 和 oMLX 的元数据。oMLX 通过 `owned_by: omlx` 识别,优先读取 `/v1/models/status` 的匹配模型或别名窗口,权限不足时回退到模型列表。普通兼容响应和内网 IP 本身不足以触发本地策略。探测失败可继续保存。

本地输入预算取窗口的 60%,上限 131,072 Token,并预留输出空间。窗口未知时使用 32,768 的保守窗口。工具发现和回包上限按本地窗口收紧,输出字段使用 `max_tokens`。模型只报告架构上限时,新通道保留窗口未知状态。切换模型或服务会清除旧的自动判定。页面在后台应用这些策略。

验证成本时,应比较完整任务的输入、缓存命中、输出及调用次数。工具搜索和分段回读可能增加调用。已有配置文件的显式覆盖继续生效。

### 按提供商和模型关闭推理

使用 `reasoning.mode: off`,或在对话中选择“推理 → 关闭”。实际请求参数按通道区分:
Expand Down
10 changes: 10 additions & 0 deletions docs/zh-CN/token-usage-monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

- 输入总量由 Pi 的 input、cacheRead、cacheWrite 相加;输出使用 output。
- 总量为输入与输出之和;缓存读取和缓存写入已经包含在输入中,不能重复叠加。
- 供应商单独报告的推理 Token 已包含在输出中,面板单列展示。
- 模型调用次数按已观察到的轮次统计,重试会产生新的轮次。
- 未报告的细分不能推断为没有发生。推理 Token 等没有独立字段时,不根据文本长度伪造账单计数。
- 深度评估使用其持久预算账本;供应商未报告用量时采用原有保守计费策略。

## 查看

Expand All @@ -16,3 +18,11 @@
已有模型用量表保持兼容。旧时间线用量事件可作为历史记录补齐;新记录仅由原生 Pi 事件生成。原生流式消息只传输增量,完整消息在结束时提交,避免重复累计与逐块重复发送整份内容。

实现:`internal/multiagent/pi_runner.go`、`internal/database/model_token_usage.go`、前端用量页面。基础模型 usage 语义见 [Pi AI](https://github.com/earendil-works/pi/tree/v1.5.2/packages/ai)。

## DeepSeek

在 AI 通道的推理设置中,可用 `off` 处理简单问答和格式转换,或用 `auto` 配合 `low` 保留低强度推理。官方接口未指定强度时默认使用 `high`,`medium` 也映射到 `high`。通道的「单次输出上限」通过 `max_tokens` 发送,推理和正文共用额度。对照面板里的输出、推理 Token 和缓存命中率,可比较相同任务在不同强度下的用量。[DeepSeek 推理模式](https://api-docs.deepseek.com/guides/thinking_mode/) · [Chat Completions](https://api-docs.deepseek.com/api/create-chat-completion/)

官方接口自动缓存可复用的请求前缀。Pi 按名称固定 MCP 工具定义的顺序,以减少相同工具目录在请求之间的前缀变化。缓存命中以供应商返回的用量为准。[DeepSeek 上下文缓存](https://api-docs.deepseek.com/guides/kv_cache/)

后台默认根据官方 DeepSeek 地址、DeepSeek 线路或模型名自动启用按需工具发现、JSON 回包精简与可回读的大结果预览。用户配置模型后即可使用。验证节省效果时,对照完整任务的输入、输出、缓存命中和调用次数,并检查任务产物与证据完整性。具体阈值见[配置说明](configuration.md#deepseek-输入优化)。
25 changes: 13 additions & 12 deletions internal/agentruntime/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@ import (
const ProtocolVersion = 1

type Model struct {
Provider string `json:"provider"`
API string `json:"api,omitempty"`
ID string `json:"id"`
BaseURL string `json:"baseUrl"`
APIKey string `json:"apiKey"`
Headers map[string]string `json:"headers,omitempty"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Thinking string `json:"thinking,omitempty"`
ExtraFields map[string]any `json:"extraFields,omitempty"`
Compat map[string]any `json:"compat,omitempty"`
InputBudget int `json:"inputBudget,omitempty"`
Provider string `json:"provider"`
API string `json:"api,omitempty"`
ID string `json:"id"`
BaseURL string `json:"baseUrl"`
APIKey string `json:"apiKey"`
Headers map[string]string `json:"headers,omitempty"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Thinking string `json:"thinking,omitempty"`
ExtraFields map[string]any `json:"extraFields,omitempty"`
Compat map[string]any `json:"compat,omitempty"`
InputBudget int `json:"inputBudget,omitempty"`
CompactToolJSON bool `json:"compactToolJSON,omitempty"`
}

type Tool struct {
Expand Down
1 change: 1 addition & 0 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,7 @@ func setupRoutes(
protected.PUT("/config", configHandler.UpdateConfig)
protected.POST("/config/apply", configHandler.ApplyConfig)
protected.POST("/config/test-openai", configHandler.TestOpenAI)
protected.POST("/config/detect-context", configHandler.DetectContext)
protected.POST("/config/test-typesafe", configHandler.TestTypeSafe)
protected.POST("/config/test-vision", configHandler.TestVision)
protected.POST("/config/list-models", configHandler.ListModels)
Expand Down
44 changes: 40 additions & 4 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,12 @@ type OpenAIConfig struct {
// Headers 追加或覆盖发给模型网关的 HTTP 请求头,值支持 ${ENV_VAR} 展开。
// 用于兼容要求私有鉴权头、版本头或组织头的第三方 OpenAI 兼容供应商。
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
// Reasoning 控制 Agent ChatModel 的 thinking / reasoning_effort / output_config 等(单/多代理路径生效)。
// ContextWindow 是模型原生上下文窗口(token);0 表示未知。用于校验/压缩 max_total_tokens。
ContextWindow int `yaml:"context_window,omitempty" json:"context_window,omitempty"`
// ContextProfile selects auto, economy, custom or local; local_auto records
// a local backend identified through the model metadata probe.
ContextProfile string `yaml:"context_profile,omitempty" json:"context_profile,omitempty"`
// Reasoning 控制 Agent ChatModel 的 thinking / reasoning_effort / output_config 等(Agent 单/多代理路径生效)。
Reasoning OpenAIReasoningConfig `yaml:"reasoning,omitempty" json:"reasoning,omitempty"`
}

Expand All @@ -748,6 +753,8 @@ type AIChannelConfig struct {
MaxTotalTokens int `yaml:"max_total_tokens,omitempty" json:"max_total_tokens,omitempty"`
MaxCompletionTokens int `yaml:"max_completion_tokens,omitempty" json:"max_completion_tokens,omitempty"`
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
ContextWindow int `yaml:"context_window,omitempty" json:"context_window,omitempty"`
ContextProfile string `yaml:"context_profile,omitempty" json:"context_profile,omitempty"`
Reasoning OpenAIReasoningConfig `yaml:"reasoning,omitempty" json:"reasoning,omitempty"`
}

Expand All @@ -764,6 +771,8 @@ func (c AIChannelConfig) ToOpenAIConfig() OpenAIConfig {
MaxTotalTokens: c.MaxTotalTokens,
MaxCompletionTokens: c.MaxCompletionTokens,
Headers: ExpandHeaderMap(c.Headers),
ContextWindow: c.ContextWindow,
ContextProfile: NormalizeContextProfile(c.ContextProfile),
Reasoning: c.Reasoning,
}
}
Expand All @@ -781,6 +790,8 @@ func AIChannelFromOpenAI(id, name string, oa OpenAIConfig) AIChannelConfig {
MaxTotalTokens: oa.MaxTotalTokens,
MaxCompletionTokens: oa.MaxCompletionTokens,
Headers: ExpandHeaderMap(oa.Headers),
ContextWindow: oa.ContextWindow,
ContextProfile: oa.ContextProfile,
Reasoning: oa.Reasoning,
}
}
Expand Down Expand Up @@ -862,10 +873,14 @@ func (c *Config) ApplyDefaultAIChannel() {
}

func (c OpenAIConfig) MaxCompletionTokensEffective() int {
if c.MaxCompletionTokens > 0 {
return c.MaxCompletionTokens
out := c.MaxCompletionTokens
if out <= 0 {
out = DefaultMaxCompletionTokens
}
return DefaultMaxCompletionTokens
if window := c.EffectiveContextWindow(); window > 0 && out > window/2 {
out = window / 2
}
return out
}

// IsDeepSeekEndpointOrModel reports whether the channel targets DeepSeek's
Expand Down Expand Up @@ -1561,6 +1576,11 @@ func Load(path string) (*Config, error) {
if err := validateOpenAIOutputLimits(cfg.OpenAI); err != nil {
return nil, err
}
for id, ch := range cfg.AI.Channels {
if err := ValidateContextSettings("ai.channels."+id, ch.ContextWindow, ch.ContextProfile); err != nil {
return nil, err
}
}
// 如果配置了工具目录,从目录加载工具配置
if cfg.Security.ToolsDir != "" {
inlineTools := append([]ToolConfig(nil), cfg.Security.Tools...)
Expand Down Expand Up @@ -1627,6 +1647,22 @@ func validateOpenAIOutputLimits(openAI OpenAIConfig) error {
if openAI.MaxCompletionTokens < 0 {
return fmt.Errorf("openai.max_completion_tokens 必须为正数")
}
return ValidateContextSettings("openai", openAI.ContextWindow, openAI.ContextProfile)
}

// ValidateContextSettings 校验主通道与 ai.channels 共用的上下文配置。
func ValidateContextSettings(prefix string, window int, profile string) error {
prefix = strings.TrimSpace(prefix)
if prefix == "" {
prefix = "openai"
}
if window < 0 {
return fmt.Errorf("%s.context_window 不能为负数", prefix)
}
if raw := strings.TrimSpace(profile); raw != "" &&
!strings.EqualFold(raw, "auto") && NormalizeContextProfile(raw) == "" {
return fmt.Errorf("%s.context_profile 只支持 auto、economy、custom、local 或 local_auto", prefix)
}
return nil
}

Expand Down
Loading
Loading