docs(comments): 注释中文化 · internal/runtime/model + internal/services/backthink(54 文件,纯替换)
- 只翻注释/文档字符串:`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:
1 parent
f5cf117482
commit
3dc1232bc2
54 files changed
+1923
-1923
No files matched your search
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user