兼容 OpenAI 风格的
POST /v1/chat/completions仍可用,但 xAI 文档将其视为 legacy。这篇演示最小客户端,并在 stderr 提示迁移。
先看清成品
- mock 回显消息。
- 官方无效密钥 HTTP 400。
- 1 项 pytest 通过。
合规提醒:Grok 可结合网页与 X 平台实时信息,实时内容未必准确,重要结论须核对可靠来源。API 密钥在 console.x.ai 管理,勿提交公开仓库。遵守 xAI 使用政策;勿用于骚扰、欺诈或未授权自动化。价格与套餐以官网为准。
运行说明:无正式
XAI_API_KEY。A/B 用 mock;对官方api.x.ai无效密钥实测 HTTP 400(Incorrect API key)。C 校验 search_parameters。Chat Completions 在文档中标为 legacy,新集成优先 Responses。2026-10-23(Asia/Shanghai)pytest 通过。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
grok_chat.py |
legacy 客户端 |
mock_xai.py |
假服务 |
test_105a.py |
测试 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| Python | 3.10+ |
| 环境变量 | XAI_API_KEY;可选 XAI_BASE_URL / XAI_MODEL |
关键事实
| 事实 | 来源 |
|---|---|
主机多为 https://api.x.ai,Bearer 认证 |
官方 Docs |
| Chat Completions 标为 legacy;新项目优先 Responses | 官方 Chat Completions 指南 |
| 模型 ID 以控制台/文档为准(示例 grok-4 需按账号可见性调整) | 实践 |
文件结构
105-grok-A-src/
├── grok_chat.py
├── mock_xai.py
└── test_105a.py
第 1 步:客户端
#!/usr/bin/env python3
"""xAI Grok chat completions client (legacy endpoint; prefer Responses for new apps)."""
from __future__ import annotations
import argparse, json, os, sys, urllib.error, urllib.request
def chat(base, key, model, message, system=None):
msgs = []
if system:
msgs.append({"role": "system", "content": system})
msgs.append({"role": "user", "content": message})
req = urllib.request.Request(
f"{base.rstrip('/')}/v1/chat/completions",
data=json.dumps({"model": model, "messages": msgs}).encode(),
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=120) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
raise SystemExit(f"HTTP {e.code}: {e.read().decode(errors='replace')}") from e
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("message", nargs="?", default="用一句话介绍 Grok")
ap.add_argument("--base", default=os.environ.get("XAI_BASE_URL", "https://api.x.ai"))
ap.add_argument("--key", default=os.environ.get("XAI_API_KEY", ""))
ap.add_argument("--model", default=os.environ.get("XAI_MODEL", "grok-4"))
args = ap.parse_args(argv)
if not args.key:
print("设置 XAI_API_KEY", file=sys.stderr); return 2
r = chat(args.base, args.key, args.model, args.message)
print(r["choices"][0]["message"]["content"])
print("# note: Chat Completions is legacy; new integrations should use /v1/responses", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())
第 2 步:Mock
#!/usr/bin/env python3
"""Mock xAI chat completions + responses."""
from __future__ import annotations
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
API_KEY = "xai-test-key"
class H(BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def _auth(self):
return self.headers.get("Authorization") == f"Bearer {API_KEY}"
def _send(self, code, obj):
b = json.dumps(obj).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(b)))
self.end_headers()
self.wfile.write(b)
def do_POST(self):
if not self._auth():
self._send(401, {"error": "Unauthorized"}); return
n = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(n) or b"{}")
if self.path == "/v1/chat/completions":
msgs = body.get("messages") or []
last = ""
for m in msgs:
if m.get("role") == "user":
last = m.get("content") or last
model = body.get("model") or "grok-4"
self._send(200, {
"id": "chatcmpl-xai",
"model": model,
"choices": [{"index": 0, "message": {"role": "assistant", "content": f"[legacy-chat] {last[:160]}"}, "finish_reason": "stop"}],
})
return
if self.path == "/v1/responses":
# simplified Responses API shape
inp = body.get("input") or body.get("messages") or ""
if isinstance(inp, list):
text = ""
for m in inp:
if isinstance(m, dict) and m.get("role") == "user":
c = m.get("content")
text = c if isinstance(c, str) else str(c)
else:
text = str(inp)
model = body.get("model") or "grok-4"
self._send(200, {
"id": "resp_mock",
"model": model,
"output": [{"type": "message", "content": [{"type": "output_text", "text": f"[responses] {text[:160]}"}]}],
"output_text": f"[responses] {text[:160]}",
})
return
self.send_error(404)
def main():
import argparse
ap = argparse.ArgumentParser(); ap.add_argument("--port", type=int, default=9105)
args = ap.parse_args()
HTTPServer(("127.0.0.1", args.port), H).serve_forever()
if __name__ == "__main__":
main()
第 3 步:测试
import subprocess, sys, time
from pathlib import Path
HERE = Path(__file__).resolve().parent
def test_chat():
port = 19150
proc = subprocess.Popen([sys.executable, str(HERE/"mock_xai.py"), "--port", str(port)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
time.sleep(0.3)
r = subprocess.run([sys.executable, str(HERE/"grok_chat.py"), "hello grok", "--base", f"http://127.0.0.1:{port}", "--key", "xai-test-key"], capture_output=True, text=True, check=True)
assert "hello grok" in r.stdout
assert "legacy" in r.stderr.lower()
finally:
proc.terminate(); proc.wait(timeout=5)
第 4 步:运行
$ pytest grok chat mock
. [100%]
1 passed in 0.36s
$ invalid key api.x.ai
HTTP 400
{"code":"invalid-argument","error":"Incorrect API key provided. You can obtain an API key from https://console.x.ai."}
怎么检查结果?
- pytest 绿。
- stderr 含 legacy 提示。
调试示例
Incorrect API key。 打开 console.x.ai 创建密钥;确认环境变量无引号/空格。400 与部分厂商的 401 不同,以响应 JSON 为准。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 还要用 chat? | 兼容旧代码 | 新代码走 B 篇 |
| 模型 404 | ID 变更 | 对照控制台 |
| 流式 | 按文档 stream | 本篇未演示 |
模板规则
- 密钥保密。
- legacy 接口勿作为新架构默认。
- 输出需核对。
项目资源
| 用途 | 地址 |
|---|---|
| Chat Completions 指南(官方) | https://docs.x.ai/docs/guides/chat-completions |
| API Reference(官方) | https://docs.x.ai/docs/api-reference |
| Console | https://console.x.ai/ |
能跑通 legacy,接着迁到 Responses。