feat!: 账号池改渠道网关,对外协议换成 vivid /v1
- 新增 channels/channel_models:上游 URL + AES-GCM 加密 key +
并发/权重/本地统计,模型经渠道 /v1/models 导入绑定
- internal/upstream:vivid 协议客户端(NDJSON 流 + jobId 断流重连 +
uploads 预签名 + models + balance + 错误分类)
- service/v1 重写:权重分组调度、Redis 并发槽 conc:c:、故障转移,
站内/测试/对外统一走渠道执行器
- 对外端点换成 vivid 四件套:POST /v1/generate(NDJSON+jobId 重连)、
POST /v1/uploads(SigV4 预签名 PUT)、GET /v1/models、GET /v1/balance,
错误包 {"error":{"code","message"}}
- API key 新发改 vk_+48hex;删除全部内置 provider 与 token 池
- 管理端:渠道 CRUD/拉模型/导入/余额端点,前端账号页换渠道页,
概览/文档/测试弹窗适配
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
+107
-252
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
// API 对接文档 — OpenAI-compatible. Lists live models and shows ready-to-run
|
||||
// curl / Python(openai SDK) examples for image + video, wired to this
|
||||
// deployment's base URL and the caller's model ids.
|
||||
// API 对接文档 — Vivid 协议(/v1/generate NDJSON 长连接)。列出在售模型
|
||||
// 与计费档位,示例代码直接可跑。
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { auth } from '../auth'
|
||||
import { api } from '../api'
|
||||
@@ -10,7 +9,7 @@ import { points } from '../credits'
|
||||
import { mediaCaps, presetMap } from '../videoCaps'
|
||||
|
||||
const base = computed(() => location.origin) // /v1 is same-origin (dev: Vite proxy)
|
||||
const keyHint = computed(() => auth.user?.api_keys?.[0]?.key_preview || 'YOUR_API_KEY')
|
||||
const keyHint = computed(() => auth.user?.api_keys?.[0]?.key_preview || 'vk_xxxx')
|
||||
|
||||
const models = ref([])
|
||||
const presets = ref({}) // /video-presets: key → 预设(参考资产分类上限的真源)
|
||||
@@ -31,8 +30,10 @@ const videoModels = computed(() => models.value.filter((m) => m.type === 'video'
|
||||
function pubName(m) {
|
||||
return m?.alias || m?.id || ''
|
||||
}
|
||||
const sampleImage = computed(() => pubName(imageModels.value[0]) || 'firefly-image-4')
|
||||
const sampleVideo = computed(() => pubName(videoModels.value[0]) || 'firefly-kling3')
|
||||
const sampleImage = computed(() => pubName(imageModels.value[0]) || 'gpt-image-2')
|
||||
const sampleVideo = computed(() => pubName(videoModels.value[0]) || 'video-model')
|
||||
const sampleQuality = computed(() => (imageModels.value[0]?.resolutions || [])[0] || '1K')
|
||||
const sampleVQuality = computed(() => (videoModels.value[0]?.resolutions || [])[0] || '720p')
|
||||
const sampleSeconds = computed(() => String(videoModels.value[0]?.durations?.[0] || '8s').replace(/s$/, ''))
|
||||
|
||||
function modeEmpty(m) {
|
||||
@@ -61,183 +62,118 @@ function priceEmpty(m) {
|
||||
if (perSecondPrice(m) != null) return false
|
||||
return !(m.durations || []).some((d) => durPrice(m, d) != null)
|
||||
}
|
||||
// 视频时长展示:固定/离散档列出来;区间档(duration_min/max)显示 min–max s。
|
||||
function durText(m) {
|
||||
const ds = m.durations || []
|
||||
if (ds.length) return ds.join('/')
|
||||
if ((m.duration_max || 0) > 0) return `${m.duration_min}–${m.duration_max}s`
|
||||
return ''
|
||||
}
|
||||
|
||||
// ---- request parameter tables ----
|
||||
const imageParams = [
|
||||
['model', 'string', '必填', '模型名(别名优先),见上表(图像)'],
|
||||
['prompt', 'string', '必填', '文字描述'],
|
||||
['size', 'string', '可选', '宽x高,如 "1024x1024"。同时决定「比例」+「分辨率档」(按长边)。具体怎么填见下方对照表;留空 = 1:1 · 2K'],
|
||||
]
|
||||
const editParams = [
|
||||
['image', 'file', '必填', '输入图;多张参考图重复 image[] 字段(multipart 文件上传)'],
|
||||
['prompt', 'string', '必填', '编辑/参考描述'],
|
||||
['model', 'string', '必填', '模型名(别名优先,需支持图生图)'],
|
||||
['size', 'string', '可选', '同图像:决定比例 + 分辨率档(见下方对照表)'],
|
||||
]
|
||||
const videoParams = [
|
||||
['model', 'string', '必填', '模型名(别名优先),见上表(视频)'],
|
||||
['prompt', 'string', '必填', '文字描述'],
|
||||
['seconds', 'string|int', '必填', '时长秒数,如 "5" "8"(取决于模型支持)'],
|
||||
['size', 'string', '可选', '如 "1280x720" / "720x1280" → 决定比例与分辨率'],
|
||||
['input_reference', 'file', '可选', '参考图/首尾帧(multipart 文件上传;多张时重复 input_reference[] 字段)。runway 图生视频必填 1 张,张数上限见上方模型表'],
|
||||
['reference_mode', 'string', '可选', '参考图用途:"frame"=首尾帧(最多 2 张,第 1 张为首帧、第 2 张为尾帧,只传 1 张即仅首帧),"asset"=普通参考图。留空用模型默认;仅支持单一模式的模型不可传'],
|
||||
const generateParams = [
|
||||
['model', 'string', '必填', '模型名(别名优先),见上表 /v1/models 的 id'],
|
||||
['prompt', 'string', '必填', '文本描述'],
|
||||
['quality', 'string', '可选', '清晰度档位,取该模型 qualities 之一(图像如 1K/2K/4K,视频如 720p/1080p);不传 = 该模型最低档'],
|
||||
['ratio', 'string', '可选', '宽高比,如 "16:9";不传 = 类型默认'],
|
||||
['duration', 'int64', '可选', '视频时长(秒);range/options 模型必填,图片忽略'],
|
||||
['refs', 'array<string>', '可选', '参考素材 key 数组:先 POST /v1/uploads 拿预签名直传,PUT 上传后把返回的 key 放这里。30 分钟内有效,张数上限见模型表'],
|
||||
['jobId', 'string', '可选', '重连模式:流意外断开后只带 jobId 原样重发,不创建新任务、不重复扣费;已完成直接回终态帧'],
|
||||
]
|
||||
|
||||
// ---- size → 比例 × 分辨率档 对照表(用 size 该传的值)----
|
||||
// size 的长边映射档位:<1800→1K · 1800–3499→2K · ≥3500→4K;宽高比映射比例。
|
||||
const sizeTable = [
|
||||
{ ratio: '1:1 · 方', k1: '1024x1024', k2: '2048x2048', k4: '4096x4096' },
|
||||
{ ratio: '5:4 · 横', k1: '1280x1024', k2: '2560x2048', k4: '3840x3072' },
|
||||
{ ratio: '4:3 · 横', k1: '1024x768', k2: '2048x1536', k4: '4096x3072' },
|
||||
{ ratio: '3:2 · 横', k1: '1200x800', k2: '2400x1600', k4: '3600x2400' },
|
||||
{ ratio: '16:9 · 横', k1: '1280x720', k2: '2048x1152', k4: '4096x2304' },
|
||||
{ ratio: '2:1 · 横', k1: '1440x720', k2: '2880x1440', k4: '4096x2048' },
|
||||
{ ratio: '21:9 · 超宽', k1: '1680x720', k2: '2520x1080', k4: '5040x2160' },
|
||||
{ ratio: '3:1 · 超宽', k1: '1536x512', k2: '2304x768', k4: '3840x1280' },
|
||||
{ ratio: '4:1 · 超宽', k1: '1728x432', k2: '2880x720', k4: '4096x1024' },
|
||||
{ ratio: '8:1 · 超宽', k1: '1728x216', k2: '2880x360', k4: '4096x512' },
|
||||
{ ratio: '4:5 · 竖', k1: '1024x1280', k2: '2048x2560', k4: '3072x3840' },
|
||||
{ ratio: '3:4 · 竖', k1: '768x1024', k2: '1536x2048', k4: '3072x4096' },
|
||||
{ ratio: '2:3 · 竖', k1: '800x1200', k2: '1600x2400', k4: '2400x3600' },
|
||||
{ ratio: '9:16 · 竖', k1: '720x1280', k2: '1152x2048', k4: '2304x4096' },
|
||||
{ ratio: '1:3 · 竖', k1: '512x1536', k2: '768x2304', k4: '1280x3840' },
|
||||
{ ratio: '1:4 · 竖', k1: '432x1728', k2: '720x2880', k4: '1024x4096' },
|
||||
{ ratio: '1:8 · 竖', k1: '216x1728', k2: '360x2880', k4: '512x4096' },
|
||||
]
|
||||
|
||||
// ---- 视频 size → 比例 × 分辨率(720p / 1080p)----
|
||||
// 视频按「短边」判档:短边 <1080 → 720p,≥1080 → 1080p;宽高比映射比例。
|
||||
const videoSizeTable = [
|
||||
{ ratio: '16:9 · 横', p720: '1280x720', p1080: '1920x1080' },
|
||||
{ ratio: '9:16 · 竖', p720: '720x1280', p1080: '1080x1920' },
|
||||
{ ratio: '1:1 · 方', p720: '720x720', p1080: '1080x1080' },
|
||||
{ ratio: '4:3 · 横', p720: '960x720', p1080: '1440x1080' },
|
||||
{ ratio: '3:4 · 竖', p720: '720x960', p1080: '1080x1440' },
|
||||
{ ratio: '3:2 · 横', p720: '1080x720', p1080: '1620x1080' },
|
||||
{ ratio: '2:3 · 竖', p720: '720x1080', p1080: '1080x1620' },
|
||||
const uploadParams = [
|
||||
['kind', 'string', '必填', '素材大类:image / video / audio(与文件实际类型一致)'],
|
||||
['mime', 'string', '必填', '文件 MIME(如 image/png),须与 kind 自洽;PUT 上传的 Content-Type 必须与它一字不差(已签进 URL)'],
|
||||
]
|
||||
|
||||
// ---- examples (built in script so refs resolve correctly) ----
|
||||
const examples = computed(() => [
|
||||
{
|
||||
title: '文生图 · curl',
|
||||
title: '文生图 · curl (NDJSON 流)',
|
||||
code:
|
||||
`curl ${base.value}/v1/images/generations \\
|
||||
`curl -N ${base.value}/v1/generate \\
|
||||
-H "Authorization: Bearer ${keyHint.value}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"model": "${sampleImage.value}",
|
||||
"prompt": "a corgi running in a golden wheat field, cinematic",
|
||||
"size": "2048x2048"
|
||||
}'`,
|
||||
"quality": "${sampleQuality.value}",
|
||||
"ratio": "1:1"
|
||||
}'
|
||||
# 响应是 NDJSON:首行 {"jobId":"...","status":"running"},空行为心跳(忽略),
|
||||
# 末行终态 {"status":"succeeded","result":"<预签名URL>","creditsCharged":10}`,
|
||||
},
|
||||
{
|
||||
title: '文生图 · Python (openai SDK)',
|
||||
title: '生图 · Python (流式读终态)',
|
||||
code:
|
||||
`import urllib.request
|
||||
from openai import OpenAI
|
||||
`import json, requests
|
||||
|
||||
client = OpenAI(api_key="${keyHint.value}", base_url="${base.value}/v1")
|
||||
r = requests.post("${base.value}/v1/generate", stream=True, timeout=600,
|
||||
headers={"Authorization": "Bearer ${keyHint.value}"},
|
||||
json={"model": "${sampleImage.value}", "prompt": "a cute cat", "quality": "${sampleQuality.value}"})
|
||||
|
||||
resp = client.images.generate(
|
||||
model="${sampleImage.value}",
|
||||
prompt="a corgi running in a golden wheat field, cinematic",
|
||||
size="2048x2048", # 2K · 1:1,见下方对照表
|
||||
)
|
||||
# 结果是图片 URL(上游原始直链,会过期 → 尽快下载/转存)
|
||||
urllib.request.urlretrieve(resp.data[0].url, "out.png")`,
|
||||
job_id, final = None, None
|
||||
for line in r.iter_lines():
|
||||
if not line: # 空行 = 心跳,忽略
|
||||
continue
|
||||
frame = json.loads(line)
|
||||
job_id = frame.get("jobId") or job_id
|
||||
if frame.get("status") in ("succeeded", "failed"):
|
||||
final = frame
|
||||
break
|
||||
|
||||
# 断流重连:只带 jobId 重发即继续等同一任务(不重复扣费)
|
||||
# requests.post(..., json={"jobId": job_id})
|
||||
print(final)`,
|
||||
},
|
||||
{
|
||||
title: '图生图 / 参考图 · curl (multipart)',
|
||||
title: '参考素材上传 · curl (预签名直传)',
|
||||
code:
|
||||
`curl ${base.value}/v1/images/edits \\
|
||||
`# 1) 申请上传会话 → 拿 {key, url}
|
||||
resp=$(curl -s ${base.value}/v1/uploads \\
|
||||
-H "Authorization: Bearer ${keyHint.value}" \\
|
||||
-F model="${sampleImage.value}" \\
|
||||
-F prompt="把这张图改成赛博朋克风格" \\
|
||||
-F size="2048x2048" \\
|
||||
-F image=@input.png
|
||||
# 多张参考图:重复 -F image=@a.png -F image=@b.png`,
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"kind":"image","mime":"image/png"}')
|
||||
|
||||
# 2) PUT 直传(Content-Type 必须与申请的 mime 完全一致)
|
||||
curl -X PUT -H "Content-Type: image/png" --data-binary @input.png "<url>"
|
||||
|
||||
# 3) key 放进 /v1/generate 的 refs
|
||||
curl -N ${base.value}/v1/generate \\
|
||||
-H "Authorization: Bearer ${keyHint.value}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"model":"${sampleImage.value}","prompt":"改成赛博朋克风格","refs":["<key>"]}'`,
|
||||
},
|
||||
{
|
||||
title: '图生图 · Python (openai SDK)',
|
||||
title: '视频 · curl',
|
||||
code:
|
||||
`import urllib.request
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(api_key="${keyHint.value}", base_url="${base.value}/v1")
|
||||
|
||||
resp = client.images.edit(
|
||||
model="${sampleImage.value}",
|
||||
image=open("input.png", "rb"), # 多张:image=[open("a.png","rb"), open("b.png","rb")]
|
||||
prompt="把这张图改成赛博朋克风格",
|
||||
)
|
||||
# 结果是图片 URL(上游原始直链,会过期 → 尽快下载/转存)
|
||||
urllib.request.urlretrieve(resp.data[0].url, "out.png")`,
|
||||
},
|
||||
{
|
||||
title: '视频 · curl(创建 → 轮询 → 下载)',
|
||||
code:
|
||||
`# 1) 创建任务 → 立即返回 {"id": "...", "status": "queued"}
|
||||
curl ${base.value}/v1/videos \\
|
||||
`curl -N ${base.value}/v1/generate \\
|
||||
-H "Authorization: Bearer ${keyHint.value}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"model": "${sampleVideo.value}",
|
||||
"prompt": "a paper boat sailing down a rainy street, cinematic",
|
||||
"seconds": "${sampleSeconds.value}",
|
||||
"size": "1280x720"
|
||||
"quality": "${sampleVQuality.value}",
|
||||
"ratio": "16:9",
|
||||
"duration": ${sampleSeconds.value}
|
||||
}'
|
||||
|
||||
# 2) 轮询状态,直到 status=completed
|
||||
curl ${base.value}/v1/videos/<VIDEO_ID> \\
|
||||
-H "Authorization: Bearer ${keyHint.value}"
|
||||
|
||||
# 3) 下载 mp4(完成后)
|
||||
curl ${base.value}/v1/videos/<VIDEO_ID>/content \\
|
||||
-H "Authorization: Bearer ${keyHint.value}" -o out.mp4`,
|
||||
},
|
||||
{
|
||||
title: '视频 · Python (requests, 轮询)',
|
||||
code:
|
||||
`import time, requests
|
||||
|
||||
base = "${base.value}/v1"
|
||||
h = {"Authorization": "Bearer ${keyHint.value}"}
|
||||
|
||||
# 1) 创建
|
||||
job = requests.post(f"{base}/videos", headers=h, json={
|
||||
"model": "${sampleVideo.value}",
|
||||
"prompt": "a paper boat sailing down a rainy street",
|
||||
"seconds": "${sampleSeconds.value}",
|
||||
"size": "1280x720",
|
||||
}).json()
|
||||
vid = job["id"]
|
||||
|
||||
# 2) 轮询
|
||||
while True:
|
||||
s = requests.get(f"{base}/videos/{vid}", headers=h).json()
|
||||
if s["status"] in ("completed", "failed"):
|
||||
break
|
||||
time.sleep(5)
|
||||
|
||||
# 3) 下载
|
||||
if s["status"] == "completed":
|
||||
mp4 = requests.get(f"{base}/videos/{vid}/content", headers=h).content
|
||||
open("out.mp4", "wb").write(mp4)`,
|
||||
# 与图像同一端点:quality 传分辨率档,duration 传秒数`,
|
||||
},
|
||||
{
|
||||
title: '列出模型 · curl',
|
||||
code:
|
||||
`curl ${base.value}/v1/models \\
|
||||
-H "Authorization: Bearer ${keyHint.value}"`,
|
||||
-H "Authorization: Bearer ${keyHint.value}"
|
||||
|
||||
# => [{"id":"...","kind":"image","qualities":["1K","2K"],"refImages":4}, ...]`,
|
||||
},
|
||||
{
|
||||
title: '查询余额 · curl',
|
||||
code:
|
||||
`curl ${base.value}/v1/user/balance \\
|
||||
`curl ${base.value}/v1/balance \\
|
||||
-H "Authorization: Bearer ${keyHint.value}"
|
||||
|
||||
# => {"object":"user.balance","balance":12000,"used":680,"total":12680}`,
|
||||
# => {"balance":980}`,
|
||||
},
|
||||
])
|
||||
|
||||
@@ -255,7 +191,7 @@ async function copy(text) {
|
||||
<header>
|
||||
<span class="sticker">✦ 开发者</span>
|
||||
<h1 class="mt-3 text-4xl md:text-5xl font-bold tracking-tight">接口文档</h1>
|
||||
<p class="text-white/45 mt-2">完全兼容 OpenAI 接口规范 — 改个 <code class="text-white/70">base_url</code> 和 <code class="text-white/70">api_key</code> 即可直接调用。图像 / 视频 / 图生图全支持。</p>
|
||||
<p class="text-white/45 mt-2">Vivid 协议 — 创建任务后连接保持到任务完成(NDJSON 流),一次性返回终态结果,无需轮询。图像 / 视频 / 参考素材全支持。</p>
|
||||
</header>
|
||||
|
||||
<!-- quickstart -->
|
||||
@@ -264,10 +200,10 @@ async function copy(text) {
|
||||
<h2 class="text-sm font-semibold text-white/80">基础信息</h2>
|
||||
<dl class="mt-4 space-y-3 text-sm">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<dt class="text-white/45">Base URL</dt><dd class="font-mono text-white/90">{{ base }}/v1</dd>
|
||||
<dt class="text-white/45">Base URL</dt><dd class="font-mono text-white/90">{{ base }}</dd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<dt class="text-white/45">鉴权</dt><dd class="font-mono text-white/90">Authorization: Bearer <key></dd>
|
||||
<dt class="text-white/45">鉴权</dt><dd class="font-mono text-white/90">Authorization: Bearer vk_…</dd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<dt class="text-white/45">你的 Key</dt><dd class="font-mono text-white/70">{{ keyHint }}</dd>
|
||||
@@ -279,13 +215,10 @@ async function copy(text) {
|
||||
<div class="card p-6">
|
||||
<h2 class="text-sm font-semibold text-white/80">端点</h2>
|
||||
<ul class="mt-4 space-y-2.5 text-sm font-mono">
|
||||
<li class="flex items-center gap-2"><span class="badge-get">GET</span><span class="text-white/80">/v1/models</span></li>
|
||||
<li class="flex items-center gap-2"><span class="badge-get">GET</span><span class="text-white/80">/v1/user/balance</span><span class="text-white/35 font-sans text-xs">查余额</span></li>
|
||||
<li class="flex items-center gap-2"><span class="badge-post">POST</span><span class="text-white/80">/v1/images/generations</span><span class="text-white/35 font-sans text-xs">文生图</span></li>
|
||||
<li class="flex items-center gap-2"><span class="badge-post">POST</span><span class="text-white/80">/v1/images/edits</span><span class="text-white/35 font-sans text-xs">图生图(multipart)</span></li>
|
||||
<li class="flex items-center gap-2"><span class="badge-post">POST</span><span class="text-white/80">/v1/videos</span><span class="text-white/35 font-sans text-xs">建视频任务</span></li>
|
||||
<li class="flex items-center gap-2"><span class="badge-get">GET</span><span class="text-white/80">/v1/videos/{id}</span><span class="text-white/35 font-sans text-xs">查状态</span></li>
|
||||
<li class="flex items-center gap-2"><span class="badge-get">GET</span><span class="text-white/80">/v1/videos/{id}/content</span><span class="text-white/35 font-sans text-xs">下载 mp4</span></li>
|
||||
<li class="flex items-center gap-2"><span class="badge-post">POST</span><span class="text-white/80">/v1/generate</span><span class="text-white/35 font-sans text-xs">创建任务(NDJSON 流)</span></li>
|
||||
<li class="flex items-center gap-2"><span class="badge-post">POST</span><span class="text-white/80">/v1/uploads</span><span class="text-white/35 font-sans text-xs">参考素材上传会话</span></li>
|
||||
<li class="flex items-center gap-2"><span class="badge-get">GET</span><span class="text-white/80">/v1/models</span><span class="text-white/35 font-sans text-xs">可售模型 + 能力规格</span></li>
|
||||
<li class="flex items-center gap-2"><span class="badge-get">GET</span><span class="text-white/80">/v1/balance</span><span class="text-white/35 font-sans text-xs">查余额</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
@@ -323,13 +256,11 @@ async function copy(text) {
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex flex-wrap items-center gap-1 text-[11px]">
|
||||
<template v-if="m.type === 'image'">
|
||||
<span v-for="r in (m.resolutions || [])" :key="r" class="cap cap-k">{{ r }}</span>
|
||||
<template v-for="r in (m.resolutions || [])" :key="r">
|
||||
<span class="cap cap-k">{{ r }}</span>
|
||||
</template>
|
||||
<template v-if="m.type === 'video'">
|
||||
<span v-for="r in (m.resolutions || [])" :key="r" class="cap cap-k">{{ r }}</span>
|
||||
<span v-if="(m.durations || []).length >= 2" class="cap cap-dur">{{ m.durations[0] }}–{{ m.durations[m.durations.length - 1] }}</span>
|
||||
<span v-else-if="(m.durations || []).length === 1" class="cap cap-dur">{{ m.durations[0] }}</span>
|
||||
<span v-if="durText(m)" class="cap cap-dur">{{ durText(m) }}</span>
|
||||
</template>
|
||||
<span v-if="m.reference_mode === 'frame' && m.max_reference_images > 0" class="cap cap-frame">首尾帧 {{ Math.min(m.max_reference_images, 2) }}</span>
|
||||
<span v-if="m.reference_mode === 'frame' && m.max_reference_images > 2" class="cap cap-ref">参考图 {{ refImages(m) }}</span>
|
||||
@@ -353,14 +284,14 @@ async function copy(text) {
|
||||
<!-- parameters -->
|
||||
<section class="grid lg:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold mb-3">文生图参数 <span class="text-xs font-normal text-white/40">/v1/images/generations</span></h2>
|
||||
<h2 class="text-lg font-semibold mb-3">生成参数 <span class="text-xs font-normal text-white/40">POST /v1/generate</span></h2>
|
||||
<div class="card overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
|
||||
<th class="px-4 py-2.5 font-medium">参数</th><th class="px-4 py-2.5 font-medium">类型</th><th class="px-4 py-2.5 font-medium">必填</th><th class="px-4 py-2.5 font-medium">说明</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="p in imageParams" :key="p[0]" class="border-b border-white/[0.04] last:border-0">
|
||||
<tr v-for="p in generateParams" :key="p[0]" class="border-b border-white/[0.04] last:border-0">
|
||||
<td class="px-4 py-2.5 font-mono text-white/85">{{ p[0] }}</td>
|
||||
<td class="px-4 py-2.5 text-white/50 font-mono text-xs">{{ p[1] }}</td>
|
||||
<td class="px-4 py-2.5 text-white/55">{{ p[2] }}</td>
|
||||
@@ -372,14 +303,14 @@ async function copy(text) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold mb-3">图生图参数 <span class="text-xs font-normal text-white/40">/v1/images/edits · multipart</span></h2>
|
||||
<h2 class="text-lg font-semibold mb-3">上传参数 <span class="text-xs font-normal text-white/40">POST /v1/uploads</span></h2>
|
||||
<div class="card overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
|
||||
<th class="px-4 py-2.5 font-medium">参数</th><th class="px-4 py-2.5 font-medium">类型</th><th class="px-4 py-2.5 font-medium">必填</th><th class="px-4 py-2.5 font-medium">说明</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="p in editParams" :key="p[0]" class="border-b border-white/[0.04] last:border-0">
|
||||
<tr v-for="p in uploadParams" :key="p[0]" class="border-b border-white/[0.04] last:border-0">
|
||||
<td class="px-4 py-2.5 font-mono text-white/85">{{ p[0] }}</td>
|
||||
<td class="px-4 py-2.5 text-white/50 font-mono text-xs">{{ p[1] }}</td>
|
||||
<td class="px-4 py-2.5 text-white/55">{{ p[2] }}</td>
|
||||
@@ -388,85 +319,13 @@ async function copy(text) {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="text-xs text-white/40 mt-3">
|
||||
用法:POST 拿 <code class="text-white/70">url</code> → PUT 直传 → 返回的 <code class="text-white/70">key</code> 放进
|
||||
<code class="text-white/70">/v1/generate</code> 的 <code class="text-white/70">refs</code>。
|
||||
预签名 URL 仅用于 PUT(不可 GET 读回),30 分钟内有效。
|
||||
支持:图 png/jpeg/webp,视频 mp4/webm/mov,音频 mp3/wav/m4a/ogg。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-2">
|
||||
<h2 class="text-lg font-semibold mb-3">视频参数 <span class="text-xs font-normal text-white/40">/v1/videos · 异步</span></h2>
|
||||
<div class="card overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
|
||||
<th class="px-4 py-2.5 font-medium">参数</th><th class="px-4 py-2.5 font-medium">类型</th><th class="px-4 py-2.5 font-medium">必填</th><th class="px-4 py-2.5 font-medium">说明</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="p in videoParams" :key="p[0]" class="border-b border-white/[0.04] last:border-0">
|
||||
<td class="px-4 py-2.5 font-mono text-white/85">{{ p[0] }}</td>
|
||||
<td class="px-4 py-2.5 text-white/50 font-mono text-xs">{{ p[1] }}</td>
|
||||
<td class="px-4 py-2.5 text-white/55">{{ p[2] }}</td>
|
||||
<td class="px-4 py-2.5 text-white/60 text-xs">{{ p[3] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- size 对照表(课时表)—— 解决"传错分辨率" -->
|
||||
<section>
|
||||
<h2 class="text-lg font-semibold mb-1">图像分辨率对照表 · <code class="text-white/70 text-sm">size</code> 该传什么</h2>
|
||||
<p class="text-xs text-white/45 mb-3">
|
||||
左边选比例,上面选分辨率档,交叉格里就是 <code class="text-white/70">size</code> 要传的值(直接复制)。
|
||||
没有 <code class="text-white/70">quality</code> 参数,图像分辨率只看 <code class="text-white/70">size</code> 的<strong class="text-white/70">长边</strong>。
|
||||
档位必须是该模型支持的(见上方「可用模型」的分辨率列),不支持会自动回退到该模型最低档。
|
||||
</p>
|
||||
<div class="card overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
|
||||
<th class="px-4 py-2.5 font-medium">比例</th>
|
||||
<th class="px-4 py-2.5 font-medium">1K</th>
|
||||
<th class="px-4 py-2.5 font-medium">2K</th>
|
||||
<th class="px-4 py-2.5 font-medium">4K</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="row in sizeTable" :key="row.ratio" class="border-b border-white/[0.04] last:border-0">
|
||||
<td class="px-4 py-2.5 text-white/75">{{ row.ratio }}</td>
|
||||
<td class="px-4 py-2.5 font-mono text-white/85">{{ row.k1 }}</td>
|
||||
<td class="px-4 py-2.5 font-mono text-white/85">{{ row.k2 }}</td>
|
||||
<td class="px-4 py-2.5 font-mono text-white/85">{{ row.k4 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="text-xs text-white/40 mt-2">
|
||||
例:想要 <strong class="text-white/70">2K 的 16:9 横图</strong> → <code class="text-white/70">"size": "2048x1152"</code>。
|
||||
留空 size = 默认 <strong class="text-white/70">1:1 · 2K</strong>。
|
||||
</p>
|
||||
|
||||
<!-- 视频分辨率(720p / 1080p,按短边判) -->
|
||||
<h2 class="text-lg font-semibold mb-1 mt-8">视频分辨率对照表 · <code class="text-white/70 text-sm">size</code> 该传什么</h2>
|
||||
<p class="text-xs text-white/45 mb-3">
|
||||
视频用 <code class="text-white/70">720p</code> / <code class="text-white/70">1080p</code> 两档,只看 <code class="text-white/70">size</code> 的<strong class="text-white/70">短边</strong>(短边 ≥1080 = 1080p,否则 720p)。
|
||||
档位必须是该视频模型支持的(如 grok-video 仅 720p),不支持会被拒。
|
||||
</p>
|
||||
<div class="card overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
|
||||
<th class="px-4 py-2.5 font-medium">比例</th>
|
||||
<th class="px-4 py-2.5 font-medium">720p</th>
|
||||
<th class="px-4 py-2.5 font-medium">1080p</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="row in videoSizeTable" :key="row.ratio" class="border-b border-white/[0.04] last:border-0">
|
||||
<td class="px-4 py-2.5 text-white/75">{{ row.ratio }}</td>
|
||||
<td class="px-4 py-2.5 font-mono text-white/85">{{ row.p720 }}</td>
|
||||
<td class="px-4 py-2.5 font-mono text-white/85">{{ row.p1080 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="text-xs text-white/40 mt-2">
|
||||
例:想要 <strong class="text-white/70">720p 的 16:9 横版视频</strong> → <code class="text-white/70">"size": "1280x720"</code>;
|
||||
竖版 9:16 → <code class="text-white/70">"720x1280"</code>。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- examples -->
|
||||
@@ -488,23 +347,19 @@ async function copy(text) {
|
||||
<section>
|
||||
<h2 class="text-lg font-semibold mb-3">响应 & 计费</h2>
|
||||
<div class="card p-6 space-y-3 text-sm text-white/70">
|
||||
<p><strong class="text-white/90">图像</strong>(generations / edits)返回 OpenAI 图片格式:<code class="text-white/85 font-mono">{{ '{ "created": ..., "data": [{ "url": "..." }] }' }}</code> —— <code class="text-white/85 font-mono">data[0].url</code> 是产物 URL,服务端不留存(<strong class="text-white/90">不返回 base64</strong>)。多数模型返回上游<strong class="text-white/90">原始直链</strong>;少数上游需鉴权的(如 gpt-image)会返回一个本站转发链 <code class="text-white/85 font-mono">/v1/images/{id}/content</code>,由服务端带账号凭据取回。<strong class="text-white/90">两种链接都会过期</strong>,请<strong class="text-white/90">尽快下载或转存到你自己的存储</strong>。</p>
|
||||
<p><strong class="text-white/90">视频</strong>(异步,Sora 风格三步):</p>
|
||||
<ol class="list-decimal list-inside space-y-1 text-white/65 pl-1">
|
||||
<li><code class="text-white/85 font-mono">POST /v1/videos</code> 立即返回任务对象 <code class="text-white/85 font-mono">{{ '{ "id": "...", "object": "video", "status": "queued", ... }' }}</code></li>
|
||||
<li>轮询 <code class="text-white/85 font-mono">GET /v1/videos/{id}</code>,<code class="text-white/70">status</code> 从 <code class="text-white/70">queued → in_progress → completed</code>(或 <code class="text-white/70">failed</code>)</li>
|
||||
<li>完成后 <code class="text-white/85 font-mono">GET /v1/videos/{id}/content</code> 返回 <strong class="text-white/90">mp4 原始二进制</strong>(非 base64、非 URL)</li>
|
||||
</ol>
|
||||
<p><strong class="text-white/90">计费(预扣)</strong>:生成<strong class="text-white/90">前</strong>按上表价格从你的 Key 账号预扣积分;图像或视频上游失败会自动退回 —— 失败不扣费。</p>
|
||||
<p><strong class="text-white/90">参数映射</strong>:<code class="text-white/70">size</code>(宽x高)同时决定<strong class="text-white/90">比例 + 分辨率档</strong>(长边:<1800→1K · 1800–3499→2K · ≥3500→4K),<code class="text-white/70">seconds</code>→视频时长。<strong class="text-white/90">没有 quality 参数</strong>,分辨率只看 size。档位须是该模型支持的(不支持会回退到该模型最低档);参数须落在定价表内否则 400,余额不足 402。</p>
|
||||
<p><strong class="text-white/90">NDJSON 流</strong>(<code class="text-white/85 font-mono">application/x-ndjson</code>):连接建立即回执首行
|
||||
<code class="text-white/85 font-mono">{{ '{"jobId":"…","status":"running"}' }}</code>;
|
||||
等待期间每 25 秒一个空行心跳(请忽略);末行为终态
|
||||
<code class="text-white/85 font-mono">succeeded</code> / <code class="text-white/85 font-mono">failed</code> —— 读最后一行 JSON 即结果。</p>
|
||||
<p><strong class="text-white/90">断流重连</strong>:流意外断开时,只带 <code class="text-white/85 font-mono">jobId</code> 重发同一请求即可重连续等 —— 不会重复创建任务、不重复扣费;已完成直接回终态帧。</p>
|
||||
<p><strong class="text-white/90">产物 URL</strong>:<code class="text-white/85 font-mono">result</code> 是只读预签名 URL(30 分钟),过期即焚,请及时下载转存。任务类型即文件类型。</p>
|
||||
<p><strong class="text-white/90">计费(预扣)</strong>:生成<strong class="text-white/90">前</strong>按上表价格从你的 Key 账号预扣积分;终态 <code class="text-white/85 font-mono">failed</code> 时 <code class="text-white/85 font-mono">refunded:true</code>,预扣积分自动退回 —— 失败不扣费。</p>
|
||||
<div class="pt-2 grid sm:grid-cols-2 gap-2 text-xs">
|
||||
<div class="flex items-center gap-2"><span class="badge-err">401</span> Key 无效 / 上游需重新授权</div>
|
||||
<div class="flex items-center gap-2"><span class="badge-err">404</span> 未知 model / 视频任务不存在</div>
|
||||
<div class="flex items-center gap-2"><span class="badge-err">400</span> 参数缺失 / 不支持或未定价</div>
|
||||
<div class="flex items-center gap-2"><span class="badge-err">402</span> 积分不足</div>
|
||||
<div class="flex items-center gap-2"><span class="badge-err">409</span> 视频尚未完成(content 未就绪)</div>
|
||||
<div class="flex items-center gap-2"><span class="badge-err">429</span> 账号并发已满,请重试</div>
|
||||
<div class="flex items-center gap-2"><span class="badge-err">503</span> 上游繁忙,请重试</div>
|
||||
<div class="flex items-center gap-2"><span class="badge-err">4001</span> 参数/业务拒绝:模型不可用、quality 不在白名单、refs 超限等</div>
|
||||
<div class="flex items-center gap-2"><span class="badge-err">4002</span> 积分不足,请先充值</div>
|
||||
<div class="flex items-center gap-2"><span class="badge-err">4011</span> API Key 无效或未授权</div>
|
||||
<div class="flex items-center gap-2"><span class="badge-err">4293</span> 并发任务数已达上限 / 上游繁忙,请重试</div>
|
||||
<div class="flex items-center gap-2"><span class="badge-err">5001</span> 服务异常,请稍后重试</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user