flysafe 迁移指南:从 anthropic.New(BaseURL hack) 到 minimax.New(ModeAnthropic)¶
适用版本:flyto-agent core PR1(anthropic 数据驱动能力)+ PR2(能力机制推广到 6 个 provider),2026-04-11 推送到主干之后. 面向读者:flysafe SDK 维护者. 文档状态:已验证(2026-04-11).代码路径分析 + curl 双路实测 + Go 端到端 integration test 三重通过,
minimax.New(ModeAnthropic)的构造链已补齐与anthropic.New()等同的 5 个api.ClientOption(含 classifier / retry / overflow).置信度 100%.
1. TL;DR¶
- flysafe 目前用
anthropic.New(Config{BaseURL: "https://api.minimaxi.com/anthropic", BearerAuth: true, …})把 MiniMax 的 Anthropic 兼容端点冒充成 Anthropic,这是历史 hack. - PR1+PR2 落地之后,这种用法会出现两个新坑:
- thinking false-positive warning:
WarningEvent{Code:"feature_unsupported"}误报,模型其实支持; - caching 阈值被保守兜底:
CachingMinTokens回退到 4096(而正确值是 1024),short system prompt 的 caching silent 失效. - 推荐迁移:改用
minimax.New(minimax.Config{Mode: minimax.ModeAnthropic, …}),HTTP wire 格式完全等同,能力决策走 minimax 静态表,两个坑同时消除. - 次选:保留 anthropic hack,但在 engine 初始化时往
ModelRegistry注入"MiniMax-M2.7-highspeed"的ModelInfo.PR1 的跨 provider 透明性保证 anthropic helper 会优先读 registry. - 不推荐:
anthropic.Config.ModelOverrides,verify 后确认不 work(只影响Models()返回值,不改变 helper 决策路径).
2. 历史背景:flysafe 当前的 hack 用法¶
flysafe 是 flyto-agent core 的首个消费者,使用 MiniMax 的 Token Plan Key(MINIMAX_TOKEN_PLAN_KEY).这种 key 有两个特点:
- 免费额度按 call 次数限额,不按 token 计费;
- 必须通过 MiniMax 的 Anthropic 兼容端点调用:
https://api.minimaxi.com/anthropic/v1/messages.
在 minimax provider 还没有 ModeAnthropic 实现,或者 flysafe 不知道其存在的时候,最自然的实现方式是复用 anthropic provider 的代码路径 + 改 BaseURL:
import "git.flytoex.net/yuanwei/git.flytoex.net/yuanwei/flyto-agent/pkg/providers/anthropic"
p := anthropic.New(anthropic.Config{
APIKey: os.Getenv("MINIMAX_TOKEN_PLAN_KEY"),
BaseURL: "https://api.minimaxi.com/anthropic",
BearerAuth: true,
EnableCaching: true,
ThinkingBudget: 8000,
})
// 然后 Request.Model = "MiniMax-M2.7-highspeed"
为什么这个 hack 能工作¶
HTTP wire 层面,MiniMax 的 Anthropic 兼容端点真的是 Anthropic 协议:
- POST
/anthropic/v1/messages - Bearer auth(
Authorization: Bearer $KEY),不是 Anthropic 官方的x-api-keyheader - 请求体与响应体的 JSON schema 基本对齐 Anthropic
- SSE 事件序列完整(
message_start→content_block_start→content_block_delta→ ... →message_stop)
minimax provider 代码中 provider.go:17-20 的 LEGACY 注释也明确记录了这条历史路径:
// 历史包袱(LEGACY): flysafe 早期通过 BaseURL="https://api.minimaxi.com/anthropic"
// 使用 MiniMax,走的是 Anthropic 兼容端点。
// 迁移路径:flysafe 改用 minimax.New(Config{APIKey: "..."}) 即可,
// 无需关心底层使用哪种格式。
在 PR1 之前,anthropic provider 的能力决策基本是 no-op(caching 全开,thinking 按 model ID 前缀判断),flysafe 的 hack 用法几乎零副作用.问题从 PR1 落地开始出现.
3. PR1+PR2 带来的问题¶
3.1 PR1+PR2 做了什么¶
RFC 全文参见 data-driven-capabilities-rfc.md.这里只讲对 flysafe 有影响的两点:
- 能力决策数据化.anthropic provider 的
SupportsThinking/CachingMinTokens等能力不再硬编码,而是走req.Capabilities(engine 从ModelRegistry注入).包内静态表(anthropicModels)降级为兜底. - 双开关协议(want × can).用户 want 某能力(
Config.ThinkingBudget > 0)但模型 can=false 时,provider 会 emitWarningEvent{Code:"feature_unsupported"},把 silent disable 转成 loud warning.
核心 helper 的伪代码形态:
func (p *Provider) resolveThinkingSupport(req *flyto.Request) bool {
if req.Capabilities != nil {
return req.Capabilities.SupportsThinking
}
// 兜底路径:扫包级静态表
return modelSupportsThinkingFallback(req.Model)
}
这里关键的一点是:anthropic provider 的兜底只扫 anthropicModels,不知道 MiniMax 模型的存在.flysafe 的 model ID "MiniMax-M2.7-highspeed" 在 anthropicModels 里一定查不到.
3.2 坑 A:thinking false-positive warning¶
触发路径:
- flysafe 调用
p.Stream(ctx, req),req.Model = "MiniMax-M2.7-highspeed",req.Capabilities = nil(没经过 engine 注入). - anthropic provider 的
detectFeatureWarnings(req)里: req.NeedsThinking == true或者Config.ThinkingBudget > 0→ want=trueresolveThinkingSupport(req)→req.Capabilities为 nil → 走兜底modelSupportsThinkingFallback("MiniMax-M2.7-highspeed")→ 扫anthropicModels静态表查不到 → false- want=true, can=false → emit
WarningEvent{Code:"feature_unsupported", Message:"... MiniMax-M2.7-highspeed 不支持 thinking ..."}.
实际真相:MiniMax-M2.7-highspeed 是支持 thinking 的.minimax provider 的静态表(minimaxModels)里 SupportsThinking=true.
行为后果: - thinking budget 仍然会注入到请求里(warning 只是提示,不阻断),所以运行结果零回归; - 但是 flysafe 的 observability / log 会产生误报 warning; - 用户看到 warning 会怀疑是不是模型配置错了,引入额外的心智负担.
3.3 坑 B:caching 阈值被保守兜底(silent 性能 degrade)¶
触发路径:
req.Capabilities = nil.- anthropic 的
resolveCachingMinTokens(req)兜底扫anthropicModels查"MiniMax-M2.7-highspeed"→ 查不到 → 返回保守兜底 4096. - anthropic wire 层决定打
cache_control标记的条件是estimatedTokens >= minTokens.如果 flysafe 的 system prompt 估算 token 在 1024–4095 之间: - 用 minimax 真实阈值 1024:应该打 cache mark
- 用 anthropic 兜底阈值 4096:不会打 cache mark
- MiniMax 端收到请求但没有
cache_control标记 → 不命中 prompt cache → 按全量计费.
实际真相:minimax 静态表中 MiniMax-M2.7-highspeed 的 CachingMinTokens = 1024.
行为后果: - 没有任何错误,没有任何 warning; - flysafe 原本期望的 caching 省钱悄无声息失效; - 只有对比账单或抓 wire 才能发现.这是比坑 A 更危险的一种回归.
4. 推荐方案:迁移到 minimax.New(ModeAnthropic)¶
4.1 Before / After 代码对比¶
Before(现状 hack):
import "git.flytoex.net/yuanwei/git.flytoex.net/yuanwei/flyto-agent/pkg/providers/anthropic"
p := anthropic.New(anthropic.Config{
APIKey: os.Getenv("MINIMAX_TOKEN_PLAN_KEY"),
BaseURL: "https://api.minimaxi.com/anthropic",
BearerAuth: true,
EnableCaching: true,
ThinkingBudget: 8000,
})
After(推荐):
import "git.flytoex.net/yuanwei/git.flytoex.net/yuanwei/flyto-agent/pkg/providers/minimax"
p := minimax.New(minimax.Config{
APIKey: os.Getenv("MINIMAX_TOKEN_PLAN_KEY"),
Region: minimax.RegionChina, // 默认即为 China,可省略
Mode: minimax.ModeAnthropic, // 关键:走 Anthropic 兼容端点
ThinkingBudget: 8000,
})
几处关键变化:
- 不再需要手写
BaseURL:minimax.New内部根据Region和Mode自动拼https://api.minimaxi.com/anthropic(参见core/pkg/providers/minimax/provider.go:111-130). - 不再需要
BearerAuth: true:minimax provider 在ModeAnthropic分支内部 appendapi.WithBearerAuth()(参见core/pkg/providers/minimax/provider.go:129). - 不再需要
EnableCaching:minimax.Config根本没有这个字段,minimax wire 层自动处理 caching,不需要消费者开关. ThinkingBudget字段名相同,语义一致.
4.2 HTTP wire 层等价性¶
以下两条路径在 HTTP 线上完全等同:
| 维度 | anthropic.New + BaseURL hack | minimax.New(ModeAnthropic) |
|---|---|---|
| URL | https://api.minimaxi.com/anthropic/v1/messages |
https://api.minimaxi.com/anthropic/v1/messages |
| Auth header | Authorization: Bearer $KEY |
Authorization: Bearer $KEY |
| 请求体 JSON schema | Anthropic messages API | Anthropic messages API |
| SSE event 序列 | Anthropic 协议 | Anthropic 协议 |
4.3 为什么迁移成本低¶
- 调用侧 API 变化仅是 import 路径 + 字段名删减,没有语义变化.
- Go 类型检查会直接提示
EnableCaching字段不存在,迁移漏改点有编译器兜底. - Request / Response 类型(
flyto.Request/flyto.Event)保持不变. - 能力决策走 minimax 静态表(
minimaxModels): MiniMax-M2.7-highspeed:SupportsThinking=true,CachingMinTokens=1024→ 两个坑同时消失.- 还可以顺带拿到 minimax provider 的
detectFeatureWarnings语义(dispatcher 层统一,两条 stream path 共用,见core/pkg/providers/minimax/provider.go:169-171).
5. 次选方案:保留 hack + 手动往 registry 注入 ModelInfo¶
如果 flysafe 因为某些原因(比如代码库里还有别的地方依赖 anthropic.Provider 的具体类型)短期不方便切 provider 实例类型,有一个零 provider 切换的补救方案.
5.1 原理:PR1 的跨 provider 透明性¶
PR1 设计时就明确约定:registry 注入的 req.Capabilities 优先于任何 provider 包内静态表.anthropic provider 的 helper 结构是:
if req.Capabilities != nil {
return req.Capabilities.SupportsThinking // 直接返回 registry 数据
}
// 否则才扫 anthropicModels 兜底
这意味着:只要 registry 里有 "MiniMax-M2.7-highspeed" 的条目,哪怕 provider 是 anthropic 代码路径,能力决策也会走 registry 数据,两个坑同样消失.
5.2 示例代码¶
import (
"git.flytoex.net/yuanwei/git.flytoex.net/yuanwei/flyto-agent/pkg/config"
"git.flytoex.net/yuanwei/git.flytoex.net/yuanwei/flyto-agent/pkg/flyto"
)
// 在 engine 初始化处(flysafe 自己的 main/bootstrap)
registry := config.NewModelRegistry()
registry.Register("MiniMax-M2.7-highspeed", &flyto.ModelInfo{
ID: "MiniMax-M2.7-highspeed",
SupportsThinking: true,
SupportsCaching: true,
CachingMinTokens: 1024,
ContextWindow: 205_000,
// 其他字段按需补齐
})
// 然后把 registry 挂到 engine 上,engine 会在每次 Stream 前
// 把 ModelInfo 填到 req.Capabilities。
5.3 次选方案的代价¶
- 代价是 flysafe 需要自己维护一份 MiniMax 的 ModelInfo,和 minimax provider 的静态表人肉保持同步;
- 未来 minimax provider 升级(比如增加新模型,调整 CachingMinTokens)时,flysafe 这边拿不到自动更新;
- 但是紧急修复两个坑时这是最小侵入的选项.
6. 不推荐方案:anthropic.Config.ModelOverrides¶
看起来 anthropic provider 的 Config 里有个 ModelOverrides []flyto.ModelInfo 字段,直觉上应该能解决问题:
// 直觉上以为这样就能救
p := anthropic.New(anthropic.Config{
// ... 老的 hack 字段
ModelOverrides: []flyto.ModelInfo{{
ID: "MiniMax-M2.7-highspeed",
SupportsThinking: true,
CachingMinTokens: 1024,
}},
})
但这个方案不 work.verify 过代码后确认:
ModelOverrides字段的作用范围只影响Provider.Models()方法的返回值(给 CLI / probe 工具列模型用);resolveThinkingSupport/resolveCachingMinTokens等 helper 只扫anthropicModels包级变量,不读Config.ModelOverrides.
所以注入 ModelOverrides 之后:
- p.Models() 里会出现 MiniMax-M2.7-highspeed(没用);
- 但 warning / caching 决策路径完全不变,两个坑都原样存在.
如果后续有人想改 anthropic provider 让 ModelOverrides 真的参与能力决策,那是另一个 RFC 的事.目前不要依赖这条路径.
7. 验证状态¶
flyto-agent core 维护者做过的三重验证:
7.1 代码路径分析¶
对比 anthropic.New(BaseURL hack) 和 minimax.New(ModeAnthropic) 的初始化路径,确认 HTTP wire 层完全等同:
- 都实例化
internal/transport下的api.Client(package 名为api) - 都使用
api.WithBearerAuth() - baseURL 都是
https://api.minimaxi.com/anthropic - message path 都是
/v1/messages - 2026-04-11 起,两者的
api.ClientOption链完全对等:WithMessagePath/WithAPIVersion("2023-06-01")/WithClassifier(AnthropicClassifier)/WithRetryPolicy(NewAnthropicRetryPolicy)/WithOverflowHandler(DefaultOverflowHandler)/WithBearerAuth.见core/pkg/providers/minimax/provider.go:122-149.
7.2 curl 双路实测(2026-04-11)¶
从 flyto-agent core 的维护者环境直接用 curl 打 MiniMax 的 Anthropic 兼容端点,复现命令:
非流式 + 不带 anthropic-version header:
curl -sS -D /tmp/h.txt -o /tmp/b.json \
-X POST "https://api.minimaxi.com/anthropic/v1/messages" \
-H "Authorization: Bearer $MINIMAX_TOKEN_PLAN_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-M2.7-highspeed",
"max_tokens": 64,
"messages": [{"role":"user","content":"Say hello."}]
}'
# 预期:HTTP/1.1 200 OK,body 为 Anthropic Message 格式
流式 SSE + 不带 anthropic-version header:
curl -sS -N \
-X POST "https://api.minimaxi.com/anthropic/v1/messages" \
-H "Authorization: Bearer $MINIMAX_TOKEN_PLAN_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-M2.7-highspeed",
"max_tokens": 64,
"stream": true,
"messages": [{"role":"user","content":"Say hello."}]
}'
# 预期:HTTP/1.1 200 OK,完整 Anthropic 协议 SSE 事件序列
# (message_start / content_block_start / content_block_delta ... / message_stop)
两路均 HTTP 200 通过.
7.3 Go 端到端 integration test(2026-04-11 补做)¶
新增文件:core/pkg/providers/minimax/provider_integration_test.go(//go:build integration tag 隔离,默认 go test ./... 不触发).
测试核心路径:
1. 从 core/.env 加载 MINIMAX_TOKEN_PLAN_KEY
2. 构造 minimax.New(Config{APIKey: key, Region: RegionChina, Mode: ModeAnthropic})
3. 发一个最小 prompt("Say ONE word: OK")走完整 provider.Stream(ctx, req) 链路
4. 断言 1: 收到 *flyto.UsageEvent(provider 层的流终止信号;DoneEvent 由 engine 层追加,不在 provider channel 中)
5. 断言 2: 拼接 TextEvent + TextDeltaEvent 的内容包含 "OK"
6. 断言 3: 没有任何 WarningEvent{Code:"feature_unsupported"}(minimax provider 走自己的 minimaxModels 静态表,M2.7-highspeed.SupportsThinking=true 被正确识别)
运行命令:
go test -tags integration -run TestIntegration_ModeAnthropic_TokenPlan \
./pkg/providers/minimax/ -v
实测结果(2026-04-11,补齐 robustness options 之前 + 之后各跑一次):
=== RUN TestIntegration_ModeAnthropic_TokenPlan
provider_integration_test.go:120: ✓ 端到端通过: model=MiniMax-M2.7-highspeed
mode=Anthropic text="OKOK"
events=map[text:1 text_delta:1 thinking:1 thinking_delta:5 usage:1]
--- PASS: TestIntegration_ModeAnthropic_TokenPlan (4.07s)
两次跑都通过.text="OKOK" 是因为 TextDeltaEvent 增量和 TextEvent 完整块都包含 "OK",拼接后是 "OKOK".收到的 thinking_delta 事件证明 M2.7-highspeed 的 thinking 协议完整工作.
7.4 置信度与建议¶
- 代码路径等价 + curl 双路 + Go 端到端 integration test,置信度 100%.
- 建议 flysafe 的切换步骤:
- 在 staging / dev 环境切到
minimax.New(ModeAnthropic); - 跑 flysafe 的主回归集,对比 Before/After 的 log,token 消耗,warning 事件;
- 特别关注:之前的
WarningEvent{Code:"feature_unsupported"}应该消失;caching 命中率应该提升(阈值从 4096 降到 1024,更多短 system prompt 能打上 cache,账单应该下降); - 无异常后切全量生产.
- 回滚方案:一行 git revert 即可.
minimax.New(ModeAnthropic)和anthropic.New(BaseURL hack)在 HTTP wire 层完全等同,server side 看不出差别,回滚零风险.
8. ModeAnthropic 构造链的演化(2026-04-11 补齐 robustness options)¶
8.1 此前缺失的 4 个 option(历史)¶
在 2026-04-11 之前,minimax.New(ModeAnthropic) 在构造 api.Client 时相比 anthropic.New() 少注入了 4 个 option.这些都不影响成功路径,但在错误 / 重试 / 异常处理上会有可观察差异:
| 缺失的 option | anthropic.New 行为 | 旧 minimax.New(ModeAnthropic) 行为 | 影响 |
|---|---|---|---|
WithAPIVersion("2023-06-01") |
注入 anthropic-version header |
不发 anthropic-version header |
无影响.minimax 端不要求这个 header(curl 实测确认). |
WithClassifier(AnthropicClassifier) |
错误消息按 Anthropic 错误码分类 | 走 DefaultClassifier,错误消息通用化 |
弱影响.出错时错误消息可读性稍差,但错误码和 retryable 判定不受影响. |
WithRetryPolicy(NewAnthropicRetryPolicy) |
针对 429 / 529 有专用重试 backoff | 使用默认重试策略 | 中等影响.生产环境遇到 MiniMax 端限流 / 过载时,重试行为没有针对性优化. |
WithOverflowHandler(DefaultOverflowHandler) |
自动修正 max_tokens 超过模型上限的情况 |
不做修正,错误直接抛回调用方 | 极低影响.只有当 flysafe 显式传超限 max_tokens 才触发. |
8.2 2026-04-11 补齐¶
core/pkg/providers/minimax/provider.go:122-149 已更新 ModeAnthropic 分支,完整对齐 anthropic/provider.go:269-283 的构造链:
case ModeAnthropic:
var opts []api.ClientOption
opts = append(opts,
api.WithMessagePath("/v1/messages"),
api.WithAPIVersion("2023-06-01"),
api.WithClassifier(&api.AnthropicClassifier{Hinter: &api.DefaultHinter{}}),
api.WithRetryPolicy(retry.NewAnthropicRetryPolicy(retry.AnthropicRetryOpts{})),
api.WithOverflowHandler(retry.DefaultOverflowHandler()),
)
if cfg.HTTPClient != nil {
opts = append(opts, api.WithHTTPClient(cfg.HTTPClient))
}
opts = append(opts, api.WithBearerAuth())
p.anthroClient = api.NewClient(cfg.APIKey, baseURL+"/anthropic", opts...)
补齐后的端到端验证:integration test 双重跑通过(补齐前 + 补齐后各一次),成功路径不受影响;错误 / 重试 / 溢出路径的 robustness 现在与 anthropic.New() 完全对等.
8.3 如果之后发现新缺陷¶
如果 flysafe 使用中发现 minimax.New(ModeAnthropic) 与 anthropic.New() 还有行为差异(除了 endpoint 和 Bearer auth 这种故意的差异),不要在 flysafe 侧 workaround,请回 flyto-agent core 提 issue / PR.理由:
- 这些属于 provider 级别的 protocol 细节,是 core 的责任边界;
- flysafe 侧的 workaround 会在未来 core 修复时形成双重重试之类的隐患.
9. 支持 / 提问渠道¶
TODO:待定.目前可以在 flyto-agent core 仓库提 issue,或在团队 IM 找 core 维护者.
附录:相关文件 / 行号参考¶
- minimax provider 主文件:
core/pkg/providers/minimax/provider.go ModeAnthropic常量定义:provider.go:45-48- LEGACY flysafe 注释:
provider.go:17-20 New(cfg Config)构造:provider.go:103-146ModeAnthropic分支构造 anthroClient:provider.go:121-130Streamdispatcher(统一 warning 检测):provider.go:158-180- 数据驱动能力 RFC:
docs/architecture/data-driven-capabilities-rfc.md - 数据驱动能力迁移记录:
docs/architecture/data-driven-capabilities-migration.md - minimax provider 文档:
docs/providers/minimax.md(§11 同步了消费者注意事项)