JellyToken 的密钥按环境分开存、模型名对着控制台清单逐个核一遍

文章导读
先做两件事再写业务代码:密钥不进版本库、按环境各给一份;模型标识不手打、从控制台逐字复制并落成清单文件。这两步都不涉及业务逻辑,但能挡掉联调阶段大部分“明明配了却调不通”的报错。排查顺序建议固定为:先确认密钥来自哪套环境,再确认模型标识在平台侧存在,最后才怀疑参数和网络。
📋 目录
  1. 壹 把密钥从代码里挪到环境变量
  2. 贰 为测试和生产各建一份配置
  3. 叁 对着控制台页面核对模型标识
  4. 肆 把常见鉴权与模型相关错误分类记录
  5. 伍 在联调前跑一次连通性自检
A A

先做两件事再写业务代码:密钥不进版本库、按环境各给一份;模型标识不手打、从控制台逐字复制并落成清单文件。这两步都不涉及业务逻辑,但能挡掉联调阶段大部分“明明配了却调不通”的报错。排查顺序建议固定为:先确认密钥来自哪套环境,再确认模型标识在平台侧存在,最后才怀疑参数和网络。

常见故障点集中在两处:密钥把测试和生产写在同一份配置或同一个值里,出错时分不清打到哪套环境;模型名从记忆里手打,大小写、连字符、版本后缀差一个字符就报模型不存在。建议密钥统一走环境变量,变量名一致、值由环境文件注入,启动时打印环境名并跑一次连通性自检;模型标识只从控制台复制,落成 models.yaml 纳入版本库。边界是:环境变量只解决密钥从哪来,不保证权限和配额正确;模型是否可用以控制台当前账号可见列表为准。

把密钥从代码里挪到环境变量

目标很单一:代码里不出现密钥字符串,密钥的值由运行环境注入,测试和生产各注入各的。

命名约定可以这样定:统一前缀 JELLYTOKEN_,按用途命名而不是在变量名里塞环境名——JELLYTOKEN_API_KEY、JELLYTOKEN_BASE_URL、JELLYTOKEN_DEFAULT_MODEL。环境差异通过环境文件或部署平台的变量分组提供,两边变量名完全一致,只是值不同。另外单独设一个 JELLYTOKEN_ENV=test|prod,只用于日志和自检提示,不参与选密钥。

为什么不写成 JELLYTOKEN_PROD_API_KEY 和 JELLYTOKEN_TEST_API_KEY 两组?因为那样代码里必然出现按环境取值的分支,漏改一处就会串环境。变量名一致、值随部署注入,切换环境只换文件,代码里没有分支。

读取时用一层封装,读不到就立即失败,并把变量名和环境名写进报错里:

import os

def require_env(name: str) -> str:
    value = os.environ.get(name, "").strip()
    if not value:
        env = os.environ.get("JELLYTOKEN_ENV", "unknown")
        raise RuntimeError(
            f"缺少环境变量 {name}(当前 JELLYTOKEN_ENV={env})。"
            "请确认已加载对应环境的配置文件,且该变量没有被导出为空值。"
        )
    return value

API_KEY = require_env("JELLYTOKEN_API_KEY")
BASE_URL = require_env("JELLYTOKEN_BASE_URL")
DEFAULT_MODEL = require_env("JELLYTOKEN_DEFAULT_MODEL")

注意两点:报错信息只写变量名,不打印密钥内容;取值时做一次 strip(),处理从控制台复制时带进来的换行和空格——这类不可见字符是 401 的常见来源之一。

为测试和生产各建一份配置

配置文件按环境拆开,文件名直接体现环境,例如 config/test.yaml 与 config/prod.yaml。配置文件里不写密钥值,只声明从哪个环境变量读:

# config/test.yaml
env: test
base_url: https://test.example.invalid   # 换成控制台给出的地址
api_key_env: JELLYTOKEN_API_KEY          # 只写变量名,不写值
timeout_seconds: 30
log_level: debug
model_manifest: config/models.yaml        # 两套环境共用同一份

# config/prod.yaml
env: prod
base_url: https://prod.example.invalid
api_key_env: JELLYTOKEN_API_KEY
timeout_seconds: 60
log_level: info
model_manifest: config/models.yaml

api_key_env 写的是变量名而不是密钥本身,所以这份配置可以进版本库;真正的值放在本地 .env 或部署平台的变量里,不进版本库。

随环境变化的字段:base_url、超时时间、日志级别、是否打印详细请求信息、并发上限(测试环境通常收得更紧)。必须保持一致的字段:模型清单的引用路径、请求参数结构、结果解析与重试逻辑、错误分类表。

JellyToken 的密钥按环境分开存、模型名对着控制台清单逐个核一遍

