runpod-migrate

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Migrate to Runpod REST v2

迁移至Runpod REST v2

Moves a codebase off the GraphQL API (
api.runpod.io/graphql
) and REST v1 (
rest.runpod.io/v1
) onto REST v2 (
api.runpod.io/v2
).
The payoff, in one line each — you deliver these to the user at step 6, matched to their actual code. Don't recite them now:
  • See stock before you rent
    GET /v2/catalog/gpus?include=AVAILABILITY&product=POD
    . v1 had no catalog at all, so every capacity retry loop was blind.
  • Endpoints return their own job URLs
    requestUrls.run
    , no more string-building.
  • Real lifecycle states
    PROVISIONING
    /
    STARTING
    /
    ERROR
    and an
    actions
    list, so wait-loops fail fast instead of timing out.
  • Mistakes fail loudly — unknown request fields are rejected by name, with structured errors and honest status codes.
The full set, organized as if their code does X → v2 offers Y: reference/unlocks.md — open it at step 6.
将代码库从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中打开查看。

Before you touch any code

修改代码前的准备

Infer the scope, state it, and move — do not open with a questionnaire:
The user saysScope
"migrate to v2" / nothing specific
all
— REST v1 and GraphQL
"just the REST stuff", "leave GraphQL alone"
rest
— REST v1 only
"get us off GraphQL"
graphql
— GraphQL only
The table resolves every phrasing, so scope is not the thing to interrupt for. Say which row you matched and carry on. The question that does need asking comes later — at step 3, when the inventory shows the code depends on a capability v2 removed. That one is a real fork and you cannot answer it for them.
Some things have no v2 equivalent and must stay on GraphQL regardless of scope: account/billing identity (
myself
), secrets, spot/interruptible pods, cluster create/delete. A "full" migration still leaves those calls in place — say so up front rather than letting the user discover it at the end.
Never rewrite the serverless job API.
https://api.runpod.ai/v2/<endpointId>/run
,
/runsync
,
/status
,
/stream
,
/cancel
is a different API that happens to have
v2
in its path. It is unchanged and out of scope. The inventory reports it separately so you do not touch it.
推断迁移范围,明确告知后继续——不要以问卷形式开场:
用户表述迁移范围
"迁移至v2" / 无具体说明
all
——REST v1 GraphQL
"只处理REST相关内容"、"保留GraphQL不变"
rest
——仅REST v1
"脱离GraphQL"
graphql
——仅GraphQL
该表格可覆盖所有表述方式,因此无需因范围问题中断流程。说明你匹配的是哪一行,然后继续。后续有一个必须询问的问题——在步骤3中,当盘点结果显示代码依赖v2已移除的功能时,这是真正需要决策的节点,无法替用户回答。
部分功能没有v2等效替代方案,无论范围如何都必须保留在GraphQL:账户/账单身份(
myself
)、密钥、抢占式/可中断Pod、集群创建/删除。即使是「完整迁移」,这些调用也会保留——请提前告知用户,而非让他们在最后才发现。
切勿修改无服务器任务API
https://api.runpod.ai/v2/<endpointId>/run
/runsync
/status
/stream
/cancel
是一个独立的API,只是路径中包含
v2
。它未做变更且不在迁移范围内。盘点工具会单独报告这些调用,请勿修改。

The workflow

工作流程

1. Inventory — never migrate what you have not counted

1. 盘点——未统计的内容绝不迁移

The scanner ships beside this file, in the installed skill directory — not in the user's repo. Resolve its path first; your working directory is their project:
bash
undefined
扫描工具与本文档一同发布,位于已安装的技能目录中——而非用户仓库。首先确定其路径;你的工作目录是用户的项目:
bash
undefined

1. Claude Code plugin installs expose the plugin root:

1. Claude Code插件安装后会暴露插件根目录:

SCAN="$CLAUDE_PLUGIN_ROOT/skills/runpod-migrate/scripts/rp_api_inventory.py"
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:

2. 否则替换为你加载此SKILL.md的目录——你知道路径:

[ -f "$SCAN" ] || SCAN="<directory containing this SKILL.md>/scripts/rp_api_inventory.py"
[ -f "$SCAN" ] || SCAN="<包含此SKILL.md的目录>/scripts/rp_api_inventory.py"

3. Last resort, search the usual install roots:

3. 最后一种方法,搜索常见安装目录:

