换成 Gemini 4 Argon 之后接口直接说模型不存在,先别急着改代码:这个报错通常同时可能来自三层,model 字段写法、密钥所属项目的模型授权、以及当前接入区域是否提供该模型。建议按反过来的顺序排查,先把返回体原文抄全判断错误类别,再逐字核对 model 字段,最后用同一把密钥请求一个确认可用的旧模型做对照,把范围缩到最小再动手改配置。
模型不存在的报错,先看返回体再改参数。通常参数错误表现为 400 与 INVALID_ARGUMENT 一类,错误信息会点名 model 字段;权限错误表现为 403 与 PERMISSION_DENIED,指向项目或密钥而不是某个参数;区域或接入点不支持时常被包装成 404。判断方法是用同一把密钥请求一个已确认可用的旧模型:旧通新不通,范围锁在模型名或该模型授权;两者都不通,优先查密钥、项目与接入地址。没确认模型 ID 前不建议改鉴权或换密钥。
把报错返回体完整抄下来,先分清参数错误和权限错误
两类错误在处理动作上完全不同:参数错误改请求,权限错误改项目配置或等待开通。所以在改任何代码之前,先把完整响应体(不是只看 SDK 抛出的那行异常摘要)复制出来,按下面几个维度归类。
| 判断维度 | 参数错误 | 权限错误 |
|---|---|---|
| HTTP 状态码 | 常见 400 | 常见 403 |
| 错误类型字段 | INVALID_ARGUMENT / invalid_request_error 一类 | PERMISSION_DENIED / permission_denied 一类 |
| 指向对象 | 明确点名 model 字段,偶尔连带 apiVersion | 指向项目、密钥或模型授权,不指向具体请求参数 |
| 提示语特征 | model not found、unknown model、not supported | does not have access to model、API key not valid、未启用 |
| 处理方向 | 改 model 字段或请求路径 | 去项目页看授权与配额 |
要注意中间层可能改写状态码:网关、自建代理或 SDK 包装层有时把上游的 400、403 统一抛成 404。所以判断以响应体内的 error.status、error.type 和提示语原文为准,不要只凭 HTTP 状态码下结论。适用场景是所有通过 SDK 或代理接入的调用;验证方式是把响应体 JSON 原样贴到排查记录里,逐字段对照上表;风险边界是这一步只做归类,不产生修复动作。
逐字核对请求里的 model 字段:大小写、空格、连字符
归类为参数错误之后,才轮到核对字符串。这一层是最容易出问题、也最容易验证的一层。逐项检查:
- 大小写:模型 ID 通常区分大小写,Argon 写成 argon 不保证等价。
- 首尾空格与换行:模板字符串里
models/${id}的尾部空格、YAML 值后面多余的空格、从网页复制带进来的全角空格。 - 连字符与下划线:gemini-4-argon 与 gemini_4_argon 是两个不同字符串,不能互认。
- 前缀重复:有的接口要求带
models/前缀,有的只接受裸 ID,混用会拼成models/models/...。 - 粘错的版本段:把版本号或日期段误当成模型名的一部分。
- 不可见字符:从文档页面复制时带入的零宽字符,肉眼看不出差异。
检查链路依次是:环境变量或配置文件 → 配置加载与模板渲染 → 业务代码拼接 → 请求体序列化 → 落日志。最容易被模板字符串带进空格的位置是配置读取后没 strip()、以及在拼 URL 时在变量前后多敲了一个空格。下面这段可以放在发请求之前执行,用来确认最终真正发出去的字符串:
import json, os
model_id = os.environ.get('MODEL_ID', '')
print(repr(model_id)) # repr 能看出尾随空格和不可见字符
model_id = model_id.strip()
assert model_id and ' ' not in model_id, 'model 字段疑似带空格'
body = {'contents': [{'parts': [{'text': 'ping'}]}]}
print(json.dumps({'model': model_id, 'body': body}, ensure_ascii=False))
把打印出来的字符串和项目页模型列表里的 ID 逐字符比对,不要凭记忆核对。验证方式是日志里的 model 字段去掉引号后与页面 ID 完全一致;风险边界是这一步只能排除写法问题,不能证明该项目已经获得这个模型的授权。
用同一把密钥换回旧模型名发一次请求做对照
写法确认无误后,用同一把密钥、同一个接入地址、同一个方法后缀,把 model 换成你之前确认可用的旧模型 ID 发一次请求。两次请求只允许 model 一个变量不同,其他全部保持一致,否则对照失去意义。
# 旧模型对照请求
curl -s -o old.json -w 'old:%{http_code}\n' \
-H "x-goog-api-key: $API_KEY" \
-H 'Content-Type: application/json' \
`--data-raw` '{"contents":[{"parts":[{"text":"ping"}]}]}' \
"https://<你的接入地址>/v1beta/models/<已确认可用的旧模型ID>:generateContent"
# 换成新模型再发一次,其余参数不动
curl -s -o new.json -w 'new:%{http_code}\n' \
-H "x-goog-api-key: $API_KEY" \
-H 'Content-Type: application/json' \
`--data-raw` '{"contents":[{"parts":[{"text":"ping"}]}]}' \
"https://<你的接入地址>/v1beta/models/<Gemini 4 Argon 的候选ID>:generateContent"
结果判读:
- 旧模型 200、新模型报模型不存在或 not supported:密钥和接入地址基本没问题,问题在模型名写法或该模型未授权、未在当前区域提供。
- 旧模型 403、新模型 403:指向密钥或项目层面,先别继续改模型名。
- 两个都失败且路径相同:优先检查接入地址、API 版本段和方法后缀是否正确。
- 旧模型 200、新模型返回 PERMISSION_DENIED:写法基本正确,卡在授权。
适用场景是所有能拿到一把可用密钥的情况;验证方式是两次请求的状态码和响应体并列记录;风险边界是这个实验不能区分“未授权”和“该区域不提供”,这两者要靠下一步的项目页确认。
在项目页确认该模型的授权与配额状态
如果对照实验指向权限,就到项目页逐项核对,重点看这几项:
- 对应模型服务的 API 启用状态:项目里该项 API 是否已启用。
- 模型访问授权:模型库或模型管理页里该模型的启用、申请访问状态是否已通过。
- 密钥或服务账号的权限范围:当前使用的 API key 或服务账号,是否具备调用该模型的角色。
- 配额项:按模型维度看 RPM、TPM 或每日调用配额,配额为 0 或已用尽时通常表现为拒绝调用。
未开通时的典型表现有三个:该模型不出现在项目的可用模型列表里;调用返回 403 或明确的未启用提示;配额页里看不到该模型的条目。需要说明的是,配额耗尽和未授权在提示语上不一定能一眼区分,建议两处都看。验证方式是把项目页截图或条目名称与上一步的响应体对应起来;风险边界是项目页状态存在生效延迟,刚点完启用不一定立刻可调用。
按定位结果给出两条修法并各写验证方式
修法一:改模型名
适用场景是对照实验里旧模型通、新模型报参数错误。动作:从项目页模型列表复制确切 ID,写进配置项而不是硬编码在业务代码里,重载配置后复测。验证方式是用第 3 节同一条请求只替换 model,返回 200 且响应体里出现候选内容字段,才算通过;若仍然报模型不存在,说明不是写法问题,转到修法二。风险边界是不要拿“旧模型可用”当作新模型可用的证据。
修法二:走开通流程,等待期做临时降级
适用场景是返回体指向 PERMISSION_DENIED,或项目页显示该模型未授权。动作:在项目页提交该模型的启用或访问申请,按页面提示等待审批或生效;期间把调用切到已确认可用的旧模型,切换点只放在配置层,例如通过环境变量注入,不要在两处代码里各写一次分支。
# 配置层降级,不要在业务代码里再开分支
MODEL_ID="${MODEL_ID:-<已确认可用的旧模型ID>}"
验证方式:开通生效后,用同一脚本把配置里的 MODEL_ID 换回新模型 ID,观察返回体。如果这时返回的是权限类错误而不是模型不存在,说明写法已经正确,问题只剩授权生效时点,可以继续等待或联系项目管理员确认。风险边界是临时降级只解决调用中断,不代表新模型已经可用,也不要把它当作长期的容量方案。