改两行就能跑
OpenAI Python SDK 的地址由 base_url 决定,默认指向 api.openai.com。换成站点地址后,其余调用代码不用动,网关按请求体里的 model 字段选上游。一个 Key 能调多个厂商的模型,换模型只改一个字符串,账单在站点统一结算,不用给每家分别绑卡。
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["YUN25_API_KEY"],
base_url="https://ai.idcidiot.com/v1",
timeout=60.0,
max_retries=2,
)base_url 写到 /v1 为止,SDK 会在后面拼 /chat/completions。写成完整路径会得到 /v1/chat/completions/chat/completions,直接 404。Key 从环境变量读,别写死在代码里;客户端对象可以全局复用一个,它自带连接池,每次请求都新建会拖慢速度。
项目里把 openai 的版本钉住。SDK 大版本之间改过默认重试和超时行为,升级后先跑一遍冒烟用例。虚拟环境用 venv 或 uv 都行,重点是别让系统 Python 里的旧版本混进来。地址和模型名放进配置或环境变量,测试环境和生产环境跑同一份代码。
一次普通调用
resp = client.chat.completions.create(
model="claude-sonnet-5",
messages=[
{"role": "system", "content": "回答保持简短,不要复述问题。"},
{"role": "user", "content": "解释一下 SSE 里的 retry 字段。"},
],
temperature=0.3,
max_tokens=600,
)
print(resp.choices[0].message.content)
print(resp.usage.prompt_tokens, resp.usage.completion_tokens)返回对象和官方 SDK 一致,usage 里是输入输出 token 数,扣费按这个算。max_tokens 限制的是输出长度,不是上下文总量,别把它当上下文上限用。claude-sonnet-5 的上下文是 1,000,000 token,最大输出 128,000 token。
messages 里的 system 同样计费,而且每次请求都要重发一遍,系统提示写得越长,成本越随调用次数放大。temperature 只影响采样,不改计费;想省钱先看输入长度和输出上限,这两项才是账单的主要来源。
流式
stream = client.chat.completions.create(
model="claude-sonnet-5",
messages=[{"role": "user", "content": "写一个 20 行的 Python 日志装饰器"}],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)开了 stream_options 后,末尾一块的 choices 是空数组,usage 才带统计,所以循环里要先判断 chunk.choices,不判断会抛 IndexError。流式降低的是首字延迟,总耗时不一定更短,适合需要边生成边显示的场景。
要把全文存下来,就在循环里把 delta.content 累加到一个列表,结束后 "".join(...)。只打印不保存,程序退出时拿不到完整结果。finish_reason 在末尾一块里,值为 length 说明输出被 max_tokens 截断,不是模型写完了。
超时与重试
SDK 默认对连接错误、408、409、429 和 5xx 自动重试两次,max_retries 可以改。重试对幂等请求安全,对长输出意义不大,超时该单独调:
import httpx
client = OpenAI(
api_key=os.environ["YUN25_API_KEY"],
base_url="https://ai.idcidiot.com/v1",
timeout=httpx.Timeout(10.0, read=300.0),
max_retries=3,
)第一个参数是连接超时,read 是两次数据之间的等待上限。生成长文时把 read 放大到 300 秒,不要把整体超时直接调成 10 分钟,连接阶段卡住会白等。重试次数也不是越多越好,持续 429 时加次数只会放大压力。
超时还会牵扯计费。客户端断开时上游可能已经生成完并正常扣费,重试等于再买一次同样的输出。长输出优先放大超时,别指望重试救回来。
错误处理
from openai import APIConnectionError, APIStatusError, APITimeoutError, RateLimitError
try:
resp = client.chat.completions.create(
model="claude-sonnet-5",
messages=[{"role": "user", "content": "ping"}],
)
except RateLimitError:
print("限流,退避后重试")
except APITimeoutError:
print("读超时,缩小 max_tokens 或放大 read 超时")
except APIConnectionError:
print("连接失败,检查网络和 base_url")
except APIStatusError as e:
print(e.status_code, e.response.text)RateLimitError 是 APIStatusError 的子类,APITimeoutError 是 APIConnectionError 的子类,捕获顺序要从具体到宽泛,反过来写后面的分支永远进不去。e.response.text 里是网关返回的原文,诊断 400 比 str(e) 有用。
把 status_code 和返回原文记进日志,再按状态码分流:401 查 Key,404 查地址,400 查请求体字段,429 退避,5xx 重试。这套判断比在代码里到处 except Exception 稳。
成本
claude-sonnet-5 的 model_ratio 是 1.5,Claude Max 分组 0.85 倍,输入价 = 1.5 × 2 × 0.85 = $2.55/百万 token,输出按 completion_ratio 5 折算为 $12.75/百万 token。一次 5,000 输入、1,200 输出的调用约 $0.0128 + $0.0153 = $0.028,一天 1,000 次就是 $28 上下,按 22 个工作日算一个月 $600 出头。
把脚本里的模型换成 glm-5.3-flash(官渠国模 0.2 倍,$0.16/$0.56)能把这个数字压到十分之一,适合分类、抽取这类短输出任务。倍率表在 价格页,模型清单在 /model/。先跑通上面这段示例,再把 client 抽成模块级单例。