[ -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"

Then, from the root of the user's repo:

```bash
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
lands in the user's repo — mention it, and remove it or gitignore it before you hand the migration back.
Stdlib-only Python, no install. It reports every call site bucketed by generation — GraphQL, REST v1, v1/GraphQL field names, REST v2 already, serverless job API, SDK/CLI wrappers — plus a suggested file-by-file order.
Show the user the inventory table before editing anything. Users routinely do not know what they are on: an agent picked a version for them months ago and wrote it down nowhere. "3 files on v1, 2 on GraphQL, 1 already on v2, 2 on the job API — leave those alone" is often the single most useful output of this whole skill.
[ -f "$SCAN" ] || SCAN=$(find ~/.claude ~/.agents ~/.codex ~/.config -name rp_api_inventory.py 2>/dev/null | head -1) python3 "$SCAN" --help >/dev/null || echo "未找到扫描工具——请先解决此问题再继续"

然后,在用户仓库的根目录执行:

```bash
python3 "$SCAN" . > runpod-api-inventory.md
python3 "$SCAN" . --json > runpod-api-inventory.json   # 若要基于此文件驱动编辑
python3 "$SCAN" . --scope rest                          # 仅REST迁移场景
runpod-api-inventory.md
会生成在用户仓库中——告知用户这一点,并在交付迁移结果前删除或添加到.gitignore中。
该工具仅依赖Python标准库,无需额外安装。它会按API代际分类报告所有调用站点——GraphQL、REST v1、v1/GraphQL 字段名、已使用的REST v2、无服务器任务API、SDK/CLI包装器——并建议逐文件迁移的顺序。
在编辑任何内容前,先向用户展示盘点表格。用户通常不清楚自己使用的版本:可能是某个代理几个月前选择的版本,且未留下记录。“3个文件使用v1,2个使用GraphQL,1个已使用v2,2个使用任务API——这些无需修改”往往是整个技能最实用的输出。

What it detects, and what it cannot

扫描工具能检测和不能检测的内容

It is regex line-scanning, but the classification is what makes it usable — plain
grep -r runpod
gets two things actively wrong:
  • api.runpod.ai/v2
    vs
    api.runpod.io/v2
    .
    One letter apart.
    .ai
    is the serverless job API and must not be touched;
    .io
    is the control plane you are migrating to. Grepping for
    v2
    tells you the codebase is "already migrated" when it is not.
  • Names legal in both versions.
    /pods
    is a v1 path and a v2 path;
    ["pods"]
    is v2 envelope-unwrapping;
    idleTimeout
    is top-level in v1 and nested under
    workers
    in v2. The scanner suppresses a hit when the same line carries v2 context, so it reports work that remains rather than every occurrence of a word.
It also looks for field names, not just URLs, which is what catches the files that never spell "runpod": a module reading
p["costPerHr"]
off a wrapper's return value has no URL, no import, no operation name — and is exactly what a v2 rename breaks silently.
Four things it genuinely cannot resolve. Check them by hand, every time:
Blind spotHow to close it
Base URL lives in config, not code (
settings.yaml
,
.env
, a ConfigMap, Terraform)
The scanner does read those files, so the URL surfaces — but the call sites using it are elsewhere. Grep for whoever reads that config key.
Paths assembled by a helper
_url("pods", pod_id, "stop")
Reported under possible indirect call sites. Advisory, because
resp.json()["pods"]
looks identical. Open each one.
SDK wrappers (
import runpod
)
The API generation is a property of the installed version, not the code. Check
requirements.txt
/ lockfile and the SDK's own release notes.
Generated clientsThe OpenAPI/GraphQL document is the real source. Regenerate from the v2 spec instead of editing generated files.
Then read the code the scanner flagged. It finds call sites; it does not understand your wrappers. Trace who calls them — a renamed response field like
costPerHr → cost
breaks every caller, not just the request builder. This is the one step where a code-graph or LSP index earns its keep, if one is already available.
它基于正则表达式逐行扫描,但分类逻辑使其实用——单纯的
grep -r runpod
会犯两个严重错误:
  • api.runpod.ai/v2
    api.runpod.io/v2
    。仅一个字母之差。
    .ai
    是无服务器任务API,绝对不能修改;
    .io
    是你要迁移到的控制平面。仅通过
    grep
    查找
    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版本是已安装SDK的属性,而非代码的属性。检查
requirements.txt
/锁定文件以及SDK自己的发布说明。
生成的客户端OpenAPI/GraphQL文档是真实来源。从v2规范重新生成,而非编辑生成的文件。
然后查看扫描工具标记的代码。它能找到调用站点,但无法理解你的包装器。追踪调用这些包装器的代码——像
costPerHr → cost
这样的响应字段重命名会影响所有调用者,而非仅请求构建器。如果已有代码图或LSP索引,这一步会发挥作用。

2. Brief the breaking changes — before the diff, not after

2. 提前说明破坏性变更——在生成差异前,而非之后

Read reference/breaking-changes.md and tell the user which ones actually apply to their code. Two classes, and the second is the one they are afraid of:
  1. Renames and moves — loud. v2 rejects unknown request fields with
    422
    listing them by name, so a missed rename cannot slip into production silently.
  2. Same name, different meaning — quiet, and the reason a green test suite is not proof. The reference enumerates every one of them; the two that bite hardest:
    flashboot
    went from boolean to a three-value enum, and v1's
    /billing/endpoints
    (serverless spend) is v2's
    /billing/serverless
    — v2's
    /billing/endpoints
    bills a different product (public endpoints) and answers
    200
    with a correct total for that product, which is not the one the caller asked for.
阅读**reference/breaking-changes.md**,并告知用户哪些变更实际适用于他们的代码。分为两类,第二类是用户担心的:
  1. 重命名和移动——明显。v2会以
    422
    状态码拒绝未知请求字段并列出字段名称,因此遗漏的重命名不会静默进入生产环境。
  2. 同名但含义不同——隐蔽,这也是测试套件全绿不能作为验证依据的原因。参考文档列举了所有这类变更;其中最容易出问题的两个:
    flashboot
    从布尔值变为三值枚举,v1的
    /billing/endpoints
    (无服务器支出)对应v2的
    /billing/serverless
    ——v2的
    /billing/endpoints
    是针对另一产品(公开端点)的账单,会返回
    200
    状态码和该产品的正确总额,但这并非调用者所需。

3. Plan, split into required vs cleanup

3. 制定计划,分为必填项与清理项

Write the plan down before editing, and keep these buckets separate all the way through to the final summary:
  • Required — it does not work on v2 without this.
  • Cleanup — it works either way, but v2 lets you delete code (hand-built job URLs, hand-rolled availability retry, polling loops that can now be SSE).
  • Decisions the user must make — the code depends on something v2 removed outright: spot/interruptible pods, savings plans,
    dockerEntrypoint
    , placement constraints (
    countryCodes
    ,
    minRAMPerGPU
    , …), pod
    reset
    , per-pod GPU fallback. See breaking-changes.md Class 3 — and check it rather than working from memory, because things leave this bucket as v2 grows. CUDA pinning,
    templateId
    and CPU endpoint writes all used to be here and are not any more.
Stop and ask before writing code in that third bucket — but bring the replacement with you. Some of these have a working rebuild and some genuinely have nothing, and the difference decides what you are asking. Where a rebuild exists (
countryCodes
catalog filter +
dataCenterIds
+ a placement assert
, per-pod GPU fallback → an availability-ordered loop), show it and ask the one question that changes it — for
countryCodes
, whether the restriction was a preference or a compliance requirement. Where nothing exists, the options are accept the behavior change, keep that call on v1/GraphQL, or redesign around it, and only the user can pick.
Either way, do not present a removal as a dead end when a rebuild exists — that pushes the user into keeping a v1 call they did not need to keep. And never drop the field with a
# no v2 equivalent
comment: that is the failure mode this bucket exists to prevent, because it silently changes what their infrastructure does. If the bucket is empty, say so — that is reassuring and takes one line.
在编辑前写下计划,并在整个过程中直至最终总结都保持这些分类:
  • 必填项——不修改则无法在v2上运行。
  • 清理项——无论是否修改都能运行,但v2允许你删除代码(手动构建的任务URL、手动实现的可用性重试、现在可改用SSE的轮询循环)。
  • 用户必须决策的项——代码依赖v2已彻底移除的功能:抢占式/可中断Pod、储蓄计划、
    dockerEntrypoint
    、放置约束(
    countryCodes
    minRAMPerGPU
    等)、Pod重置、每Pod GPU fallback。查看breaking-changes.md的第三类——请查阅文档而非凭记忆,因为随着v2发展,部分功能会移出此类。CUDA固定、
    templateId
    和CPU端点写入曾属于此类,但现在已不再是。
在编写第三类的代码前,请停止并询问用户——但要提供替代方案。其中部分功能有可行的重构方案,部分则完全没有,这会影响你询问的内容。对于有重构方案的(
countryCodes
目录过滤器 +
dataCenterIds
+ 放置断言
,每Pod GPU fallback → 按可用性排序的循环),展示方案并询问唯一能改变决策的问题——比如对于
countryCodes
,限制是偏好还是合规要求。对于没有替代方案的,选项是接受行为变更、保留该调用在v1/GraphQL,或重新设计,只有用户能做出选择。
无论哪种情况,当存在重构方案时,不要将移除操作表述为死胡同——这会迫使用户保留原本无需保留的v1调用。切勿仅添加
# no v2 equivalent
注释就删除字段:这正是此类存在要避免的失败模式,因为它会静默改变基础设施的行为。如果此类为空,请明确说明——这能让用户放心,只需一句话。

4. Migrate, one file per commit

4. 迁移,每个文件对应一次提交

Work in the scanner's suggested order (fewest call sites first) — with one override: if several call sites share a transport helper, migrate the helper first, whatever its count. The scanner orders by call-site count and cannot see imports, so it will happily put a consumer ahead of the module it imports its client from. Migrating a consumer first means writing against an interface you are about to change.
Per file:
  • Map paths and fields with reference/rest-v1-to-v2.md or reference/graphql-to-v2.md. If a field isn't in the tables, or the API disagrees with them, check the spec directly — Ground truth.
  • gpuTypeIds
    +
    gpuTypePriority
    on a pod means you are writing new code, not renaming fields.
    A v2 pod takes one GPU type, so the server-side fallback becomes a client-side loop over the catalog. Working implementation: breaking-changes.md → Replacing the GPU fallback list. Endpoints need no loop
    gpu.pools
    is already a list and workers land on whichever listed pool has capacity.
  • Always request availability on catalog reads — with
    product
    .
    Any
    GET /v2/catalog/gpus
    ,
    /catalog/cpus
    , or
    /catalog/datacenters
    this migration introduces gets
    include=AVAILABILITY
    (
    GPU_AVAILABILITY
    /
    CPU_AVAILABILITY
    for datacenters). Availability is the top-of-mind question for every Runpod user and the call costs the same. Do not omit it because the current code did not ask for it — v1 could not.
    include=AVAILABILITY
    alone is a
    400
    .
    On
    /catalog/gpus
    and
    /catalog/cpus
    ,
    product
    is required with it and invalid without it —
    400
    either way. There is no default, deliberately: the same GPU can be scarce for
    POD
    and plentiful for
    SERVERLESS
    , so the context has to be stated. Pick the one matching what you are creating (
    POD
    ,
    SERVERLESS
    , or
    CLUSTER
    ; CPUs take
    POD
    or
    SERVERLESS
    ):
    GET /v2/catalog/gpus?include=AVAILABILITY&product=POD
  • Offer the rollback flag (
    RUNPOD_API_V1=1
    ) while v2 is new to them: reference/rollback-flag.md. Worth it for a service in production; skip it for a one-off script.
  • Never change behavior and API version in the same commit — including the improvements v2 makes possible. Failing fast on
    status == "ERROR"
    instead of timing out is a genuine win and it belongs in the next commit; folding it into the migration commit means a rollback has to give up both. Land those in the cleanup bucket, separately.
A full before/after of a real client — pod create with GPU fallback, endpoint create with the container config inlined, GraphQL dashboard — is in reference/worked-example.md.
按照扫描工具建议的顺序(调用站点最少的优先)进行——但有一个例外:如果多个调用站点共享传输辅助函数,先迁移辅助函数,无论其调用次数多少。扫描工具按调用站点数量排序,无法识别导入关系,因此可能会将消费者放在其导入的客户端模块之前。先迁移消费者意味着要针对即将变更的接口编写代码。
每个文件的处理步骤:
  • 使用**reference/rest-v1-to-v2.mdreference/graphql-to-v2.md**映射路径和字段。如果字段不在表格中,或API与表格不符,请直接查看规范——权威来源
  • 在Pod上使用
    gpuTypeIds
    +
    gpuTypePriority
    意味着你在编写新代码,而非重命名字段
    。v2的Pod仅支持一种GPU类型,因此服务器端fallback变为客户端遍历目录的循环。可行实现:breaking-changes.md → 替换GPU fallback列表端点无需循环——
    gpu.pools
    已是列表,Worker会自动部署到有容量的池。
  • 读取目录时始终请求可用性信息——并指定
    product
    。本次迁移引入的任何
    GET /v2/catalog/gpus
    /catalog/cpus
    /catalog/datacenters
    请求都要添加
    include=AVAILABILITY
    (数据中心对应
    GPU_AVAILABILITY
    /
    CPU_AVAILABILITY
    )。可用性是每个Runpod用户最关心的问题,且调用成本相同。不要因为当前代码未请求就省略——v1根本无法请求。
    include=AVAILABILITY
    会返回
    400
    。在
    /catalog/gpus
    /catalog/cpus
    上,使用
    include=AVAILABILITY
    时必须指定
    product
    ,否则会返回
    400
    。没有默认值,这是故意设计的:同一GPU对于
    POD
    可能稀缺,但对于
    SERVERLESS
    可能充足,因此必须明确上下文。选择与你要创建的资源匹配的值(
    POD
    SERVERLESS
    CLUSTER
    ;CPU对应
    POD
    SERVERLESS
    ):
    GET /v2/catalog/gpus?include=AVAILABILITY&product=POD
  • 提供回滚标志
    RUNPOD_API_V1=1
    ),当用户刚接触v2时很有用:reference/rollback-flag.md。对于生产环境的服务值得添加;一次性脚本可跳过。
  • 切勿在同一提交中同时修改行为和API版本——包括v2带来的改进。将
    status == "ERROR"
    时快速失败而非超时的逻辑放在下一次提交中;将其合并到迁移提交意味着回滚时必须放弃这一改进。将这些放在清理项中,单独提交。
