feat(model): 按 provider 形状独立适配推理开关 + per-persona 提供商链真接线(裁决 03.4.2/08.4.1/03.4.1 余项)
- 新增 reasoning_shape.go:**每个 provider 形状一个 resolver,只发自己形状的字段**
· deepseek:thinking{type} + reasoning_effort(依据 DevDocs/reference/deepseek 官方本地副本)
· vllm:chat_template_kwargs.enable_thinking · dashscope:顶层 enable_thinking(**只允许显式声明**)
· openai:仅 reasoning_effort · anthropic:自己的 thinking{budget_tokens}(未改)
· 未知 kind → WARN 且一个字段都不发(不许静默失效)
- 形状载体:models.json 组级 `reasoning.kind`(可热更新,未声明则不写该键)+ env `LLM_REASONING_KIND`
- 红线:`effort:none` 在只有布尔开关的形状上翻成**显式关思考**,绝不降成任何强度档
- 03.4.1 余项:`Request.PersonaID` + `RuntimeLLM.ForPersona(pid)` 视图;
每 persona 协议一条链并消费 `RouteProviderForPersona`;`agent/request.go` 非流式路径同一钩子
- **生产装配(主会话补)**:默认 agent 与 personaAgentPool 都改用 `ForPersona(pid)` 视图——
否则角色级模型/提供商路由在生产里永不被选中(形同虚设)
- 复核:build/vet 绿 + model/agent/config/cmd 包测试绿;5 条变异反证先红后还原
(其中一条「红但没钉住」已改用例:通道读取加 2s 超时,避免整包 panic 掩盖信号)
- 未落:DashScope 的 auto 推断(缺官方依据,只允许显式声明);Qwen `/think` 前导消息
(design/01 §2.1 与 design/11 §3 口径冲突,停手报告)
This commit is contained in:
1 parent
71fefe5fc3
commit
8890c30bb5
17 files changed
+1496
-134
No files matched your search
@@ -10,6 +10,17 @@ const defaultPurpose = "agent"
|
||||
// configured model/temperature/max-tokens/purpose and the registered tool
|
||||
// schemas. When a router is injected it overrides the model from the request
|
||||
// contents, which is how vision requests are routed to the vision model.
|
||||
//
|
||||
// 本路径与流式主路径(loop.buildRequest)共用同一处 persona 钩子(裁决 03.4.1):
|
||||
// 角色由**适配器视图**携带(model.PersonaScoped,见 model/persona_stack.go),
|
||||
// 写成请求上的 PersonaID,模型路由也按它走。为什么角色不在本函数参数里:RunStep
|
||||
// 由 loop 与 automations 调用,角色只存在于 loop 的会话环境 ctx 里,而 agent
|
||||
// 不能反向 import loop;按角色分开注入的适配器视图(一角色一 agent,
|
||||
// design/22 §4.2 方案 A)是唯一不依赖 ctx 的载体。
|
||||
//
|
||||
// 角色**已知**时才切到 RouteRequestForPersona:未知角色(普通 *model.Router 或
|
||||
// 未绑角色的视图)保持原来的 RouteRequest —— 后者带 vision 模型的改写规则,
|
||||
// RouteRequestForPersona 刻意不含(图像路由已归 vision.Router,裁决 14.4.1/14.4.5)。
|
||||
func (a *Agent) buildRequest(messages []model.Message) model.Request {
|
||||
req := model.Request{
|
||||
Model: a.opts.Model,
|
||||
@@ -19,7 +30,16 @@ func (a *Agent) buildRequest(messages []model.Message) model.Request {
|
||||
Purpose: a.purpose(),
|
||||
Tools: a.toolSchemas(),
|
||||
}
|
||||
if ps, ok := a.adapter.(model.PersonaScoped); ok {
|
||||
req.PersonaID = ps.ScopedPersona()
|
||||
}
|
||||
if a.router != nil {
|
||||
if pr, ok := a.router.(model.PersonaRouter); ok && req.PersonaID != "" {
|
||||
if routeModel, _ := pr.RouteRequestForPersona(req.PersonaID, req); routeModel != "" {
|
||||
req.Model = routeModel
|
||||
}
|
||||
return req
|
||||
}
|
||||
if routeModel, _ := a.router.RouteRequest(req); routeModel != "" {
|
||||
req.Model = routeModel
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
)
|
||||
|
||||
// 本文件钉住非流式路径(agent.RunStep)的 persona 钩子(裁决 03.4.1):它必须与
|
||||
// 流式主路径(loop.buildRequest 的 RouteRequestForPersona)同源。角色由适配器视图
|
||||
// 携带(model.PersonaScoped),因为它不经 ctx 也能到达这里,且一角色一 agent 的装配
|
||||
// 方式(design/22 §4.2 方案 A)本来就把它定在装配处。
|
||||
|
||||
// personaScopedAdapter 是带角色的 mockAdapter 视图,模拟 model.PersonaStack:
|
||||
// RunStep 应据此把角色写进请求,并按角色路由模型名。
|
||||
type personaScopedAdapter struct {
|
||||
mockAdapter
|
||||
persona string
|
||||
}
|
||||
|
||||
// ScopedPersona 实现 model.PersonaScoped。
|
||||
func (p *personaScopedAdapter) ScopedPersona() string { return p.persona }
|
||||
|
||||
// TestRunStep_PersonaScopedAdapterRoutesByPersona 断言:带角色的适配器视图让非流式
|
||||
// 路径按 persona 路由(而不是按消息 role),并把角色带到请求上,供模型层选
|
||||
// persona 的协议链(models.json personaProviders)。
|
||||
func TestRunStep_PersonaScopedAdapterRoutesByPersona(t *testing.T) {
|
||||
t.Parallel()
|
||||
router := model.NewRouter("default",
|
||||
model.WithRoleModel(model.RoleUser, "user-model"),
|
||||
model.WithPersonaModel("cyrene", "persona-model"),
|
||||
)
|
||||
ad := &personaScopedAdapter{
|
||||
mockAdapter: mockAdapter{resp: model.Response{Model: "persona-model", Content: "ok"}},
|
||||
persona: "cyrene",
|
||||
}
|
||||
a := newTestAgent(t, ad, nil, WithModel(ad, router))
|
||||
|
||||
if _, err := a.RunStep(context.Background(), "s",
|
||||
[]model.Message{model.TextMessage(model.RoleUser, "hi")}); err != nil {
|
||||
t.Fatalf("RunStep: %v", err)
|
||||
}
|
||||
if ad.gotReq.PersonaID != "cyrene" {
|
||||
t.Fatalf("req.PersonaID = %q, want cyrene", ad.gotReq.PersonaID)
|
||||
}
|
||||
if ad.gotReq.Model != "persona-model" {
|
||||
t.Fatalf("req.Model = %q, want persona-model (persona binding wins over the message role)",
|
||||
ad.gotReq.Model)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunStep_PersonaStampedWithoutRouter asserts the persona travels with the
|
||||
// adapter view even when no router is injected — the carrier must not depend on
|
||||
// the routing decision, or an adapter-only wiring would silently lose it.
|
||||
func TestRunStep_PersonaStampedWithoutRouter(t *testing.T) {
|
||||
t.Parallel()
|
||||
ad := &personaScopedAdapter{
|
||||
mockAdapter: mockAdapter{resp: model.Response{Content: "ok"}},
|
||||
persona: "cyrene",
|
||||
}
|
||||
a := newTestAgent(t, ad, nil, WithModel(ad, nil))
|
||||
|
||||
if _, err := a.RunStep(context.Background(), "s",
|
||||
[]model.Message{model.TextMessage(model.RoleUser, "hi")}); err != nil {
|
||||
t.Fatalf("RunStep: %v", err)
|
||||
}
|
||||
if ad.gotReq.PersonaID != "cyrene" {
|
||||
t.Fatalf("req.PersonaID = %q, want cyrene", ad.gotReq.PersonaID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunStep_UnknownPersonaKeepsLegacyRouting 断言未绑定角色的 agent 行为不变:
|
||||
// 没有角色就继续走原来的 RouteRequest,其中包含「有图像 → 换视觉模型」这条老规则
|
||||
// (RouteRequestForPersona 故意不含它,图像路由已归 vision.Router)。这条断言防止
|
||||
// 把两代路由语义混在一起。
|
||||
func TestRunStep_UnknownPersonaKeepsLegacyRouting(t *testing.T) {
|
||||
t.Parallel()
|
||||
router := model.NewRouter("default",
|
||||
model.WithRoleModel(model.RoleUser, "user-model"),
|
||||
model.WithVisionModel("vision-model"),
|
||||
)
|
||||
ad := &mockAdapter{resp: model.Response{Model: "vision-model", Content: "ok"}}
|
||||
a := newTestAgent(t, ad, nil, WithModel(ad, router))
|
||||
|
||||
msg := model.ImageMessage(model.RoleUser, "what is this", "data:image/png;base64,abc")
|
||||
if _, err := a.RunStep(context.Background(), "s", []model.Message{msg}); err != nil {
|
||||
t.Fatalf("RunStep: %v", err)
|
||||
}
|
||||
if ad.gotReq.PersonaID != "" {
|
||||
t.Fatalf("req.PersonaID = %q, want empty for an unbound agent", ad.gotReq.PersonaID)
|
||||
}
|
||||
if ad.gotReq.Model != "vision-model" {
|
||||
t.Fatalf("req.Model = %q, want vision-model (the legacy image routing must survive)",
|
||||
ad.gotReq.Model)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user