Qwen3.8-Max-Preview 调用返回 401 / 是密钥没生效还是权限没开?

文章导读
调用 Qwen3.8-Max-Preview 返回 401,最省时间的判断顺序是:先确认请求里到底有没有带上可用的鉴权头,再确认代码读到的密钥和你在控制台看到的是不是同一把,最后才判断这把密钥是否有权调用该模型。多数 401 卡在中间一步——环境变量没被进程读到,或者密钥字符串首尾带了空格、换行。把这三步按顺序走一遍,比反复换密钥更快定位。
📋 目录
  1. 壹 先看返回体里的错误字段而不是只看状态码
  2. 贰 确认密钥是从环境变量读进来的
  3. 叁 在控制台页面核对这把密钥可用的范围
  4. 肆 用最小请求复现,排除请求体干扰
  5. 伍 把鉴权失败和其他错误分开处理
A A

调用 Qwen3.8-Max-Preview 返回 401,最省时间的判断顺序是:先确认请求里到底有没有带上可用的鉴权头,再确认代码读到的密钥和你在控制台看到的是不是同一把,最后才判断这把密钥是否有权调用该模型。多数 401 卡在中间一步——环境变量没被进程读到,或者密钥字符串首尾带了空格、换行。把这三步按顺序走一遍,比反复换密钥更快定位。

401 在接入 Qwen3.8-Max-Preview 时通常只有两类成因:请求没有携带有效凭据(读取失败、字符串被污染、header 被覆盖),或凭据有效但无权使用该模型。前者可在本地用打印校验和最小请求排除,后者必须到控制台核对密钥状态与可用模型范围。先用最小请求确认 401 与请求体无关,再按状态码分支记录 error code 原文,才不至于把权限问题误判成密钥问题。

先看返回体里的错误字段而不是只看状态码

同样是 401,返回体里的错误码原文能区分「缺少鉴权头」和「凭据无效」两种情况。缺少鉴权头通常说明 header 没拼上、被中间层剥掉,或者变量为空拼成了 Bearer ;凭据无效则说明字符串到了服务端但不被认可,方向应转向密钥本身。所以第一步不是改代码,而是把完整响应体打出来。

import json, logging

resp = session.post(url, headers=headers, json=payload, timeout=30)

try:
    body = resp.json()
except ValueError:
    body = {"_raw": resp.text[:500]}

err = body.get("error") or {}
code = err.get("code") or err.get("type") or "-"
msg = err.get("message") or body.get("message") or "-"

logging.warning(
    "llm_call_failed status=%s error_code=%s error_msg=%s request_id=%s",
    resp.status_code, code, msg,
    resp.headers.get("x-request-id") or resp.headers.get("request-id") or "-",
)

日志里把 status 和 error_code 原文放同一行,事后按 code 分组就能看出是不是同一类失败。request_id 之类的头名称按你实际接入方返回的字段替换,没有就留 -。注意不要只记 resp.ok 或一句「调用失败」,那样第二次排查还得重跑。

确认密钥是从环境变量读进来的

代码里残留旧的硬编码密钥是常见坑:你以为改的是环境变量,实际请求用的还是文件里那行旧值。先把读取逻辑收成一个函数,并把「从哪个变量读到的」一起返回,便于日志留痕。

import os

def load_api_key():
    for name in ("DASHSCOPE_API_KEY", "QWEN_API_KEY"):  # 变量名按你的配置替换
        v = os.environ.get(name)
        if v and v.strip():
            return v.strip(), name
    raise RuntimeError("未在环境变量中找到可用的 API key")

key, source = load_api_key()
raw = os.environ[source]
print("source=%s len_raw=%d len_stripped=%d" % (source, len(raw), len(key)))
print("prefix=%s suffix=%s" % (key[:4], key[-4:]))
print("has_space=%s" % (raw != raw.strip()))