真实客户端的完整前后对比——带GPU fallback的Pod创建、内联容器配置的端点创建、GraphQL仪表板——位于**reference/worked-example.md**。

5. Verify against the live API

5. 针对真实API进行验证

Static review is not enough; v2's validator is strict and its errors are precise. Re-run the scanner to prove the call sites are gone, then exercise the real paths:
bash
python3 "$SCAN" . --scope rest --fail-on-legacy   # exit 1 if v1 remains
Two markers, and they mean different things — do not reach for the wrong one:
MarkerUse it for
rp-migrate: keep-v1
legacy code kept on purpose — a
RUNPOD_API_V1
rollback branch, or a GraphQL call with no v2 equivalent. Reported under kept on purpose.
rp-migrate: ignore
a false positive on code that is already correct. Says "this isn't legacy", not "this is legacy I'm keeping". Reported under marked false positives.
Both accept
line
,
start
/
end
region, or
file
scope, and both drop out of the plan and out of
--fail-on-legacy
. Using
keep-v1
to silence a false positive records a lie in the report — reach for
ignore
there.
Where the gate can still be wrong. The same blind spots from step 1 invert after a migration: before, they hide v1 code; after, they can flag correct v2 code. The scanner handles the two common cases — trailing
# was imageName
annotations, and a base URL held in a constant (
f"{BASE}/pods"
where
BASE
is a v2 URL defined anywhere in the file). Beyond those — a base URL imported from another module, or built at runtime from config — it can still misread correct code as legacy. Read the flagged lines before believing the exit code, and mark true false positives with
ignore
rather than weakening the gate.
  • Reads are free — list pods, endpoints, volumes, catalog. Confirm you unwrap the new envelope (
    {"pods": [...]}
    , not a bare array).
  • Writes cost money. Create → assert → delete, on the smallest thing that proves the shape. Never test against resources the user already has.
  • Decode
    422
    s with the table in reference/breaking-changes.md — including the confusing one where a missing required field makes the validator report your valid fields as "additional properties not allowed".
