跳转到内容

Models

OpenAI SDK

官方的 openai 包可以跟任何实现了 OpenAI HTTP API 的端点通信。改掉 base URL 和密钥,其余部分——重试、流式、分页、带类型的错误——都按文档正常工作。

  • A Grafilab API key. In the console open API (https://app.grafilab.ai/api) and click Generate New Key — see API Keys.

别把密钥写进源码。下面的示例都从环境变量里读取。

终端窗口
pip install openai
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["GRAFILAB_API_KEY"],
base_url="https://llm.grafilab.ai/v1",
)

如果你不想改构造函数——把已有脚本指向 Grafilab 时这很方便——就把 OPENAI_BASE_URL 设为 https://llm.grafilab.ai/v1、把 OPENAI_API_KEY 设为你的 Grafilab 密钥,然后不带参数构造客户端。base URL 必须以 /v1 结尾;/chat/completions 之类的路径由 SDK 自己补上。

The model catalog is live, so list it rather than copying ids from a page. Every entry’s id is the exact string to use in any tool or SDK.

Terminal window
curl https://llm.grafilab.ai/v1/models \
-H "Authorization: Bearer $GRAFILAB_API_KEY" | jq -r '.data[].id'

The same ids appear as cards in the Playground, with context size and prices.

response = client.chat.completions.create(
model="grafilab/qwen3.6-flash",
messages=[
{"role": "system", "content": "You are terse."},
{"role": "user", "content": "Why is the sky blue?"},
],
)
print(response.choices[0].message.content)
print(response.usage)

除 model、messages、stream 和 stream_options 之外的字段,都会作为参数原样透传给模型。也就是说 temperature、top_p、max_tokens、tools 和 response_format 在底层模型支持的地方都能用——未知参数由模型忽略或拒绝,而不是由 Grafilab。文本在交付后计费,每个请求一笔交易。

用量要显式索要。没有 stream_options 的话,流式响应里根本不带 token 计数。

stream = client.chat.completions.create(
model="grafilab/qwen3.6-flash",
messages=[{"role": "user", "content": "Count to five."}],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
if chunk.usage:
print("\nusage:", chunk.usage)
elif chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")

带用量的那个 chunk 最后到达,就在 data: [DONE] 结束标记之前,它的 choices 数组是空的。

推理过程会在 message 上以 reasoning 和 reasoning_content 两个字段返回,内容相同。以内联标签形式输出思考的模型也会被归一化到这两个字段里,所以你不用从可见文本里把它们解析出来。这两个字段都不属于 OpenAI 的类型定义——从原始对象上读取。

for model in client.models.list():
print(model.id, getattr(model, "region", None), getattr(model, "tags", None))

每个 id 都是一个 Grafilab 别名,也就是要作为 model 发送的确切字符串。别名匹配不区分大小写,可以包含 /,响应里回显的是规范写法,可能和你发的不一样。除了标准的 OpenAI 字段之外,每个条目还带两个 Grafilab 扩展字段:region 和 tags。

两个 SDK 都会按状态码抛出带类型的异常。其中有两个,如果你按 OpenAI 的语义去理解会觉得意外。

异常状态码在 Grafilab 上的含义
AuthenticationError401密钥不存在、已吊销或格式不对。按密钥配置的 IP 白名单把调用方排除在外时也会抛出
PermissionDeniedError403billing_currency_unset——你的账号没有结算币种。在个人资料里设置国家/地区
NotFoundError404model_not_found。别名写错了,或者模型存在但没有你所用币种的定价
RateLimitError429是余额不足,不是吞吐量超限。 该充值,而不是退避重试
APIStatusError500基础设施故障。网关错误一律返回 500,绝不会是 502 或 504

500 可以放心重试,除非响应带了 x-should-retry: false,那表示工作已经完成并且已计费。每个响应都带 X-Request-Id 头(req_ 后面跟 24 个十六进制字符)——提工单时请附上它。

import openai
try:
raw = client.chat.completions.with_raw_response.create(
model="grafilab/qwen3.6-flash",
messages=[{"role": "user", "content": "Hello"}],
)
print(raw.headers.get("x-request-id"))
completion = raw.parse()
except openai.RateLimitError:
print("Top up your Grafilab credit balance.")
except openai.APIStatusError as err:
print(err.status_code, err.response.headers.get("x-should-retry"), err.response.text)

encoding_format 接受 float(一个 JSON 数组)或 base64(小端 float32,传输字节数大约只有前者的三分之一)。和文本一样,embeddings 在交付后计费。

result = client.embeddings.create(
model="<embedding-model-id>",
input=["the first document", "the second document"],
encoding_format="float",
)
print(len(result.data[0].embedding))

images.generate 是同步的:先生成,并在同一笔交易里扣费,所以失败不花钱。response_format 取 url 或 b64_json,n 接受 1 到 10。

image = client.images.generate(
model="<image-model-id>",
prompt="a lighthouse in fog, long exposure",
n=1,
response_format="url",
)
print(image.data[0].url)

images.edit 以 multipart 表单数据上传,需要一个支持编辑的模型——其他模型会返回 400,错误码为 edit_unsupported。单个文件上限 20 MB,单次请求上限 10 个文件。

edited = client.images.edit(
model="<edit-capable-model-id>",
image=open("source.png", "rb"),
prompt="replace the sky with a clear night sky",
)

视频是异步的:创建任务,轮询,然后下载结果。任务的状态依次是 queued → in_progress → completed 或 failed。推进状态靠的就是轮询,所以要一直轮询到终态为止。

视频在创建时计费,扣费发生在提交那笔交易里;如果任务之后失败会自动退款。完成的结果在完成七天后过期,想留下来的记得下载。

import time
video = client.videos.create(model="<video-model-id>", prompt="a paper boat crossing a puddle")
while video.status in ("queued", "in_progress"):
time.sleep(5)
video = client.videos.retrieve(video.id)
if video.status == "failed":
raise RuntimeError(video.error)
content = client.videos.download_content(video.id)
content.write_to_file("out.mp4")

GET /v1/videos/{video_id}/content 会返回 302,跳转到一个公开可读、本身不需要密钥的 MP4 URL——把链接直接丢给浏览器很方便,也正因如此不要把它记进日志。Grafilab 实现了创建、列表、查询和内容下载;SDK 的 remix、edit、extend 和 delete 方法在这里没有对应实现,remixed_from_video_id 永远是 null。