只打印前后各四位和长度,既能把日志和密钥管理页面对上号,又不会把完整密钥写进日志。len_raw 与 len_stripped 不一致,说明首尾有空白;has_space=True 基本就锁定复制时多了空格或换行。设了变量但进程读不到,通常是 docker、systemd 或 IDE 的运行环境与当前 shell 不是同一个,可以只用 python -c "import os;print([k for k in os.environ if 'KEY' in k.upper()])" 列出变量名确认,别把值打出来。

在控制台页面核对这把密钥可用的范围

本地确认密钥读对了,401 仍然出现,就要把判断交给控制台页面。需要核对的项不多:密钥状态是否为启用、是否已被删除或轮换;可用模型范围是否覆盖 Qwen3.8-Max-Preview,很多密钥是绑定业务空间或模型白名单的;创建时间是否与代码里前后四位对应的那把一致,防止同名密钥混用;另外看有无 IP 或调用来源限制。菜单名称各平台不同,按「密钥管理 / API-KEY」这类入口找即可。

Qwen3.8-Max-Preview 调用返回 401 / 是密钥没生效还是权限没开?

核对结果建议直接写成一行记录:核对时间、密钥前后四位、页面上的状态、可用模型范围是否含该模型、是否命中来源限制、结论(密钥问题 / 调用方式问题)。这条记录比截图更好用,因为下一次换人排查可以直接从结论接着往下走。如果页面显示可用范围不含 Qwen3.8-Max-Preview,401 或 403 都可能出现,此时正确动作是调整模型范围或换一把有权限的密钥,而不是继续改请求头。

用最小请求复现,排除请求体干扰

把请求裁到只剩鉴权头和一个最简单的消息体,如果仍然 401,说明与 messages、参数、温度这些东西无关,可以停止在请求体上找原因。下面的骨架只保留必要字段,地址和模型名按你的接入方式替换。

import os, requests

key = os.environ["DASHSCOPE_API_KEY"].strip()
url = "https://<你的接入地址>/compatible-mode/v1/chat/completions"

resp = requests.post(
    url,
    headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"},
    json={"model": "Qwen3.8-Max-Preview", "messages": [{"role": "user", "content": "hi"}]},
    timeout=30,
)
print(resp.status_code, resp.text[:500])

预期形态很明确:成功时是 200,返回体里能解析出模型输出字段;失败时是 4xx,返回体里带错误码原文。最小请求 200 而业务请求 401,问题就在业务代码的请求构造上——常见的是自定义 header 覆盖了 Authorization、经过网关时鉴权头被丢弃、或者请求体里又传了一套凭据。这时再逐步加回业务参数,加一步测一次,直到 401 复现为止。

把鉴权失败和其他错误分开处理

所有失败都走同一段兜底逻辑,是 401 长期查不出来的另一个原因:日志只写「调用失败」,重试逻辑还对着 401 反复重试,把真正原因埋掉。按状态码分支处理,至少让 401 和 429 走不同的路。

def classify(resp):
    s = resp.status_code
    if s == 401:
        return "auth_invalid", "凭据无效或未生效:检查 Authorization 头、密钥首尾空白、密钥状态与可用模型范围", False
    if s == 403:
        return "permission_denied", "凭据有效但权限不足:核对密钥可用模型范围与账号权限", False
    if s == 429:
        return "rate_limited", "触发限流:降低并发或拉长重试间隔", True
    if 400 <= s < 500:
        return "bad_request", "请求参数问题:记录 error_code 后修正请求体", False
    return "server_error", "服务端或网络问题:按退避策略重试", True

401 和 403 不要自动重试,重试不会改变凭据状态,只会放大日志噪音;429 才值得退避重试。验证方式:故意在环境变量值末尾补一个空格跑一次,应落到 auth_invalid 分支;再用一个明显不存在的密钥跑一次,同样落 401,但把两次记录的 error_code 原文对比一下,是否一致就是判断「缺头」和「无效」的依据。最后用最小请求跑通一次,确认 401 分支不再被误触。