接入 OpenAI SDK
官方 openai SDK 改一个 base_url 就能用,其余代码一行不动。
base_url要带/v1。 OpenAI SDK 不会替你补路径,填成https://api.apipace.com会打到/chat/completions,拿到一个看不懂的405 method not allowed。
Python
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.apipace.com/v1",
api_key=os.environ["APIPACE_KEY"],
)
r = client.chat.completions.create(
model="claude-sonnet-4.6",
max_tokens=16,
messages=[{"role": "user", "content": "用一个词打招呼"}],
)
print(r.choices[0].message.content)
你好!👋
model 填别名,见 快速开始;Claude、Kimi、MiniMax 这些别名在这个协议上都能用,只有 gemini-* 和图片别名不行。
读平台字段
request_id 和 billable_units 是标准 OpenAI 形状之外的两个字段,SDK 把它们放在 model_extra 里:
print(r.model_extra["request_id"]) # req_01M2M7K25B9BB9GM07EGJWKD6R
print(r.model_extra["billable_units"]) # {'input_tokens': 16, 'output_tokens': 9, 'usage_source': 'upstream'}
billable_units 是这一笔按多少 token 计的费;扣了多少钱拿 request_id 去查 GET /v1/requests/{id},见计费与账单。
流式
计费信息在流的最后一帧:一个 choices 为空、带 billable_units 的 chunk。SDK 会正常把它当 chunk 交给你,判一下就行:
units = None
with client.chat.completions.create(
model="claude-sonnet-4.6", max_tokens=16, stream=True,
messages=[{"role": "user", "content": "用一个词打招呼"}],
) as stream:
for chunk in stream:
extra = chunk.model_extra or {}
if "billable_units" in extra:
units = extra["billable_units"]
elif chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
print(units) # {'input_tokens': 16, 'output_tokens': 11, 'usage_source': 'upstream'}
不想读也没关系——不处理它就是了,空 choices 的 chunk 不会破坏任何标准解析逻辑。
幂等与关联编号
两个可选的头,SDK 用 extra_headers 传:
r = client.chat.completions.create(
model="claude-sonnet-4.6",
max_tokens=16,
messages=[{"role": "user", "content": "用一个词打招呼"}],
extra_headers={
"Idempotency-Key": "job-42", # 7 天内同键重放返回首发结果,不重复扣费
"X-Request-ID": "job-42", # 只是标签,之后可用它找回这一笔
},
)
常见问题
MODEL_NOT_FOUND —— model 填的是厂商原始模型名。这里只认别名,用 client.models.list() 看一眼。
405 method not allowed —— base_url 漏了 /v1。
用了 Azure 的客户端类 —— AzureOpenAI 走的是另一套路径形状,请用普通的 OpenAI。
要 Responses API —— 目前只提供 Chat Completions(/v1/chat/completions)。