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 openaiimport osfrom openai import OpenAI
client = OpenAI( api_key=os.environ["GRAFILAB_API_KEY"], base_url="https://llm.grafilab.ai/v1",)npm install openaiimport OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.GRAFILAB_API_KEY, baseURL: "https://llm.grafilab.ai/v1",});如果你不想改构造函数——把已有脚本指向 Grafilab 时这很方便——就把 OPENAI_BASE_URL 设为 https://llm.grafilab.ai/v1、把 OPENAI_API_KEY 设为你的 Grafilab 密钥,然后不带参数构造客户端。base URL 必须以 /v1 结尾;/chat/completions 之类的路径由 SDK 自己补上。
选择 model id
Section titled “选择 model id”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.
curl https://llm.grafilab.ai/v1/models \ -H "Authorization: Bearer $GRAFILAB_API_KEY" | jq -r '.data[].id'from openai import OpenAI
client = OpenAI(api_key="sk-grafilab-...", base_url="https://llm.grafilab.ai/v1")for model in client.models.list(): print(model.id)import OpenAI from "openai";
const client = new OpenAI({ apiKey: "sk-grafilab-...", baseURL: "https://llm.grafilab.ai/v1" });for await (const model of client.models.list()) { console.log(model.id);}The same ids appear as cards in the Playground, with context size and prices.
Chat completions
Section titled “Chat completions”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)const response = await client.chat.completions.create({ model: "grafilab/qwen3.6-flash", messages: [ { role: "system", content: "You are terse." }, { role: "user", content: "Why is the sky blue?" }, ],});
console.log(response.choices[0].message.content);console.log(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="")const stream = await client.chat.completions.create({ model: "grafilab/qwen3.6-flash", messages: [{ role: "user", content: "Count to five." }], stream: true, stream_options: { include_usage: true },});
for await (const chunk of stream) { if (chunk.usage) { console.log("\nusage:", chunk.usage); } else if (chunk.choices[0]?.delta?.content) { process.stdout.write(chunk.choices[0].delta.content); }}带用量的那个 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))for await (const model of client.models.list()) { console.log(model.id, (model as any).region, (model as any).tags);}每个 id 都是一个 Grafilab 别名,也就是要作为 model 发送的确切字符串。别名匹配不区分大小写,可以包含 /,响应里回显的是规范写法,可能和你发的不一样。除了标准的 OpenAI 字段之外,每个条目还带两个 Grafilab 扩展字段:region 和 tags。
两个 SDK 都会按状态码抛出带类型的异常。其中有两个,如果你按 OpenAI 的语义去理解会觉得意外。
| 异常 | 状态码 | 在 Grafilab 上的含义 |
|---|---|---|
AuthenticationError | 401 | 密钥不存在、已吊销或格式不对。按密钥配置的 IP 白名单把调用方排除在外时也会抛出 |
PermissionDeniedError | 403 | billing_currency_unset——你的账号没有结算币种。在个人资料里设置国家/地区 |
NotFoundError | 404 | model_not_found。别名写错了,或者模型存在但没有你所用币种的定价 |
RateLimitError | 429 | 是余额不足,不是吞吐量超限。 该充值,而不是退避重试 |
APIStatusError | 500 | 基础设施故障。网关错误一律返回 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)try { const { data, response } = await client.chat.completions .create({ model: "grafilab/qwen3.6-flash", messages: [{ role: "user", content: "Hello" }], }) .withResponse(); console.log(response.headers.get("x-request-id"), data.choices[0].message.content);} catch (err) { if (err instanceof OpenAI.RateLimitError) { console.log("Top up your Grafilab credit balance."); } else if (err instanceof OpenAI.APIError) { console.log(err.status, err.headers?.["x-should-retry"], err.message); }}Embeddings 与图像
Section titled “Embeddings 与图像”Embeddings
Section titled “Embeddings”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))const result = await client.embeddings.create({ model: "<embedding-model-id>", input: ["the first document", "the second document"], encoding_format: "float",});console.log(result.data[0].embedding.length);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)const image = await client.images.generate({ model: "<image-model-id>", prompt: "a lighthouse in fog, long exposure", n: 1, response_format: "url",});console.log(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",)import fs from "node:fs";import { toFile } from "openai";
const edited = await client.images.edit({ model: "<edit-capable-model-id>", image: await toFile(fs.createReadStream("source.png")), 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")let video = await client.videos.create({ model: "<video-model-id>", prompt: "a paper boat crossing a puddle",});
while (video.status === "queued" || video.status === "in_progress") { await new Promise((r) => setTimeout(r, 5000)); video = await client.videos.retrieve(video.id);}
if (video.status === "failed") throw new Error(String(video.error));
const content = await client.videos.downloadContent(video.id);// content is a Response — stream or buffer its body to a file.GET /v1/videos/{video_id}/content 会返回 302,跳转到一个公开可读、本身不需要密钥的 MP4 URL——把链接直接丢给浏览器很方便,也正因如此不要把它记进日志。Grafilab 实现了创建、列表、查询和内容下载;SDK 的 remix、edit、extend 和 delete 方法在这里没有对应实现,remixed_from_video_id 永远是 null。

