mcpField
R
runpod-migrate

runpod-migrate

github.com/runpod开发4711.1k 次安装2 天前更新
代码仓库

关于 runpod-migrate

runpod-migrate 是指导 Agent 将 Runpod GraphQL 和 REST v1 调用迁移到 REST v2 的技能,面向维护 Runpod 代码库的开发者。它通过扫描调用点、区分 API 范围并逐步验证,帮助识别破坏性变更和必须保留的旧接口。

主要功能

  1. 01扫描并分类 Runpod API 调用
  2. 02区分控制平面与 Serverless API
  3. 03逐项核对字段和路径变更
  4. 04标记 v2 不再支持的能力
  5. 05提供迁移后的验证流程

适用场景

  1. 01盘点代码库中的旧版 API 调用
  2. 02将 REST v1 或 GraphQL 调用迁至 v2
  3. 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.md
inventory-before-editing.eval.md
rest-only-scope.eval.md
reference/
breaking-changes.md
graphql-to-v2.md
rest-v1-to-v2.md
rollback-flag.md
unlocks.md
worked-example.md
scripts/
rp_api_inventory.py
tests/
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,并告诉用户哪些变更确实适用于他们的代码。变更分为两类,其中第二类才是他们真正担心的:

  1. 重命名和移动——明显可见。v2 会通过 422 拒绝未知请求字段,并逐一列出字段名,因此遗漏重命名不会悄悄混入生产环境。
  2. 名称相同,含义不同——不易察觉,也是测试套件全绿并不能证明迁移正确的原因。参考文档列出了所有这类变更;影响最大的两项是:flashboot 从布尔值变成了包含三个值的枚举;v1 的 /billing/endpoints(无服务器支出)在 v2 中变为 /billing/serverless——v2 的 /billing/endpoints 计费的是另一种产品(公共端点),并会以 200 返回该产品的正确总额,但这并不是调用方所请求的产品。
3. 制定计划,区分必需项与清理项

编辑前先写下计划,并始终将以下类别分开,直到最终总结:

  • 必需项 — 缺少这些内容,代码就无法在 v2 上运行。
  • 清理项 — 无论如何都能运行,但 v2 允许你删除代码(手动拼接的作业 URL、手动实现的可用性重试、现在可以改用 SSE 的轮询循环)。
  • 需要用户决定的事项 — 代码依赖 v2 已彻底移除的功能:抢占式/可中断实例、savings plans、放置约束(countryCodes、minDownloadMbps 等)、pod reset、逐 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 的影响范围问题:“哪个调用方使用了这个封装层,而它的响应字段刚刚改名?”——而不是用于检测。如果它已经可用,就在这一步使用;不要仅为此专门启动一个。

评论

支持 Markdown