从 Stainless 平滑迁移到 Scalar:一份不改写配置的 SDK 生成迁移实战指南
2026 年 5 月,Stainless 宣布加入 Anthropic 并逐步关停其全部托管产品(包括 SDK 生成器),新注册、新项目和新 SDK 当日即停止开放。对于所有依赖 Stainless 生成 SDK 的团队而言,问题只有一个:用户已经写了大量调用代码,如何在不破坏他们的情况下完成迁移?
本篇指南基于 Scalar 官方迁移文档(documentation/migration/stainless.md)写成,围绕 Scalar 的核心迁移设计理念——"你不应该重新编写任何东西"——完整讲解从导出 OpenAPI、导入 stainless.yml、校验 API 表面到接管发布管线的五步迁移路径,并结合作者仓库中的配置参考、自定义代码机制与源码实现展开纵深说明。读完你将掌握一套可复制的迁移流程,能够在保持既有调用点(call site)不变的前提下,用 Scalar 无缝接管原有 SDK 的生成与发布。
背景:Stainless 关停了什么,又留下了什么
Stainless 在公告中明确了两件事:
"As we focus on Claude Platform capabilities and connecting agents to APIs, we'll be winding down all hosted Stainless products, including our SDK generator. Starting today, new signups, projects, and SDKs will not be available."
- 关停的是全部托管产品,不仅是 SDK 生成器,也包括 Docs Platform;
- 已生成的代码归你所有,Stainless 明确表示:"As always, you own the SDKs you've generated to date, and have full rights to modify and extend them however you wish."
因此,你已经发布的 npm、PyPI、Maven、RubyGems 包会继续工作,用户照常安装;停止的是再生成(regeneration)——下一次 API 变更时,没有任何东西会自动更新。真正的"截止日期"不是 Stainless 的,而是你下一次 API 变更的那一天。更完整的行业背景与各方案评估见 Stainless wind-down 全景分析,逐项对比见 Scalar vs Stainless。
核心思想:为什么 stainless.yml 比 OpenAPI 规范更重要
很多人把迁移想成"我有一份 OpenAPI 文档,任何生成器都能读,所以随便挑一个就行"。这正是最容易踩坑的地方。
OpenAPI 文档是你的、可移植的、每个生成器都能读——但它描述的不是你的 SDK。真正承载 SDK 设计决策的是 stainless.yml:
- 哪些 operation 变成哪个 SDK 命名空间;
- 每个方法叫什么名字;
- 每个 list 端点用哪种分页方案;
- 你的包在每个语言里叫什么名字。
这些决策没有一项能在 OpenAPI 中表达。只用规范重新生成,你会得到一个"能用但表面完全不同"的 SDK:新命名空间、新方法名——对所有已安装你包的用户都是一次破坏性变更。
Scalar 的解法是直接读取 stainless.yml:resources、methods、sub-resources、models、pagination schemes、per-language 包名全部继承,因此用户已经写好的调用点继续工作。导入方式:在 Dashboard 新建 SDK 时选择 Import config,上传你的 stainless.yml;CLI 同样支持。
两个配置格式的键对键映射
"无缝迁移"不能靠一句口号,必须有可核对的键级映射。stainless.yml 与 Scalar 配置(参考 SDK 配置参考)的对应关系如下:
stainless.yml |
Scalar | 说明 |
|---|---|---|
resources(methods、models、subresources) |
resources(methods、models、subresources) |
直接对应,这是保住用户调用点的关键 |
targets |
targets |
共有语言直接对应,缺口见下 |
environments |
environments + environmentOrder |
Stainless 把首项当作默认;Scalar 显式声明顺序 |
client_settings |
clientSettings |
构造选项、认证值、默认头、环境变量名、超时、重试;Scalar 中键为 camelCase |
pagination |
pagination |
Scalar 的类型为 cursor、cursorId、cursorUrl、offset、pageNumber |
query_settings |
querySettings |
数组格式 comma、repeat、indices、brackets;嵌套格式 brackets 或 dots |
multipart_settings |
multipartSettings |
直接对应 |
security / security_schemes |
openapi.security / openapi.securitySchemes |
同一思想,不同嵌套层级 |
streaming |
streaming |
直接对应 |
settings |
settings |
两者都有但内容不同:Stainless 用于许可,Scalar 用于文件头与响应解包 |
organization |
name |
Scalar 取产品名,其余组织元数据无对应位置 |
明确不继承的部分(迁移前必须知晓):
targets: terraform与targets: sql在 Scalar 中没有等价物。如果你生成过 Terraform provider 或 SQL target,Scalar 无法替代它们;openapi.transforms没有对应实现。Scalar 的openapi键只承载 SDK 级覆盖(如代码示例语言、安全方案覆盖),不会重写输入文档。如果你用过 transforms,必须把变换后的输出作为输入,让两个生成器看到同一份 API——这是迁移 diff 出错最常见的单一原因,因为仓库里的文档并不一定是 Stainless 实际生成用的那份;edition、readme(README 示例选择)无直接对应;命名习惯上 Stainless 用snake_case,Scalar 用camelCase——这只是外观差异,不要被手抄 diff 吓到。
Scalar 还新增了 Stainless 没有的键——diagnostics(用规范质量门禁构建,见 diagnostics 指南)、ignoredEndpoints、customCasings、errors——迁移第一天完全可以忽略它们。
一个必须手工核查的行为差异
Stainless 会按命名约定自动为名为 list_* 的方法加分页,只有打破命名模式的方法才需要显式 paginated: true。也就是说,你配置里那些"隐式分页"的方法,是生成输出里第一个要核对的东西,而不是最后一个。
五步迁移流程
Step 1:导出你的 OpenAPI 文档
你已经有这份文档。唯一要检查的是:如果用过 openapi.transforms,仓库里的文档可能不是 Stainless 实际生成所依据的文档——取变换后的输出,保证两个生成器看到同一份输入。
文档中的 x-stainless-* 扩展可以原样保留:Scalar 会忽略它不认识的扩展,留在原地没有危害。这一点在源码中同样有据可查——Scalar 的代码示例 schema 在解析 OpenAPI 扩展时显式支持了 x-stainless-snippets 和 x-stainless-examples(见 packages/schemas/src/extensions/operation/x-code-samples.ts),与 x-codeSamples、x-custom-examples、x-readme、x-scalar-examples 一起被读取,这说明 Scalar 对 Stainless 产物的兼容是解析器层面的,而不是事后修补。
Step 2:直接拿走你的 stainless.yml
它就在你仓库的版本控制里。原样复制:不需要转换、不需要剥离任何内容、不需要先翻译成某种 Scalar 格式。
Step 3:导入 Scalar
新建 SDK → 选择 Import config → 同时上传 OpenAPI 文档和 stainless.yml。Scalar 读取配置,将其映射到自己的生成器上,为你配置的目标语言产出 SDK。没有中间格式、没有配置重写、不需要手工重新映射 API 表面。
Step 4:发布前认真验证
这一步保护的是你的用户。做法:对比生成的 api.md 与当前 SDK 的 api.md——两者都按资源分组列出每个方法,一次 diff 就能立刻看出公共表面是否完整。重点检查:
- 嵌套子资源上的方法名;
- list 端点的分页,尤其是使用自定义 cursor 的;
- 客户端类名,以及用户传凭据时用的环境变量名。
如果某处对不上,直接反馈给 Scalar 修正映射,比自己绕开问题更快。
Step 5:从你自己的仓库发布
生产仓库本来就是你的,npm、PyPI、Maven、RubyGems 包也是你的——迁移不会改变包名和注册表,用户继续安装今天安装的东西。变的是谁在推送那个仓库:
- 卸载 Stainless GitHub App;
- 接管
.github/workflows中的发布工作流; - 把 registry token 重新指向 Scalar 的发布流程。
Scalar 通过向你的仓库发起 Pull Request 的方式发布,因此每个 release 都保持可审查。
自定义代码:先识别,再再生
如果你编辑过生成文件,那些编辑就是仓库里的普通 commit——Stainless 通过语义三方合并(semantic three-way merge)应用它们,所以手写改动与生成代码交错存放,而不是作为独立的补丁集保存。
再生成之前,先识别哪些文件承载了手写改动。Scalar 支持自定义代码,迁移顺畅与否,取决于你是否提前知道要保留哪些文件。Scalar 的机制(详见 自定义代码指南):
- 每次构建都做三方合并:比较先前生成的代码、新生成的代码、你仓库的当前状态,然后落到
scalar-next分支——未被改动的生成文件干净更新,你的编辑被保留,你新增的文件不受影响; - 把定制提交到
scalar-next集成分支,绝不提交到存放纯净生成输出的scalar-generated; - 下次构建从最新 OpenAPI 再生成并把你的改动合并进
scalar-next; - 发布 PR 由 Scalar 从
scalar-next对默认分支保持开启,大多数变更自动合并,只有真正的冲突需要处理(冲突会停在scalar-merge-conflict分支,可在 Dashboard 或 GitHub 上解决)。
实操建议:新文件放在独立路径里永远不会冲突,所以优先新增 helper 文件,而不是深挖进生成文件内部去编辑。
Scalar 生成什么样的 SDK
以下是来自公开 Warp SDK 的真实生成 TypeScript:
import WarpAPI from "warp-hr";
const client = new WarpAPI({
apiKey: process.env["API_KEY"], // defaults to the API_KEY env var
});
const list = await client.customWorkerFields.list();
错误是类型化的,status 集合由你的规范生成:
import { APIError } from "warp-hr";
try {
const list = await client.customWorkerFields.list();
} catch (err) {
if (err instanceof APIError) {
console.log(err.status, err.name, err.headers);
}
throw err;
}
如果你是 Stainless 用户,这些输出应该很眼熟:资源命名空间方法、类型化错误、自动分页、支持 Retry-After 的重试、零运行时依赖(除非你启用了需要依赖的功能)。Warp 包就带着 "dependencies": {} 发布。这种接近并非巧合——生成 SDK 是公共 API 契约,Stainless 建立的约定已经印在大量开发者的肌肉记忆里,无谓的分歧只会让用户买单;从迁移角度看,这意味着你的调用点继续工作。
如果你的文档站用了 Docs Platform
Stainless 的文档平台是一个 Astro 项目,仓库托管在 stainless-sdks GitHub 组织下而不是你的组织。Stainless 自己的指引是:fork 出来,自行接管 CI、部署、域名和全部运维工作。
如果不想自己运维这套,Scalar Docs 可以用同一份 OpenAPI 文档渲染 API 参考,配合 Markdown 和 MDX 指南,用一份 scalar.config.json 控制导航与主题——本仓库的文档站本身就是这么构建和托管的(assetsDir、navigation、siteConfig 均在此文件中配置)。
无论选谁,你都会保留的东西
Stainless 说得清楚:已经生成的 SDK 属于你,有完整的修改与扩展权利。没有哪条截止日期会弄坏任何已发布的东西——唯一的问题是下一次 API 变更时会发生什么。迁移窗口期值得立刻做的三件事(即使你还没决定去向):
- 立刻快照当前公共 API 表面——一份
api.md,或一个遍历已发布包并导出每个方法签名的脚本。没有它,你无法证明迁移是非破坏性的; - 再生成任何东西之前,找到你的手写代码——Stainless 把自定义代码三方合并进生成文件,越早定位受影响文件,任何生成器都处理得越好;
- 如果用了 Docs Platform,现在就把仓库从
stainless-sdks组织拿出来——它是 fork 加 CI 设置,趁源码还在时操作最容易。
迁移完成后,用 api.md 的 diff 作为回归护栏:把它放进 CI,让每一次再生成都先证明"公共表面没有漂移",再考虑发布。
本文依据当前仓库中 Stainless 迁移指南、Scalar vs Stainless 对比、wind-down 全景分析 及 SDK 配置参考 撰写。Stainless 于 2026 年 5 月宣布关停,其文档可能变更或下线;文中涉及 Stainless 的陈述以其官方公告与文档为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.24 K638- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python670
SlideSCIPPT插件,支持素材库、AI助手、一键添加图片标题,复制粘贴位置、一键图片对齐、一键插入Markdown(加粗、超链接等行内样式、代码块、LaTeX等块级样式)、便捷导出图片!C#230
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python52874
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go22545
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java36351