关于 runpod-migrate
runpod-migrate 是指导 Agent 将 Runpod GraphQL 和 REST v1 调用迁移到 REST v2 的技能,面向维护 Runpod 代码库的开发者。它通过扫描调用点、区分 API 范围并逐步验证,帮助识别破坏性变更和必须保留的旧接口。
主要功能
- 01扫描并分类 Runpod API 调用
- 02区分控制平面与 Serverless API
- 03逐项核对字段和路径变更
- 04标记 v2 不再支持的能力
- 05提供迁移后的验证流程
适用场景
- 01盘点代码库中的旧版 API 调用
- 02将 REST v1 或 GraphQL 调用迁至 v2
- 03评估迁移中移除的功能依赖
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ runpod/ runpod-plugins-official --skill runpod-migrate安装后在 Claude Code 中输入/runpod-migrate确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/availability-and-fallback.eval.mdinventory-before-editing.eval.mdrest-only-scope.eval.mdreference/breaking-changes.mdgraphql-to-v2.mdrest-v1-to-v2.mdrollback-flag.mdunlocks.mdworked-example.mdscripts/rp_api_inventory.pytests/test_rp_api_inventory.py详细介绍
迁移到 Runpod REST v2
将代码库从 GraphQL API(api.runpod.io/graphql)和 REST v1
(rest.runpod.io/v1)迁移到 REST v2(api.runpod.io/v2)。
一句话说明收益——在第 6 步,结合用户的实际代码,向他们介绍这些内容。现在不要逐条念出来:
- 租用前先查看库存——
GET /v2/catalog/gpus?include=AVAILABILITY&product=POD。v1 完全没有目录,因此每个容量重试循环都只能盲目运行。 - 端点会返回自己的作业 URL——使用
requestUrls.run,无需再拼接字符串。 - 真实的生命周期状态——提供
PROVISIONING/STARTING/ERROR和actions列表,让等待循环可以快速失败,而不是一直等到超时。 - 错误会明确报出——未知请求字段会按名称被拒绝,并返回结构化错误和准确的状态码。
完整内容按 如果他们的代码执行 X → v2 提供 Y 的方式整理: reference/unlocks.md——在第 6 步打开。
修改代码之前
推断范围,说明判断,然后继续——不要一上来就问一连串问题:
| 用户说法 | 范围 |
|---|---|
| “迁移到 v2”/没有具体说明 | all——REST v1 和 GraphQL |
| “只迁移 REST 相关内容”,“保留 GraphQL” | rest——仅 REST v1 |
| “帮我们停用 GraphQL” | graphql——仅 GraphQL |
该表涵盖了所有说法,因此无需为确定范围而打断用户。说明你匹配的是哪一行,然后继续。真正需要提问的问题稍后才会出现——在第 3 步,清单显示代码依赖某项已被 v2 移除的功能时。这才是真正需要用户做选择的分歧,你无法代替他们回答。
有些功能没有 v2 对应项,无论范围如何,都必须继续使用 GraphQL:
账户/账单身份信息(myself)和 spot/可中断 Pod。即使是“完整”迁移,这些调用也仍需保留——请提前说明,不要让用户到最后才发现。
绝不要重写无服务器作业 API。 https://api.runpod.ai/v2/<endpointId>/run、/runsync、/status、/stream、/cancel 属于另一套 API,只是路径中恰好包含 v2。它保持不变,不在迁移范围内。清单会单独报告它,因此不要碰它。
工作流程
1. 清点——绝不要迁移尚未统计的内容
扫描器与本文件放在一起,位于已安装的技能目录中,而不是用户的代码库中。先解析它的路径;你的工作目录是用户的项目:
# 1. Claude Code plugin installs expose the plugin root:
SCAN="$CLAUDE_PLUGIN_ROOT/skills/runpod-migrate/scripts/rp_api_inventory.py"
# 2. Otherwise substitute the directory you loaded this SKILL.md from — you know it:
[ -f "$SCAN" ] || SCAN="<directory containing this SKILL.md>/scripts/rp_api_inventory.py"
# 3. Last resort, search the usual install roots:
[ -f "$SCAN" ] || SCAN=$(find ~/.claude ~/.agents ~/.codex ~/.config -name rp_api_inventory.py 2>/dev/null | head -1)
python3 "$SCAN" --help >/dev/null || echo "scanner not found — resolve it before continuing"
然后,在用户代码库的根目录运行:
python3 "$SCAN" . > runpod-api-inventory.md
python3 "$SCAN" . --json > runpod-api-inventory.json # if you want to drive edits from it
python3 "$SCAN" . --scope rest # REST-only migrations
runpod-api-inventory.md 会生成在用户的代码库中——请告知用户,并在交还迁移结果前将其删除或加入 gitignore。
仅使用 Python 标准库,无需安装。它会按 API 代际归类并报告每个调用位置——GraphQL、REST v1、v1/GraphQL 字段名、REST v2 现有调用、无服务器作业 API、SDK/CLI 封装——并给出建议的逐文件处理顺序。
开始编辑前,先向用户展示清单表格。 用户往往并不知道自己在用哪个版本:Agent 几个月前替他们选了版本,却没有留下任何记录。“3 个文件使用 v1、2 个使用 GraphQL、1 个已经使用 v2、2 个使用作业 API——后者不要动”往往是这整个技能流程中最有用的结果。
它能检测什么,以及不能检测什么
它通过正则表达式逐行扫描,但分类功能才是它实用的关键——单纯执行
grep -r runpod 会主动误判两件事:
api.runpod.ai/v2与api.runpod.io/v2。 两者只差一个字母。.ai是无服务器 作业 API,不能动;.io是你要迁移到的控制平面。 搜索v2会让你误以为代码库“已经迁移”,但事实并非如此。- 两个版本中都合法的名称。
/pods既是 v1 路径,也是 v2 路径;["pods"]是 v2 信封解包;idleTimeout在 v1 中是顶层字段,在 v2 中则嵌套在workers下。扫描器会抑制同一行中带有 v2 上下文的匹配项,因此它报告的是仍需处理的工作,而不是某个词的所有出现位置。
它还会查找字段名,而不只是 URL,因此能发现那些从未写出“runpod”的文件:某个模块从包装器的返回值中读取 p["costPerHr"],既没有 URL,也没有导入语句或操作名称——而这正是 v2 重命名会悄无声息破坏的地方。
有四种情况它确实无法判断。每次都要手动检查:
| 盲点 | 如何排查 |
|---|---|
基础 URL 位于配置中,而非代码中(settings.yaml、.env、ConfigMap、Terraform) | 扫描器会读取这些文件,因此能发现 URL——但使用该 URL 的调用点在别处。搜索读取该配置键的代码。 |
由辅助函数拼接的路径——_url("pods", pod_id, "stop") | 会列在可能的间接调用点下。此项仅供参考,因为 resp.json()["pods"] 看起来完全相同。逐一打开检查。 |
SDK 包装器(import runpod) | API 版本取决于已安装的版本,而非代码。检查 requirements.txt / 锁文件以及 SDK 自身的发行说明。 |
| 生成的客户端 | OpenAPI/GraphQL 文档才是真正的来源。应根据 v2 规范重新生成,而不是编辑生成的文件。 |
然后阅读扫描器标记的代码。它能找到调用点,但无法理解你的包装器。追踪调用这些代码的位置——响应字段重命名(如 costPerHr → cost)会破坏所有调用方,而不仅仅是请求构造器。如果已有代码图或 LSP 索引,这是最值得用到它的一步。
2. 在查看差异之前说明破坏性变更,而不是之后
阅读 reference/breaking-changes.md,并告诉用户哪些变更确实适用于他们的代码。变更分为两类,其中第二类才是他们真正担心的:
- 重命名和移动——明显可见。v2 会通过
422拒绝未知请求字段,并逐一列出字段名,因此遗漏重命名不会悄悄混入生产环境。 - 名称相同,含义不同——不易察觉,也是测试套件全绿并不能证明迁移正确的原因。参考文档列出了所有这类变更;影响最大的两项是:
flashboot从布尔值变成了包含三个值的枚举;v1 的/billing/endpoints(无服务器支出)在 v2 中变为/billing/serverless——v2 的/billing/endpoints计费的是另一种产品(公共端点),并会以200返回该产品的正确总额,但这并不是调用方所请求的产品。
3. 制定计划,区分必需项与清理项
编辑前先写下计划,并始终将以下类别分开,直到最终总结:
- 必需项 — 缺少这些内容,代码就无法在 v2 上运行。
- 清理项 — 无论如何都能运行,但 v2 允许你删除代码(手动拼接的作业 URL、手动实现的可用性重试、现在可以改用 SSE 的轮询循环)。
- 需要用户决定的事项 — 代码依赖 v2 已彻底移除的功能:抢占式/可中断实例、savings plans、放置约束(
countryCodes、minDownloadMbps等)、podreset、逐 pod 的 GPU 回退策略。请参阅 breaking-changes.md 中的 Class 3,并务必核对,而不要凭记忆判断,因为随着 v2 的发展,有些内容会移出这一类别。CUDA 固定、templateId和 CPU 端点写入过去都属于这一类别,现在已不再是。
第三类中的事项,写代码前先停下来询问用户,但要一并提供替代方案。其中有些已有可行的重构方案,有些确实没有;两者的区别决定了你要询问什么。对于已有重构方案的情况(countryCodes → 目录筛选 + dataCenterIds + 放置断言,逐 pod 的 GPU 回退策略 → 按可用性排序的循环),请展示方案,并只询问会影响方案的那个问题——对于 countryCodes,应询问该限制是偏好还是合规要求。对于没有替代方案的情况,可选方案是接受行为变更、继续通过 v1/GraphQL 执行该调用,或围绕它重新设计;只有用户能做出选择。
无论哪种情况,如果存在重构方案,就不要把移除功能说成无路可走——这会导致用户继续保留本不必保留的 v1 调用。也不要用 # no v2 equivalent 注释直接删掉字段:这正是设置这一类别要防止的情况,因为它会悄无声息地改变用户基础设施的行为。如果这一类别为空,就明确说明——这能让人放心,而且只需一行。
4. 迁移:每个提交处理一个文件
按扫描器建议的顺序处理(调用点最少的优先)——但有一个例外: 如果多个调用点共用一个传输辅助函数,先迁移该辅助函数,无论它有多少 调用点。扫描器按调用点数量排序,看不到导入关系,因此很可能会把消费者 排在它导入客户端的模块之前。先迁移消费者,意味着要基于一个即将更改的 接口编写代码。
逐个文件处理:
-
使用 reference/rest-v1-to-v2.md 或 reference/graphql-to-v2.md 映射路径和字段。如果字段不在 表格中,或 API 与表格不一致,请直接检查规范 — 事实依据。
-
在 pod 上使用
gpuTypeIds+gpuTypePriority,意味着你正在编写新代码,而不是 重命名字段。 v2 pod 只接受一种 GPU 类型,因此服务端回退机制要改为 客户端遍历目录。可用的实现: breaking-changes.md → 替换 GPU 回退列表。 Endpoint 无需循环 ——gpu.pools本来就是列表,worker 会分配到 任意有容量的所列 pool。 -
读取目录时始终请求可用性,并指定
product。 本次迁移新增的任何GET /v2/catalog/gpus、/catalog/cpus或/catalog/datacenters调用,都要添加include=AVAILABILITY(datacenter 使用GPU_AVAILABILITY/CPU_AVAILABILITY)。可用性是每位 Runpod 用户最关心的问题,而且 调用成本相同。不要因为现有代码没有请求它就省略——v1 无法做到这一点。单独使用
include=AVAILABILITY会返回400。 对/catalog/gpus和/catalog/cpus,必须同时指定product,否则无效——无论哪种情况都会返回400。这里有意不提供默认值: 同一款 GPU 对POD来说可能紧缺,对SERVERLESS来说却可能充足,因此必须注明 使用场景。选择与你要创建的对象匹配的值(POD、SERVERLESS或CLUSTER;CPU 使用POD或SERVERLESS):GET /v2/catalog/gpus?include=AVAILABILITY&product=POD -
v2 对用户来说还很新时,提供回滚标志(
RUNPOD_API_V1=1): reference/rollback-flag.md。对生产环境中的服务来说很值得; 一次性脚本则可以跳过。 -
绝不要在同一个提交中同时更改行为和 API 版本——包括 v2 带来的改进。遇到
status == "ERROR"时立即失败,而不是 等到超时,这确实是个改进,应该放到下一个提交中;将它合并到 迁移提交中意味着回滚时必须同时放弃这两项更改。将这些改进单独放入 清理类别。
真实客户端的完整迁移前后对比——包含 GPU 回退的 pod 创建、内联容器配置的 endpoint 创建,以及 GraphQL 仪表板——请参阅 reference/worked-example.md。
5. 对照在线 API 验证
静态审查还不够;v2 的验证器很严格,错误信息也很精准。 重新运行扫描器,确认调用点已清除,然后测试实际路径:
python3 "$SCAN" . --scope rest --fail-on-legacy # exit 1 if v1 remains
有两个标记,含义各不相同——不要用错:
| 标记 | 用途 |
|---|---|
rp-migrate: keep-v1 | 有意保留的旧版代码——RUNPOD_API_V1 回滚分支,或没有 v2 等效项的 GraphQL 调用。报告中列在 有意保留 下。 |
rp-migrate: ignore | 已正确代码中的误报。表示“这不是旧版代码”,而不是“这是我保留的旧版代码”。报告中列在 标记为误报 下。 |
两者都接受 line、start/end 区域或 file 范围,并且都会从迁移计划和
--fail-on-legacy 检查中排除。用 keep-v1 消除误报会让报告记录不实内容——遇到这种情况
应使用 ignore。
门禁仍可能出错的情况。 第 1 步中的相同盲点,在迁移后会产生相反影响:迁移前会漏掉 v1 代码,迁移后则可能误报正确的 v2 代码。扫描器会处理两种常见情况——行尾的 # was imageName 注释,以及存放在常量中的基础 URL(例如 f"{BASE}/pods",其中 BASE 是在文件任意位置定义的 v2 URL)。除此之外——例如从其他模块导入基础 URL,或在运行时根据配置构建 URL——扫描器仍可能把正确代码误判为旧版代码。请先阅读被标记的代码行,再相信退出代码;确认是误报时,用 ignore 标记,不要削弱门禁。
- 读取免费——列出 Pod、端点、卷和目录。确认你已拆开新的封装层(
{"pods": [...]},而不是裸数组)。 - 写入会产生费用。创建 → 断言 → 删除,并使用能够验证数据结构的最小对象。切勿针对用户已有的资源进行测试。
- 根据 reference/breaking-changes.md 中的表格解读
422错误——包括一种容易混淆的情况:缺少必填字段,会导致验证器将你的有效字段报告为“不允许的附加属性”。
6. 总结——这是他们实际会阅读的内容
大多数用户只看总结,不看差异。请严格按以下结构编写:
## Required for the migration
<file:line> — what changed and why it had to
## Cleanup enabled by v2
<file:line> — what got deleted or simplified
## Behavior changes to watch
the same-name-different-meaning items that applied
## Still on GraphQL (no v2 equivalent)
myself / spot pods — and why
## Unlocks: what you can build now
tied to what this codebase already does
最后一部分价值最高。不要粘贴通用功能列表——结合这位用户正在构建和处理的问题,包括你在当前会话中已经了解的任何信息,具体指出 v2 会如何改变这些情况。“你的 wait_until_running 循环在 Pod 失败时会超时;v2 的 ERROR 状态能让它在几秒内失败”,胜过“v2 提供了更丰富的状态值”。reference/unlocks.md 正是按照如果代码执行 X → v2 提供 Y 的结构编写的。
事实依据:自行检查规范
reference/ 中的映射表已于 2026-08-10 对照在线 API 验证。
v2 仍在积极开发中,因此应将这些表视为快速查询方式,而非权威依据。两份规范均为公开内容,无需身份验证:
curl -s https://api.runpod.io/v2/openapi.json -o /tmp/rp-v2.json
curl -s https://rest.runpod.io/v1/openapi.json -o /tmp/rp-v1.json
请求体实际接受哪些内容,以及哪些字段必填(*)。在编写任何创建调用之前,值得先运行此命令——它会解析 allOf 组合,而直接查看原始 JSON 时很容易遗漏:
python3 - CreateEndpointRequest <<'PY'
import json, sys
S = json.load(open("/tmp/rp-v2.json"))["components"]["schemas"]
def merge(n, acc=None):
acc = acc if acc is not None else {"props": {}, "req": set()}
if "$ref" in n: return merge(S[n["$ref"].split("/")[-1]], acc)
for sub in n.get("allOf", []): merge(sub, acc)
acc["props"].update(n.get("properties", {})); acc["req"].update(n.get("required", []))
return acc
def kind(v):
if "$ref" in v: return v["$ref"].split("/")[-1]
if "allOf" in v: return kind(v["allOf"][0])
return v.get("type", "?")
m = merge(S[sys.argv[1]])
for k, v in sorted(m["props"].items()):
print(f" {'*' if k in m['req'] else ' '} {k:16} {kind(v)}")
PY
将参数替换为 CreatePodRequest、UpdatePodRequest、CreateTemplateRequest、CreateNetworkVolumeRequest 等。哪些架构定义中提到了某个字段——当 422 错误指出某个你不知道该放在哪里的字段时,这很有用:
python3 -c 'import json,sys; S=json.load(open("/tmp/rp-v2.json"))["components"]["schemas"]; [print(" ",n) for n,s in S.items() if sys.argv[1] in json.dumps(s)]' flashboot
来源不一致时的优先级
实时观察到的行为 > 规范 > 这些表格。 规范并不总是正确的,已知规范有误的地方会在参考文档中注明——文档说明 timeout 的默认值为 300000 毫秒,但实际返回的是 0。如果遇到运行中的 API 与规范矛盾的情况,请以 API 为准,并在总结中说明这一点,让用户知道文档中的默认值不可信。
如果发现 reference/ 中的映射已与规范不符,请修正调用并标记差异——表格中附有验证日期,正是为了能发现过时内容,而不是让它悄然出错。
GraphQL 没有机器可读的架构(已禁用内省),因此无法用这种方式检查这一部分——请参阅 reference/graphql-to-v2.md 中的注意事项。
工具说明
清单扫描器有意设计为类似 grep 的脚本,而非代码图索引:API 的生成取决于 URL 字符串和字段名称;它必须适用于任意客户代码仓库中的任何语言,并且无需任何设置就能为所有用户给出相同结果。代码智能索引(LSP,或已在运行的 MCP 图服务)在另一个步骤中更有用——即步骤 1 的影响范围问题:“哪个调用方使用了这个封装层,而它的响应字段刚刚改名?”——而不是用于检测。如果它已经可用,就在这一步使用;不要仅为此专门启动一个。