docs(comments): 注释中文化 · internal/runtime/model + internal/services/backthink(54 文件,纯替换)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 只翻注释/文档字符串:`go/scanner` 剥注释后按 token 序列+literal 逐 token 比对,**54/54 文件 SAME**;
  改动文件总行数与 HEAD 漂移 = 0(严格 1:1,无合并/拆行)
- 裁决编号与 design 节号逐字保留(03.4.1/03.4.2/03.4.3/14.4.1/14.4.5/07.4.2-4、design/04 §3.1/§3.2 等);
  `chat_template_kwargs` / `enable_thinking` / `reasoning_effort` / `thinking:{type}` 字段名与形状说明未动
- 存疑译法(已登记):`persona` 在叙事语境用「角色」、标识符语境保留 `persona`(仓库既有模块本身混用);
  `rung`→「级」;`provider` 存在「提供商/提供者」两种落笔(可按需一键统一)
- 复核:build/vet 绿 + 两包测试绿;gofmt 干净;残留英文注释 0 条
- 既有存量(未修,只报告):`model/anthropic_test.go` 612 行 >500;`applyAnthropicEvent` 115 行 >80
This commit is contained in:
AskaEth committed 2026-10-04 16:27:54 +08:00
1 parent f5cf117482
commit 3dc1232bc2
54 files changed
+1923 -1923

No files matched your search