静态审查不够;v2的验证器非常严格,错误信息精确。重新运行扫描工具以确认调用站点已移除,然后测试真实路径:
bash
python3 "$SCAN" . --scope rest --fail-on-legacy   # 若存在v1代码则返回1
有两个标记,含义不同——请勿混用:
标记使用场景
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. Summarize — this is the artifact they will actually read

6. 总结——这是用户实际会阅读的成果

Most users read the summary and not the diff. Structure it exactly like this:
undefined
大多数用户只会阅读总结而非差异。请严格按照以下结构编写:
undefined

Required for the migration

迁移必填项

file:line — what changed and why it had to
<文件:行号> —— 变更内容及原因

Cleanup enabled by v2

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)

仍保留在GraphQL(无v2等效方案)

myself / secrets / spot pods / clusters — and why
myself / 密钥 / 抢占式Pod / 集群 —— 原因

Unlocks: what you can build now

新能力:现在可构建的功能

tied to what this codebase already does

That last section is the highest-value part. Do not paste a generic feature list —
look at what this user has been building and struggling with, including anything you
already know from the session, and name where v2 changes it. "Your `wait_until_running`
loop times out on failed pods; v2's `ERROR` status lets it fail in seconds" beats "v2 has
richer status values". [reference/unlocks.md](reference/unlocks.md) is organized as
*if the code does X → v2 offers Y* for exactly this.
与该代码库已实现的功能相关联

