一、引言
openai 是 OpenAI 官方推出的 Python 客户端库,它封装了 OpenAI 系列模型(如 GPT、DALL-E、Whisper 等)的 RESTful API 调用,让开发者无需手动处理 HTTP 请求、鉴权和数据解析,就能快速将 AI 能力集成到 Python 应用中。
它的核心能力覆盖了:
- 文本生成:基于 GPT 系列模型实现对话、代码生成、内容创作与智能问答;
- 图像生成与理解:通过 DALL-E 生成图像,或结合 GPT-4V 实现视觉问答;
- 语音处理:Whisper 模型实现语音转文本,以及文本转语音;
- 向量嵌入:将文本转换为向量表示,支撑语义搜索、推荐系统等场景;
- 模型微调与助手应用:支持自定义模型训练,以及构建具备上下文记忆、工具调用能力的智能助手。
开发者只需几行代码就能调用 GPT、DeepSeek、豆包等模型完成文字对话、视觉理解、语音合成、图片生成等任务,由于接口已成为行业标准,DeepSeek、字节豆包、Moonshot、智谱等国内厂商均提供 OpenAI 兼容接口,只需替换 base_url 和 model 即可无缝切换:
Python
from openai import OpenAI
client = OpenAI(api_key="sk-...", base_url="https://api.openai.com/v1")
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello!"}],
)
print(resp.choices[0].message.content)
在 PC 或服务器端,openai 库依赖完善的 Python 标准库、充足的内存与算力,能轻松处理同步 / 异步请求、流式响应和复杂数据解析。但当我们想把 AI 能力带到 嵌入式 设备时,这些优势反而成了 "负担"。
在树莓派 Pico W、ESP32 这类资源受限的嵌入式设备上,运行 MicroPython 时,直接使用 PC 端的 openai 库几乎不可能:
- 内存极度受限:Pico 2W 可用堆内存约 150 KB,PC 端 openai SDK 依赖 httpx、pydantic 等重型库,完全无法运行;
- 无标准 HTTPS 客户端:需要基于原生 socket + ssl 手动实现 TLS 握手、HTTP/1.1 请求构造和响应解析。
- 异步模型不同:MicroPython 的
asyncio不支持asyncio.wait_for()超时,socket必须设为非阻塞模式,所有 I/O 循环需手动插入await asyncio.sleep_ms()让出 CPU。 - 大
payload发送问题:非阻塞socket缓冲区有限,发送base64图片等大JSON body时必须分块写入,否则数据截断导致服务端解析失败。 - 无 file 对象:PC 端通过
file=open(...)上传音频文件,MicroPython不支持此模式,需改用文件路径字符串流式上传。
uopenai 是专为 MicroPython 设计的轻量级 OpenAI 兼容异步客户端,依赖同样自研的 aiohttps 库,无任何其他外部依赖。它的设计目标是:在 Pico 2W 等嵌入式设备上,用与 PC 端 openai SDK 几乎相同的接口,调用 OpenAI 兼容的云端 LLM API:
Python
# 与 PC 端几乎一致的写法
from uopenai import OpenAI
client = OpenAI(api_key="...", base_url="https://ark.cn-beijing.volces.com/api/v3")
resp = await client.chat.completions.create(
model="deepseek-v3-2-251201",
messages=[{"role": "user", "content": "你好!"}],
)
print(resp.choices[0].message.content)
二、uopenai 接口说明
2.1 基本介绍
库的地址在:++https://upypi.net/en/pkgs/uopenai++
uopenai 是一个专为 MicroPython 设计的轻量级 OpenAI 兼容异步客户端库。它基于 aiohttps 实现,无其他外部依赖,支持非流式和流式(SSE)文字对话、视觉模型图片输入、base64 图片编码,特别适合内存受限的嵌入式设备(如 Pico 2W)与 OpenAI 兼容云端 API(DeepSeek、豆包、Moonshot 等)的对接。
主要功能包括:
- 文字对话(非流式) :
chat.completions.create()返回完整响应对象,含id、model、usage、choices - 文字对话(流式 SSE) :
stream=True返回aiohttps.Response,通过iter_lines()逐块读取 - 视觉模型 :支持
content为列表格式,传入image_url(base64 data URI) - 图片编码 :
OpenAI.encode_image(filepath)静态方法,将本地图片编码为 base64 字符串 - 请求超时 :
create(timeout_ms=30000)支持自定义超时,避免服务端无响应时永久阻塞 - 接口兼容 :与 PC 端
openaiSDK 保持最大接口兼容,base_url可替换为任意 OpenAI 兼容服务
内含文件包括:
Bash
uopenai/
├── code/
│ ├── uopenai.py # 驱动核心实现
│ ├── main.py # 使用示例 / 测试代码
│ └── test_4kb.jpg # 视觉测试用图(3516 字节,128x128)
├── package.json # mip 包配置(含 aiohttps 依赖)
├── README.md # 使用文档
└── LICENSE # MIT 开源协议
2.2 软件设计核心思想
与 PC 端 openai SDK 保持最大接口兼容:
Python
# PC 端 openai SDK
from openai import OpenAI
client = OpenAI(api_key="...", base_url="...")
resp = client.chat.completions.create(model="...", messages=[...])
# uopenai(MicroPython)
from uopenai import OpenAI
client = OpenAI(api_key="...", base_url="...")
resp = await client.chat.completions.create(model="...", messages=[...])
唯一区别:所有 create() 方法均为 async,需在 asyncio 事件循环中调用。
流式 SSE 读取
stream=True 时直接返回底层 aiohttps.Response,调用方通过 iter_lines() 逐行读取 SSE 数据,内存峰值仅为单行大小:
Python
stream_resp = await client.chat.completions.create(
model="...", messages=[...], stream=True
)
async for line in stream_resp.iter_lines():
if line.startswith(b"data: ") and line != b"data: [DONE]":
delta = json.loads(line[6:])["choices"][0]["delta"]
print(delta.get("content", ""), end="")
视觉模型与 base64 限制
encode_image() 将整个图片文件读入内存后编码,适合小图片(< 6 KB 原图)。Pico 2W 可用 RAM 约 150 KB,base64 编码后体积约为原图的 1.37 倍,总 JSON payload 需控制在 12 KB 以内以确保服务端正常响应。
2.3 API 速查和常用参数
2.4 使用注意
- 依赖 aiohttps :使用前必须先将
aiohttps.py(v1.1.3+)上传到设备根目录,mip 安装时会自动处理依赖。 - 所有 create() 均为 async :必须在
asyncio事件循环中调用,不支持同步调用。 - 不支持
file=open(...)传参 :MicroPython 无标准file对象,改用filepath=字符串(audio.transcriptions待实现)。 - 视觉模型图片大小限制 :
encode_image()将整个文件读入内存,建议原图 < 6 KB(base64 后 < 8 KB),总 JSON payload 控制在 12 KB 以内。超出可能导致服务端拒绝或响应超时。 - 超时设置 :默认
timeout_ms=30000(30 秒)。视觉模型响应较慢,建议设置timeout_ms=60000。 - thinking 模型兼容 :部分模型(如 doubao-seed)返回
"content": null,库已自动处理为空字符串。 - 待实现接口 :
audio.transcriptions.create()、audio.speech.create()、images.generations.create()当前为 TODO,调用后返回None。嵌入式 TTS/ASR 推荐使用 WebSocket 流式连接实现,参考xfyun_tts/xfyun_asr。
三、文字聊天和图片理解测试
这里,我们首先需要在火山引擎上创建 API Key,这是调用大模型的核心凭证:
登录火山方舟平台后,在左侧导航栏找到并进入「API Key 管理」页面,这里集中管理所有大模型服务的访问密钥,准备开启密钥创建流程。
在弹出的「创建 API Key」窗口中,所属项目默认选择 default(默认项目),名称可自定义(也可使用自动生成的时间戳命名),权限保持「全部(可访问项目下全部资源)」,确认配置后点击「创建」。
创建完成后,API Key 会出现在列表中,点击密钥旁的复制图标即可保存完整密钥(⚠️ 密钥仅创建时可完整查看,需妥善保管,避免泄露造成安全与资金风险)。
这里,我们选择下面两个模型进行测试:
- 支持多模态理解的
Doubao-Seed-2.0-mini模型 ------ 它支持文字、图片输入,能同时满足文本聊天与图片理解的需求; DeepSeek-V3.2模型,它平衡了推理能力与响应效率,适合通用问答等纯文本交互场景。
模型地址:https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seed-2-0-mini
模型地址:https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=deepseek-v3-2
注意,我们需要记住模型的 base_url 和 model 名字:
打开 Doubao-Seed-2.0-mini 模型详情页,点击右侧的「API 接入」按钮,这是从 "模型体验" 到 "实际开发调用" 的关键入口,后续所有接入配置都将从此展开。
在弹出的「快捷 API 接入」窗口中,完成 API Key 选择后,点击「STEP 2 快速接入测试」,这一步将帮助我们验证 API Key 的有效性,并获取官方提供的调用示例代码。
在快速接入测试界面中,切换到「OpenAI SDK 调用示例」标签。
从示例中,提取两个必须配置的关键参数:
base_url:https://ark.cn-beijing.volces.com/api/v3(火山方舟的 OpenAI 兼容接口地址);model:doubao-seed-2-0-mini-260215(当前选择的多模态模型 ID)。
这两个参数将直接用于 MicroPython 代码中,作为连接火山方舟 API 的核心配置。
接下来,我们在 upypi 上搜索 uopenai:
复制安装命令,在终端执行:
Bash
mpremote mip install https://upypi.net/pkgs/uopenai/1.0.0
接下来,我们将下面的图片命名为 test_4kb.jpg 烧录到测试用的单片机中:
同时将下面的测试 main.py 文件放进单片机中,记得替换为你的 API-Key:
Python
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time : 2026/04/16
# @Author : leeqingsui
# @File : main.py
# @Description : uopenai async OpenAI client test for MicroPython on Raspberry Pi Pico 2W
# ======================================== 导入相关模块 =========================================
import network
import asyncio
import time
import json
import ntptime
import os
from uopenai import OpenAI
# ======================================== 全局变量 ============================================
WIFI_SSID = "your_wifi_ssid"
WIFI_PASSWORD = "your_wifi_password"
# OpenAI 兼容接口配置(替换为你的实际 key 和 base_url)
API_KEY = "your_api_key"
BASE_URL = "https://api.openai.com/v1"
MODEL_CHAT = "your_chat_model"
MODEL_VISION = "your_vision_model"
# 测试用小图片文件(需提前上传到设备,建议 < 50 KB)
TEST_IMAGE_FILE = "test_image.jpg"
# 测试用 ~5KB 图片(base64 视觉测试用,建议 < 6 KB)
TEST_20KB_IMAGE = "test_5kb.jpg"
# ======================================== 功能函数 ============================================
def connect_wifi():
"""
连接 WiFi 并返回网络对象。
Returns:
network.WLAN: 已连接的 WLAN 对象;连接失败时返回 None。
==========================================
Connect to WiFi and return the network object.
Returns:
network.WLAN: Connected WLAN object; None if connection fails.
"""
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print("Connecting to WiFi: {}".format(WIFI_SSID))
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
timeout = 15
while not wlan.isconnected() and timeout > 0:
time.sleep(1)
timeout -= 1
print("Connecting... {}s remaining".format(timeout))
if wlan.isconnected():
print("WiFi connected, IP: {}".format(wlan.ifconfig()[0]))
else:
print("WiFi connection failed")
return None
else:
print("WiFi already connected")
return wlan
def sync_ntp():
"""
通过 NTP 同步系统时间。
==========================================
Sync system time via NTP.
"""
for host in ("ntp.aliyun.com", "ntp.tencent.com", "pool.ntp.org"):
try:
ntptime.host = host
ntptime.settime()
t = time.gmtime()
print("NTP synced via {}: {}-{:02d}-{:02d} {:02d}:{:02d}:{:02d} UTC".format(
host, t[0], t[1], t[2], t[3], t[4], t[5]))
return
except Exception as e:
print("NTP failed ({}): {}".format(host, e))
print("NTP sync unavailable.")
def prepare_test_image():
"""
生成一个极小的合法 JPEG 文件(1x1 白色像素),用于测试 encode_image()。
如果设备上已有 test_image.jpg,跳过生成。
==========================================
Generate a minimal valid JPEG (1x1 white pixel) for encode_image() testing.
Skipped if file already exists.
"""
try:
os.stat(TEST_IMAGE_FILE)
print(" image file already exists, skip generation")
except OSError:
# 最小合法 JPEG(1x1 白色像素),固定字节序列
minimal_jpeg = (
b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00"
b"\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t"
b"\x08\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a"
b"\x1f\x1e\x1d\x1a\x1c\x1c $.' \",#\x1c\x1c(7),01444\x1f'9=82<.342\x1b"
b"\xff\xc0\x00\x0b\x08\x00\x01\x00\x01\x01\x01\x11\x00"
b"\xff\xc4\x00\x1f\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b"
b"\xff\xc4\x00\xb5\x10\x00\x02\x01\x03\x03\x02\x04\x03\x05\x05\x04"
b"\x04\x00\x00\x01}\x01\x02\x03\x00\x04\x11\x05\x12!1A\x06\x13Qa"
b"\x07\"q\x142\x81\x91\xa1\x08#B\xb1\xc1\x15R\xd1\xf0$3br"
b"\x82\t\n\x16\x17\x18\x19\x1a%&'()*456789:CDEFGHIJ"
b"STUVWXYZ\xff\xda\x00\x08\x01\x01\x00\x00?\x00\xfb\xd4P\x00\x00\x00\xff\xd9"
)
with open(TEST_IMAGE_FILE, "wb") as f:
f.write(minimal_jpeg)
print(" generated minimal JPEG: {} ({} bytes)".format(TEST_IMAGE_FILE, len(minimal_jpeg)))
async def test_uopenai():
"""
uopenai 库全量测试。
Test 1: OpenAI() 初始化 + 非法参数校验
Test 2: chat.completions.create() 非流式,单轮对话
Test 3: chat.completions.create() 非流式,带 temperature/max_tokens kwargs
Test 4: chat.completions.create() 非流式,多轮对话(system + user)
Test 5: chat.completions.create() 流式(stream=True),iter_lines() 读 SSE
Test 6: encode_image() 静态方法,base64 编码
Test 7: max_tokens=2048 非流式文字
Test 8: max_tokens=2048 流式文字
Test 9: ~6KB base64 图片非流式视觉
Test 10: base_url 末尾斜杠自动去除
Test 11: 非法参数全覆盖(ValueError / TypeError)
Test 12: 响应对象属性完整性 + doubao 文字对话
Test 13: audio.speech.create() --- SKIP (TODO)
Test 14: images.generations.create() --- SKIP (TODO)
==========================================
Full test suite for uopenai library.
"""
# 1. 连接 WiFi
if not connect_wifi():
return
# 2. 同步 NTP
sync_ntp()
print("--- uopenai Test Start ---")
# ------------------------------------------------------------------ #
# Test 1: 初始化 + 非法参数校验
# ------------------------------------------------------------------ #
print("[1/14] OpenAI() init + invalid param guard")
try:
# 正常初始化
client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
ok = hasattr(client, "chat") and hasattr(client, "audio")
# 空 api_key 应抛 ValueError
caught = False
try:
OpenAI(api_key="")
except ValueError:
caught = True
print(" [1/14] PASS" if ok and caught else " [1/14] FAIL")
except Exception as e:
print(" [1/14] ERROR:", e)
# ------------------------------------------------------------------ #
# Test 2: 非流式单轮对话
# ------------------------------------------------------------------ #
print("[2/14] chat.completions.create() non-stream single turn")
try:
resp = await client.chat.completions.create(
model=MODEL_CHAT,
messages=[{"role": "user", "content": "Reply with the single word: OK"}],
)
content = resp.choices[0].message.content
print(" reply:", content)
print(" [2/14] PASS" if len(content) > 0 else " [2/14] FAIL (empty content)")
except Exception as e:
print(" [2/14] ERROR:", e)
# ------------------------------------------------------------------ #
# Test 3: 非流式,带 kwargs(temperature / max_tokens)
# ------------------------------------------------------------------ #
print("[3/14] chat.completions.create() with temperature + max_tokens")
try:
resp = await client.chat.completions.create(
model=MODEL_CHAT,
messages=[{"role": "user", "content": "Say hello in one word."}],
temperature=0.0,
max_tokens=10,
)
content = resp.choices[0].message.content
print(" reply:", content)
print(" [3/14] PASS" if len(content) > 0 else " [3/14] FAIL")
except Exception as e:
print(" [3/14] ERROR:", e)
# ------------------------------------------------------------------ #
# Test 4: 非流式,多轮对话(system + user)
# ------------------------------------------------------------------ #
print("[4/14] chat.completions.create() multi-turn (system + user)")
try:
resp = await client.chat.completions.create(
model=MODEL_CHAT,
messages=[
{"role": "system", "content": "You are a helpful assistant. Reply concisely."},
{"role": "user", "content": "What is 1+1?"},
],
)
content = resp.choices[0].message.content
print(" reply:", content)
# 回答应包含 "2"
print(" [4/14] PASS" if "2" in content else " [4/14] FAIL (no '2' in reply)")
except Exception as e:
print(" [4/14] ERROR:", e)
# ------------------------------------------------------------------ #
# Test 5: 流式对话(stream=True),iter_lines() 读 SSE
# ------------------------------------------------------------------ #
print("[5/14] chat.completions.create() stream=True + iter_lines()")
try:
stream_resp = await client.chat.completions.create(
model=MODEL_CHAT,
messages=[{"role": "user", "content": "Count from 1 to 3, one number per line."}],
stream=True,
max_tokens=50,
)
chunks_received = 0
full_text = ""
async for line in stream_resp.iter_lines():
line = line.strip()
if not line:
continue
if line == b"data: [DONE]":
break
# SSE 格式:b"data: {...}"
if line.startswith(b"data: "):
raw = line[6:]
try:
obj = json.loads(raw)
delta = obj.get("choices", [{}])[0].get("delta", {})
piece = delta.get("content", "")
if piece:
full_text += piece
chunks_received += 1
except Exception:
pass
print(" chunks received:", chunks_received)
print(" assembled text:", full_text)
print(" [5/14] PASS" if chunks_received > 0 else " [5/14] FAIL (no chunks)")
except Exception as e:
print(" [5/14] ERROR:", e)
# ------------------------------------------------------------------ #
# Test 6: encode_image() 静态方法
# ------------------------------------------------------------------ #
print("[6/14] OpenAI.encode_image()")
try:
prepare_test_image()
b64 = OpenAI.encode_image(TEST_IMAGE_FILE)
print(" base64 length:", len(b64))
# base64 字符只含 A-Z a-z 0-9 + / =
valid = all(c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" for c in b64)
print(" [6/14] PASS" if len(b64) > 0 and valid else " [6/14] FAIL (invalid base64)")
except Exception as e:
print(" [6/14] ERROR:", e)
# ------------------------------------------------------------------ #
# Test 7: max_tokens=2048 非流式文字
# ------------------------------------------------------------------ #
print("[7/14] max_tokens=2048 non-stream text")
try:
resp = await client.chat.completions.create(
model=MODEL_CHAT,
messages=[{"role": "user", "content": "Write a short poem about the ocean in 3 lines."}],
max_tokens=2048,
)
content = resp.choices[0].message.content
finish = resp.choices[0].finish_reason
print(" reply:", content)
print(" finish_reason:", finish)
print(" [7/14] PASS" if len(content) > 0 and finish == "stop" else " [7/14] FAIL")
except Exception as e:
print(" [7/14] ERROR:", e)
# ------------------------------------------------------------------ #
# Test 12: max_tokens=2048 流式文字
# ------------------------------------------------------------------ #
print("[8/14] max_tokens=2048 stream text")
try:
stream_resp = await client.chat.completions.create(
model=MODEL_CHAT,
messages=[{"role": "user", "content": "Write a short poem about the ocean in 3 lines."}],
stream=True,
max_tokens=2048,
)
full_text = ""
async for line in stream_resp.iter_lines():
line = line.strip()
if not line or line == b"data: [DONE]":
continue
if line.startswith(b"data: "):
try:
delta = json.loads(line[6:]).get("choices", [{}])[0].get("delta", {})
full_text += delta.get("content", "")
except Exception:
pass
print(" assembled:", full_text)
print(" [8/14] PASS" if len(full_text) > 0 else " [8/14] FAIL")
except Exception as e:
print(" [8/14] ERROR:", e)
# ------------------------------------------------------------------ #
# Test 9: ~5KB base64 图片非流式视觉
# ------------------------------------------------------------------ #
print("[9/14] ~5KB base64 image non-stream vision")
try:
import gc
gc.collect()
print(" free RAM before encode:", gc.mem_free())
b64 = OpenAI.encode_image("test_4kb.jpg")
gc.collect()
print(" base64 length:", len(b64), "free RAM after encode:", gc.mem_free())
resp = await client.chat.completions.create(
model=MODEL_VISION,
messages=[{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64," + b64}},
{"type": "text", "text": "What colors do you see? Reply in one sentence."},
]}],
max_tokens=100,
timeout_ms=60000,
)
gc.collect()
print(" free RAM after request:", gc.mem_free())
content = resp.choices[0].message.content
print(" reply:", content)
print(" [9/14] PASS" if len(content) > 0 else " [9/14] FAIL (empty reply)")
except Exception as e:
print(" [9/14] ERROR:", e)
# ------------------------------------------------------------------ #
# Test 12: base_url 末尾斜杠自动去除
# ------------------------------------------------------------------ #
print("[10/14] base_url trailing slash strip")
try:
c2 = OpenAI(api_key="dummy", base_url="https://api.openai.com/v1/")
ok = not c2._base_url.endswith("/")
print(" _base_url:", c2._base_url)
print(" [10/14] PASS" if ok else " [10/14] FAIL (slash not stripped)")
except Exception as e:
print(" [10/14] ERROR:", e)
# ------------------------------------------------------------------ #
# Test 11: 非法参数校验(api_key / model / messages / filepath)
# ------------------------------------------------------------------ #
print("[11/14] invalid param guards (ValueError / TypeError)")
try:
results = []
# api_key 为 None
try:
OpenAI(api_key=None)
results.append(False)
except ValueError:
results.append(True)
# model 为空
try:
await client.chat.completions.create(model="", messages=[{"role": "user", "content": "hi"}])
results.append(False)
except ValueError:
results.append(True)
# messages 为空列表
try:
await client.chat.completions.create(model=MODEL_CHAT, messages=[])
results.append(False)
except ValueError:
results.append(True)
# transcriptions filepath 为空
try:
await client.audio.transcriptions.create(model="whisper-1", filepath="")
results.append(False)
except ValueError:
results.append(True)
# encode_image filepath 为 None
try:
OpenAI.encode_image(None)
results.append(False)
except ValueError:
results.append(True)
all_ok = all(results)
print(" guard results:", results)
print(" [11/14] PASS" if all_ok else " [11/14] FAIL")
except Exception as e:
print(" [11/14] ERROR:", e)
# ------------------------------------------------------------------ #
# Test 12: 响应对象属性完整性 + doubao vision model 文字对话
# ------------------------------------------------------------------ #
print("[12/14] response attributes check + vision model text chat")
try:
resp = await client.chat.completions.create(
model=MODEL_VISION,
messages=[{"role": "user", "content": "Say yes."}],
)
has_id = isinstance(resp.id, str)
has_model = isinstance(resp.model, str)
has_usage = isinstance(resp.usage, dict)
has_choice = len(resp.choices) > 0
has_role = resp.choices[0].message.role == "assistant"
has_reason = isinstance(resp.choices[0].finish_reason, str)
print(" id:", resp.id)
print(" model:", resp.model)
print(" usage:", resp.usage)
print(" finish_reason:", resp.choices[0].finish_reason)
print(" role:", resp.choices[0].message.role)
ok = has_id and has_model and has_usage and has_choice and has_role and has_reason
print(" [12/14] PASS" if ok else " [12/14] FAIL")
except Exception as e:
print(" [12/14] ERROR:", e)
print("--- uopenai Test Done ---")
# ------------------------------------------------------------------ #
# Test 13: audio.speech.create() --- SKIP (TODO)
# ------------------------------------------------------------------ #
print("[13/14] audio.speech.create() -- SKIP (TODO: WebSocket streaming TTS)")
# ------------------------------------------------------------------ #
# Test 14: images.generations.create() --- SKIP (TODO)
# ------------------------------------------------------------------ #
print("[14/14] images.generations.create() -- SKIP (TODO: low-res image gen model)")
# ======================================== 自定义类 ============================================
# ======================================== 初始化配置 ===========================================
time.sleep(3)
print("FreakStudio: uopenai async OpenAI client test")
# ======================================== 主程序 ===========================================
if __name__ == "__main__":
asyncio.run(test_uopenai())
上面代码中,在完成了网络时间同步和连接 WIFI 后,进行了下面的测试:
- 测试 1:客户端初始化 + 非法参数校验
- 正常初始化
OpenAI客户端; - 测试空 API 密钥是否会抛出错误(鲁棒性测试)。
- 正常初始化
- 测试 2~4:基础文字对话(非流式)
- 单轮对话、带参数(温度 / 最大 token)对话、多轮对话(系统提示 + 用户提问);
- 验证纯文字聊天功能正常。
- 测试 5:流式对话(核心功能)
- 开启
stream=True,实时接收模型输出(逐字返回); - 解析 SSE 数据流,拼接完整回复;
- 模拟真实聊天的实时输出效果。
- 开启
- 测试 6:图片
Base64 编码- 调用
OpenAI.encode_image()将图片转为 Base64 字符串; - 多模态视觉的前提(大模型只能接收 Base64 格式的图片)。
- 调用
- 测试 7~8:长文本对话
- 设置
max_tokens=2048,测试大长度文本的流式 / 非流式输出; - 验证库能处理长文本响应。
- 设置
- 测试 9:多模态图片理解
- 嵌入式设备上传图片,大模型分析图片内容并回复;
- 测试 10:接口地址兼容处理
- 自动去除
base_url末尾的斜杠,避免接口调用报错。
- 自动去除
- 测试 11:全非法参数校验
- 测试空模型、空消息、空文件路径等非法输入;
- 确保库能拦截错误参数,不崩溃。
- 测试 12:响应数据完整性校验
- 验证模型返回的
id/model/usage/choices等字段完整; - 确保数据格式符合标准。
- 验证模型返回的
烧录代码,运行如下:
上图中,我们可以看到:
- ✅ 12 项核心测试全部通过(PASS),2 项待开发功能(音频 / 图片生成)已按计划跳过。
- ✅ 从 WiFi 联网 → 时间同步 →API 调用 → 文字对话 → 多模态图片理解,整个嵌入式 大模型链路 100% 跑通,而且内存占用极低、稳定性拉满。
下面,我们逐步按测试序号拆解关键信息:
[1/14] 客户端初始化 + 非法参数校验
Bash
[1/14] OpenAI() init + invalid param guard
[1/14] PASS
- 客户端
OpenAI(api_key=..., base_url=...)初始化成功; - 空 API Key 等非法参数的校验逻辑生效,库的鲁棒性正常,不会因为错误输入崩溃。
[2/14] 单轮非流式对话
Bash
[2/14] chat.completions.create() non-stream single turn
reply: OK
[2/14] PASS
模型按要求返回了 OK,说明基础纯文本 API 调用完全正常:网络请求、JSON 解析、响应处理都没 bug。
[3/14] 带参数的对话(温度 + 最大 token)
Bash
[3/14] chat.completions.create() with temperature + max_tokens
reply: Hi
[3/14] PASS
传递的 temperature=0.0、max_tokens=10 参数生效,模型按要求返回了简短的 Hi,说明参数传递逻辑正确。
[4/14] 多轮对话(带 System 提示词)
Bash
[4/14] chat.completions.create() multi-turn (system + user)
reply: 1 + 1 = 2
[4/14] PASS
模型正确理解了 "What is 1+1?" 的问题,返回了正确结果,说明多轮对话的上下文传递、System 角色处理都正常。
[5/14] 流式对话(实时逐块输出)
Bash
[5/14] chat.completions.create() stream=True + iter_lines()
chunks received: 5
assembled text: 1
2
3
[5/14] PASS
- 流式传输收到了
5个数据块,拼接后是完整的1 2 3; - 说明SSE 流式解析、逐块处理、文本拼接都完全正常,嵌入式设备也能流畅处理模型的实时输出。
[6/14] 图片 Base64 编码
Bash
[6/14] OpenAI.encode_image()
image file already exists, skip generation
base64 length: 332
[6/14] PASS
- 设备里已有测试图片,跳过了自动生成步骤;
- 编码后的 Base64 长度为 332,说明
encode_image方法工作正常,生成的字符串是合法的,为后面的图片理解测试打好了基础。
[7/14] 长文本非流式对话(max_tokens=2048)
Bash
[7/14] max_tokens=2048 non-stream text
reply: A ceaseless, cobalt breath of tide,
Where ancient secrets swell and hide,
Then whisper back to sands of time.
finish_reason: stop
[7/14] PASS
- 模型生成了完整的三行诗,
finish_reason: stop说明是模型正常结束生成,没有被截断; - 验证了库能处理大长度响应,没有超时或截断问题。
[8/14] 长文本流式对话(max_tokens=2048)
Bash
[8/14] max_tokens=2048 stream text
assembled: A cobalt whisper, deep and wide,
Where ancient secrets in darkness hide.
A restless heart, both fierce and free.
[8/14] PASS
- 流式模式下也能完整生成三行诗,和非流式结果对应;
- 说明长短文本的流式传输都稳定,没有丢块、乱序的问题。
[9/14] 多模态图片理解
SQL
[9/14] ~5KB base64 image non-stream vision
free RAM before encode: 425584
base64 length: 4688 free RAM after encode: 421216
free RAM after request: 411280
reply: This image displays smooth color gradients shifting from lime green on the left to bright magenta on the right, paired with pixelated, noisy smudges of deep blue, cyan, and violet along its bottom and right edges.
[9/14] PASS
我们看一下:
- 内存表现:编码前空闲内存 425KB,编码后 421KB,请求后 411KB,整个过程内存占用极低,嵌入式设备完全能承受,没有溢出风险;
- 图片编码:5KB 的图片转成 Base64 后是 4688 字节,格式合法;
- 模型响应 :模型准确描述了图片的颜色、渐变和噪点,说明图片被成功上传、模型解析正常,
uopenai库的多模态接口完美适配火山方舟的视觉模型。
后面主要测试库的兼容性和校验功能:
- 库自动去除了 BaseURL 末尾的斜杠,避免了接口报错的问题;
- 测试了空 API Key、空模型名、空消息列表、空文件路径等错误输入,库都正确抛出了异常;
- 以及响应对象完整性:
YAML
[12/14] response attributes check + vision model text chat
id: 021776345473490b686cf77507a74aa4905f351832e661725ea7f
model: doubao-seed-2-0-mini-260215
usage: {'prompt_tokens_details': {'cached_tokens': 0}, 'completion_tokens': 74, 'completion_tokens_details': {'reasoning_tokens': 72}, 'total_tokens': 126, 'prompt_tokens': 52}
finish_reason: stop
role: assistant
[12/14] PASS