+84 -84
View File
@@ -15,41 +15,41 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig"
)
// Anthropic Messages (/v1/messages) wire protocol constants.
// Anthropic Messages(/v1/messages)线协议常量。
const (
// anthropicAPIVersion is the Messages API version sent on every request.
// anthropicAPIVersion 是每次请求都要发送的 Messages API 版本。
anthropicAPIVersion = "2023-06-01"
// anthropicDefaultMaxTokens is the default max_tokens sent when
// Request.MaxTokens is zero. Anthropic requires max_tokens to be present and
// positive, so a sensible default is applied rather than omitting it.
// max_tokens is shared by thinking + text, so it must leave room for both.
// anthropicDefaultMaxTokens 是 Request.MaxTokens 为零时发送的默认
// max_tokens。Anthropic 要求 max_tokens 必须存在且为正,
// 因此这里给出合理默认值而不是省略它。
// max_tokens 由思考与正文共享,必须给两者都留出空间。
anthropicDefaultMaxTokens = 8192
// anthropicDataPrefix marks a payload embedded as a data: URI.
// anthropicDataPrefix 标记以 data: URI 内嵌的负载。
anthropicDataPrefix = "data:"
// anthropicThinkingTypeEnabled is the wire value of an enabled extended-
// thinking directive.
// anthropicThinkingTypeEnabled 是「开启扩展思考」指令的
// 线取值。
anthropicThinkingTypeEnabled = "enabled"
)
// anthropicThinking is the wire form of the extended-thinking directive. It is
// omitted entirely when the caller did not ask for thinking (or asked for it to
// be off), because Anthropic treats a missing field as "no thinking". DeepSeek's
// /anthropic endpoint instead defaults to enabled, so an explicit off is sent as
// {"type":"disabled"} with no budget (design/04 §3.2 fact 2).
// anthropicThinking 是扩展思考指令的线格式。调用方未要求思考
// (或要求关闭)时整段省略,因为 Anthropic 把字段缺失视为「不思考」。
// DeepSeek 的 /anthropic 端点恰好相反、默认开启,所以显式关闭会带着
// 不带预算的 {"type":"disabled"} 发出去
// (design/04 §3.2 事实 2)。
type anthropicThinking struct {
Type string `json:"type"`
BudgetTokens int `json:"budget_tokens,omitempty"`
}
// anthropicOutputConfig is the Anthropic-format reasoning strength block. Only
// DeepSeek's /anthropic endpoint documents it (and only the effort field), so it
// is never sent to the Anthropic endpoint (design/04 §3.2 fact 6).
// anthropicOutputConfig 是 Anthropic 格式的推理强度块。只有
// DeepSeek 的 /anthropic 端点记录了它(且只有 effort 字段),所以
// 它永远不会发给 Anthropic 端点(design/04 §3.2 事实 6)。
type anthropicOutputConfig struct {
Effort string `json:"effort"`
}
// anthropicRequest is the body sent to /v1/messages. Messages carry Anthropic's
// content-block model and the system prompt lives at the request root.
// anthropicRequest 是发给 /v1/messages 的请求体。Messages 采用 Anthropic
// 的内容块模型,system 提示词位于请求根。
type anthropicRequest struct {
Model string `json:"model"`
System string `json:"system,omitempty"`
@@ -63,21 +63,21 @@ type anthropicRequest struct {
Stream bool `json:"stream,omitempty"`
}
// anthropicMessage is one conversation turn. Content is an array of content
// blocks (text / image for now; tool_result blocks are added by M2).
// anthropicMessage 是一个会话回合。Content 是内容块数组
// (目前为文本 / 图片;tool_result 块由 M2 补上)。
type anthropicMessage struct {
Role string `json:"role"`
Content any `json:"content"`
}
// anthropicTool is the wire form of a tool schema.
// anthropicTool 是工具 schema 的线格式。
type anthropicTool struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
InputSchema any `json:"input_schema"`
}
// anthropicResponse is the JSON body returned by /v1/messages.
// anthropicResponse 是 /v1/messages 返回的 JSON 响应体。
type anthropicResponse struct {
ID string `json:"id"`
Model string `json:"model"`
@@ -86,8 +86,8 @@ type anthropicResponse struct {
Usage *anthropicUsage `json:"usage,omitempty"`
}
// anthropicBlock is a single content block in a response. For tool_use blocks the
// Input field is a fully decoded object (never a JSON string).
// anthropicBlock 是响应里的单个内容块。对 tool_use 块而言,
// Input 字段是已完全解码的对象(绝不是 JSON 字符串)。
type anthropicBlock struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
@@ -96,17 +96,17 @@ type anthropicBlock struct {
Input map[string]any `json:"input,omitempty"`
}
// anthropicUsage reports token consumption. Anthropic reports input_tokens and
// output_tokens rather than a single total.
// anthropicUsage 上报 token 消耗。Anthropic 上报的是 input_tokens
// 与 output_tokens,而不是单一总量。
type anthropicUsage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
}
// AnthropicChatAdapter calls an Anthropic Messages (/v1/messages) endpoint. It is
// constructed from a config.LLMConfig and implements Adapter for both
// non-streaming and token-level SSE streaming, speaking the provider's native
// content-block protocol so tool_use.input arrives as a structured object.
// AnthropicChatAdapter 调用 Anthropic Messages(/v1/messages)端点。
// 它由 config.LLMConfig 构建,同时实现非流式与 token 级 SSE 流式的
// Adapter,直接说提供商的原生内容块协议,
// 因此 tool_use.input 以结构化对象到达。
type AnthropicChatAdapter struct {
name string
baseURL string
@@ -114,34 +114,34 @@ type AnthropicChatAdapter struct {
model string
client *http.Client
recorder Recorder
// log reports reasoning adjustments and ignored directives. It is never nil:
// a discard logger tagged with this adapter's source is installed when the
// caller supplies none, so a provider decision is always traceable.
// log 上报推理调整与被忽略的指令。它永不为 nil:
// 调用方未提供时,会装入一个带有本适配器来源标记的丢弃式日志器,
// 因此提供商的每个决定都可追溯。
log *slog.Logger
}
// AnthropicOption configures an AnthropicChatAdapter.
// AnthropicOption 用于配置 AnthropicChatAdapter。
type AnthropicOption func(*AnthropicChatAdapter)
// WithAnthropicHTTPClient overrides the default HTTP client (used to inject mocks
// or a tuned transport in tests and production).
// WithAnthropicHTTPClient 覆盖默认 HTTP 客户端(用于在测试与生产
// 中注入 mock 或调优过的 transport)。
func WithAnthropicHTTPClient(c *http.Client) AnthropicOption {
return func(a *AnthropicChatAdapter) { a.client = c }
}
// WithAnthropicRecorder wires a per-call Recorder for CallRecord accounting.
// WithAnthropicRecorder 装入按调用计账的 Recorder,用于 CallRecord 统计。
func WithAnthropicRecorder(r Recorder) AnthropicOption {
return func(a *AnthropicChatAdapter) { a.recorder = r }
}
// WithAnthropicModel overrides the adapter's default model name.
// WithAnthropicModel 覆盖适配器的默认模型名。
func WithAnthropicModel(m string) AnthropicOption {
return func(a *AnthropicChatAdapter) { a.model = m }
}
// WithAnthropicLogger attaches the logger used to report reasoning adjustments
// (budget raised to fit max_tokens, ignored directives). A nil logger is ignored
// so the discard default stays in place.
// WithAnthropicLogger 挂上用于上报推理调整的日志器
// (为容纳 max_tokens 而抬高的预算、被忽略的指令)。nil 日志器会被忽略,
// 从而保留丢弃式的默认值。
func WithAnthropicLogger(l *slog.Logger) AnthropicOption {
return func(a *AnthropicChatAdapter) {
if l != nil {
@@ -150,8 +150,8 @@ func WithAnthropicLogger(l *slog.Logger) AnthropicOption {
}
}
// NewAnthropicChat builds an Anthropic adapter from a supplier config. baseURL is
// trimmed of any trailing slash before the /v1/messages path is appended.
// NewAnthropicChat 由供应商配置构建 Anthropic 适配器。baseURL 会先
// 去掉末尾斜杠,再拼接 /v1/messages 路径。
func NewAnthropicChat(cfg config.LLMConfig, opts ...AnthropicOption) *AnthropicChatAdapter {
a := &AnthropicChatAdapter{
name: "anthropic",
@@ -165,9 +165,9 @@ func NewAnthropicChat(cfg config.LLMConfig, opts ...AnthropicOption) *AnthropicC
log: discardLogger("runtime/model/anthropic"),
}
if kind := strings.TrimSpace(cfg.ReasoningKind); kind != "" && kind != runtimeconfig.ReasoningKindAuto {
// The anthropic protocol defines its own thinking shape, so an
// openai-shape declaration is inert here. Report it rather than letting a
// configured option be silently ineffective (裁决 03.4.2: 禁止静默无效).
// anthropic 协议自带思考形状,因此这里声明 openai
// 形状是无效的。要上报它,而不是任由一个已配置的选项
// 静默失效(裁决 03.4.2:禁止静默无效)。
a.log.Warn("reasoning.kind ignored: the anthropic protocol defines its own thinking shape",
"provider", ProviderAnthropic, "field", "reasoning.kind", "kind", kind)
}
@@ -177,14 +177,14 @@ func NewAnthropicChat(cfg config.LLMConfig, opts ...AnthropicOption) *AnthropicC
return a
}
// Name returns the adapter identifier.
// Name 返回适配器标识。
func (a *AnthropicChatAdapter) Name() string { return a.name }
// Chat performs a non-streaming completion. A response cut off by the token
// budget with no visible output (stop_reason max_tokens and neither text nor a
// tool_use block) is retried once with a doubled budget and, if that still
// yields nothing, replaced by a non-empty placeholder so the caller never sees a
// silent empty reply.
// Chat 执行一次非流式补全。被 token 预算截断且没有任何可见输出
// (stop_reason 为 max_tokens,且既无文本也无 tool_use 块)
// 的响应,会用翻倍的预算重试一次;
// 若仍无输出,则替换为非空占位文本,
// 让调用方永远不会看到静默的空回复。
func (a *AnthropicChatAdapter) Chat(ctx context.Context, req Request, opts Options) (Response, error) {
start := time.Now()
if a.baseURL == "" {
@@ -210,8 +210,8 @@ func (a *AnthropicChatAdapter) Chat(ctx context.Context, req Request, opts Optio
return out, nil
}
// chatOnce performs a single non-streaming /v1/messages call and normalizes the
// wire body into a Response.
// chatOnce 执行一次非流式 /v1/messages 调用,并把线格式
// 响应体归一化为 Response。
func (a *AnthropicChatAdapter) chatOnce(ctx context.Context, req Request) (Response, error) {
body, err := a.buildRequest(req, false)
if err != nil {
@@ -236,13 +236,13 @@ func (a *AnthropicChatAdapter) chatOnce(ctx context.Context, req Request) (Respo
return responseFromAnthropicWire(&wire), nil
}
// ChatStream streams structured events via onEvent and returns the aggregated
// response with usage when the provider reports it. Text blocks are delivered as
// text_start/text_delta/text_end events; tool_use blocks are delivered as
// tool_call_start/tool_call_delta/tool_call_end so the frontend can render each
// segment independently. A budget-truncated empty stream is retried once with a
// doubled budget (thinking from the retry is suppressed to avoid a duplicate
// block) and, if still empty, closed with a non-empty placeholder text segment.
// ChatStream 通过 onEvent 流出结构化事件,并在提供商上报时
// 返回带用量的聚合响应。文本块以 text_start/text_delta/text_end
// 事件投递;tool_use 块以
// tool_call_start/tool_call_delta/tool_call_end 投递,
// 前端因此可以独立渲染各片段。被预算截断的空流会用翻倍预算重试一次
// (重试产生的思考被抑制,以免出现重复块),
// 若仍为空,则用一段非空占位文本关闭。
func (a *AnthropicChatAdapter) ChatStream(ctx context.Context, req Request, onEvent func(StreamEvent)) (Response, error) {
start := time.Now()
if a.baseURL == "" {
@@ -267,8 +267,8 @@ func (a *AnthropicChatAdapter) ChatStream(ctx context.Context, req Request, onEv
return out, nil
}
// streamOnce performs a single streaming /v1/messages call and aggregates its
// SSE events into a Response.
// streamOnce 执行一次流式 /v1/messages 调用,并把其 SSE
// 事件聚合为 Response。
func (a *AnthropicChatAdapter) streamOnce(ctx context.Context, req Request, onEvent func(StreamEvent)) (Response, error) {
body, err := a.buildRequest(req, true)
if err != nil {
@@ -289,7 +289,7 @@ func (a *AnthropicChatAdapter) streamOnce(ctx context.Context, req Request, onEv
return scanAnthropicSSE(resp.Body, onEvent)
}
// resolveModel returns the effective model name.
// resolveModel 返回实际生效的模型名。
func (a *AnthropicChatAdapter) resolveModel(req Request) (string, error) {
if req.Model != "" {
return req.Model, nil
@@ -300,7 +300,7 @@ func (a *AnthropicChatAdapter) resolveModel(req Request) (string, error) {
return "", ErrNoModel
}
// endpoint returns the full /v1/messages URL.
// endpoint 返回完整的 /v1/messages URL。
func (a *AnthropicChatAdapter) endpoint() string {
return a.baseURL + "/v1/messages"
}
@@ -314,16 +314,16 @@ func (a *AnthropicChatAdapter) buildRequest(req Request, stream bool) ([]byte, e
var thinking *anthropicThinking
var output *anthropicOutputConfig
if deepSeekAnthropicEndpoint(req.Model, a.baseURL) {
// A DeepSeek /anthropic endpoint ignores budget_tokens, so max_tokens is
// deliberately not raised here: raising it would misreport the budget the
// provider actually applies. Strength goes through output_config.effort
// instead (design/04 §3.2 fact 6).
// DeepSeek 的 /anthropic 端点忽略 budget_tokens,因此这里刻意
// 不抬高 max_tokens:抬高了就会错误地反映提供商实际采用的预算。
// 强度改走 output_config.effort
// (design/04 §3.2 事实 6)。
thinking, output = anthropicDeepSeekReasoning(a.log, req.Model, req.Thinking, req.ReasoningEffort)
reportForcedToolChoiceWithThinking(a.log, string(vendorDeepSeek), req.Model, req.ToolChoice, anthropicThinkingOn(thinking))
} else {
// Extended thinking shares max_tokens with the visible answer, so on the
// Anthropic endpoint the budget is resolved together with the limit it
// requires (design/04 §3.2 red line).
// 扩展思考与可见答案共享 max_tokens,所以在 Anthropic 端点上,
// 预算是与它所需要的限额一起解出的
// (design/04 §3.2 红线)。
tokenLimit, thinking = anthropicThinkingFor(req.Thinking, tokenLimit, a.log)
a.reportAnthropicEffortIgnored(req)
}
@@ -346,10 +346,10 @@ func (a *AnthropicChatAdapter) buildRequest(req Request, stream bool) ([]byte, e
return buf, nil
}
// reportAnthropicEffortIgnored reports a reasoning_effort on an endpoint that has
// no field for it: the Anthropic Messages protocol carries thinking strength as
// budget_tokens. It only reports — inventing a budget would change the caller's
// intent.
// reportAnthropicEffortIgnored 上报落在没有对应字段的端点上的
// reasoning_effort:Anthropic Messages 协议用 budget_tokens
// 承载思考强度。它只上报——凭空编一个预算会改变调用方的
// 意图。
func (a *AnthropicChatAdapter) reportAnthropicEffortIgnored(req Request) {
if strings.TrimSpace(req.ReasoningEffort) == "" {
return
@@ -393,10 +393,10 @@ func (a *AnthropicChatAdapter) record(req Request, start time.Time, tokens int)
})
}
// responseFromAnthropicWire converts a non-streaming /v1/messages response into a
// normalized Response. Each text block contributes to the assistant content; each
// tool_use block becomes a ToolCall whose Args is taken directly from the decoded
// input object.
// responseFromAnthropicWire 把非流式 /v1/messages 响应转换为
// 归一化的 Response。每个文本块汇入助手内容;
// 每个 tool_use 块变成一个 ToolCall,其 Args 直接取自已解码的
// input 对象。
func responseFromAnthropicWire(wire *anthropicResponse) Response {
out := Response{Model: wire.Model}
var b strings.Builder
@@ -421,9 +421,9 @@ func responseFromAnthropicWire(wire *anthropicResponse) Response {
return out
}
// AnthropicToolResult builds the content block used to feed a tool result back to
// Anthropic (role: user, first content element). toolUseID must equal the id of a
// previous tool_use block; isError marks an execution failure.
// AnthropicToolResult 构建把工具结果回喂给 Anthropic 的内容块
// (role: user,第一个内容元素)。toolUseID 必须等于此前某个
// tool_use 块的 id;isError 标记执行失败。
func AnthropicToolResult(toolUseID, content string, isError bool) map[string]any {
return map[string]any{
"type": "tool_result",