最后一部分价值最高。请勿粘贴通用功能列表——查看用户一直在构建和遇到的问题,包括会话中已知的信息,明确v2带来的改变。“你的`wait_until_running`循环在Pod失败时会超时;v2的`ERROR`状态可让其在数秒内失败”比“v2有更丰富的状态值”更有效。[reference/unlocks.md](reference/unlocks.md)按「若代码实现X → v2提供Y」组织,正是为了这一目的。

Ground truth: check the spec yourself

权威来源:自行检查规范

The mapping tables in
reference/
were verified against the live API on 2026-08-10. v2 is actively developed, so treat them as a fast path, not as the authority. Both specs are public and need no auth:
bash
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
What a request body actually accepts, and what is required (
*
). Worth running before writing any create call — it resolves
allOf
composition, which a naive read of the raw JSON misses:
bash
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
Swap the argument for
CreatePodRequest
,
UpdatePodRequest
,
CreateTemplateRequest
,
CreateNetworkVolumeRequest
, … Which schemas mention a field — useful when a
422
names something you cannot place:
bash
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
reference/
中的映射表格已在2026-08-10针对真实API验证。v2仍在积极开发中,因此将其视为快速参考,而非权威来源。两个规范都是公开的,无需认证:
bash
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无法做到的:
bash
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
错误提到你不熟悉的字段时很有用:
bash
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