模型清单两套环境共用同一份,是这里最关键的一条。如果测试和生产各维护一份模型标识,很容易出现“测试能调、切到生产报模型不存在”。某个模型只在一边可用时,在清单里加一列 available_in 标出来,而不是拆成两份文件。启动日志里打印加载的配置文件路径和 env 字段,联调时第一眼就能确认读的是哪一份。

对着控制台页面核对模型标识

模型标识不靠记忆,靠复制。具体动作:

  1. 打开控制台的模型列表页,按类别逐个复制模型标识,直接粘贴进清单文件,中途不改大小写、不删减版本或日期后缀。
  2. 每个模型额外记录两项:支持的调用方式(走哪个接口路径、请求体结构,以控制台说明为准)、是否支持流式。控制台没写清楚的,先用第 5 节的脚本发一次最小请求确认,再补回清单。
  3. 清单文件纳入版本库,字段固定,改动可以通过 diff 看出。
models:
  - id: "example-chat-large"     # 原样复制,含大小写和连字符
    kind: chat                   # chat / embedding / rerank,以控制台分类为准
    available_in: [test, prod]
    note: "如控制台提供别名,记录别名与 id 的对应关系"
  - id: "example-embed-small"
    kind: embedding
    available_in: [test, prod]

清单更新要有触发条件:控制台新增或下线模型时同步改清单,改完跑一次自检。不要只在业务代码里改常量,那样配置、代码和清单会很快对不上。

把常见鉴权与模型相关错误分类记录

先读响应字段,再决定查什么。通用的错误响应一般包含 HTTP 状态码、error.code 或 error.type、error.message,以及一个请求标识字段(常见叫 request_id)。记录时至少保留状态码、code、message 和请求标识,请求标识在需要平台侧协助时能定位到具体某一次调用。

  • 鉴权失败(401/403):先确认密钥是当前环境那一份——对照启动日志里的 env 和配置文件路径;再确认请求头格式,是否带 Bearer 前缀、有没有多余空格或换行;最后才查密钥是否被撤销或过期。密钥内容不要写进日志。
  • 模型不存在(404,或 400 且 code、message 指向模型):先拿报错里的标识和清单逐字符比对,重点看大小写、连字符与下划线、版本或日期后缀;再确认该模型在当前环境和当前账号下是否可用(对照 available_in);最后才怀疑调用方式与该模型不匹配,比如把对话模型的标识用在向量接口上。
  • 参数不合法(400/422):先核对请求体字段名和类型是否与控制台示例一致,再查必填项、取值范围、消息列表结构;如果测试能过、生产不过,比较两份配置里的默认参数差异,而不是直接改业务代码。

每条错误记录后面写一行“下一步检查动作”,例如“比对清单里第 3 条模型的标识”,换人或隔天再看时不必从日志第一行重读。

在联调前跑一次连通性自检

自检只做三件事:确认环境变量齐全、确认密钥能通过鉴权、确认清单里的模型标识在平台侧可见。不跑业务逻辑,不产生业务数据。

import os
import sys

import requests
import yaml

env = os.environ.get("JELLYTOKEN_ENV", "unknown")
base = os.environ.get("JELLYTOKEN_BASE_URL", "").rstrip("/")
key = os.environ.get("JELLYTOKEN_API_KEY", "").strip()

if not base or not key:
    sys.exit(f"[FAIL] env={env} 缺少 base_url 或 api_key,检查环境文件是否已加载")

headers = {"Authorization": f"Bearer {key}"}
# 列表接口路径以控制台说明为准,示例用 /models
resp = requests.get(f"{base}/models", headers=headers, timeout=15)
print(f"[INFO] env={env} status={resp.status_code}")
if resp.status_code >= 400:
    sys.exit(f"[FAIL] 鉴权或路径异常,body={resp.text[:300]}")

with open("config/models.yaml", encoding="utf-8") as f:
    manifest = yaml.safe_load(f)

known = {m["id"] for m in manifest["models"] if env in m.get("available_in", [])}
remote = {m.get("id") for m in resp.json().get("data", [])}
missing = sorted(known - remote)
print(f"[INFO] 清单可见模型数={len(known)} 平台返回数={len(remote)}")
if missing:
    sys.exit(f"[FAIL] 清单中当前环境不可见的模型: {missing}")
print("[OK] 自检通过")

通过标准三条,全部满足才进入业务联调:脚本退出码为 0;日志里打印的 env 与预期环境一致;清单中标记为当前环境可用的模型标识全部出现在平台返回的列表里。任何一条不满足就停下来修配置,不要继续往上接业务代码。

需要替换的地方:列表接口路径(示例里的 /models 只是占位)、响应里承载模型列表的字段名、清单文件路径,都按控制台说明和本地实际改。如果平台不提供列表接口,就把这一步退化成对清单里每个模型各发一次最小请求,逐个记录成功或失败,通过标准不变。