Precedence when sources disagree

来源冲突时的优先级

observed live behavior > the spec > these tables. The spec is not always right, and the reference docs say so where it is known to be wrong —
timeout
is documented to default to
300000
ms but comes back
0
. If you hit a case where the running API contradicts the spec, trust the API, and say so in your summary so the user knows a documented default cannot be relied on.
If you find a mapping in
reference/
that no longer matches the spec, fix the call and flag the drift — the tables carry a verification date precisely so staleness is detectable rather than silent.
For GraphQL there is no machine-readable schema (introspection is disabled), so that side cannot be checked this way — see the caveat in reference/graphql-to-v2.md.
实际观察到的行为 > 规范 > 本文表格。规范并非总是正确,参考文档会指出已知错误——例如
timeout
文档默认值为
300000
毫秒,但实际返回
0
。如果遇到运行中的API与规范矛盾的情况,请信任API,并在总结中说明,让用户知道文档默认值不可靠。
如果发现
reference/
中的映射与规范不符,请修改调用并标记差异——表格带有验证日期,正是为了能检测到过时内容,而非静默失效。
对于GraphQL,没有机器可读的模式(禁用了自省),因此无法通过这种方式检查——请查看reference/graphql-to-v2.md中的说明。

Tooling notes

工具说明

The inventory scanner is deliberately a grep-class script, not a code-graph index: API generation is a property of URL strings and field names, it must work on any language in an arbitrary customer repo, and it has to give the same answer for every user with zero setup. A code-intelligence index (LSP, or an MCP graph server if one is already running) earns its keep at a different step — step 1's blast radius question, "who calls this wrapper whose response field just got renamed" — not at detection. Use one there if it is already available; do not stand one up just for this.
盘点扫描工具特意设计为grep级脚本,而非代码图索引:API版本是URL字符串和字段名的属性,必须能处理任意客户仓库中的任意语言,且无需任何设置就能为所有用户返回相同结果。代码智能索引(LSP,或已运行的MCP图服务器)在另一个步骤中发挥作用——步骤1中的“影响范围”问题:“谁调用了这个响应字段刚被重命名的包装器”——而非检测阶段。如果已有此类工具,请在该步骤使用;不要仅为此任务搭建。