หน้าแรก/ข้อผิดพลาด
ข้อผิดพลาดและขีดจำกัดอัตรา: คู่มือปฏิบัติสำหรับ Uncensored API
โค้ดการผลิตใช้เวลาในเส้นทางล้มเหลวมากกว่าเส้นทางสำเร็จ คู่มือนี้ระบุสถานะโค้ดที่ Uncensored API ส่งกลับ บอกว่าตัวใดควรลองใหม่ และให้ throttling ฝั่งไคลเอนต์สำหรับขีดจำกัด 300 คำขอต่อนาทีใน Python asyncio และ Node รวมถึงวิธีสงบในการแสดง 402 ให้ผู้ใช้ของคุณ
อัปเดต
ประเด็นสำคัญ
- ลองใหม่เฉพาะ 429, 503 และความล้มเหลวด้านการขนส่ง; แก้ไข 400, 401, 403, 404 และ 402 ที่สาเหตุ
- ข้อผิดพลาดส่งเป็น JSON พร้อม object error ที่มี code และ message ดังนั้นแยกกรณีตามสถานะก่อน แล้วจึงตามด้วย code
- token bucket ที่เติม 5 token ต่อวินาทีทำให้คีย์หนึ่งอยู่ใต้ 300 คำขอต่อนาทีโดยไม่มีการตอบกลับจากฝั่งเซิร์ฟเวอร์
- 402 หมายถึงยอดเงินแบบเติมเงินหมดหรือเครดิตทดลองใช้หมด; จัดการเป็นสถานะผลิตภัณฑ์ ไม่ใช่การล้มเหลว
ซองข้อผิดพลาด
ทุกความล้มเหลวส่ง body JSON พร้อม object error ระดับบนเดียว มีสองฟิลด์ที่สำคัญ: code สตริงสำหรับเครื่องสั้นๆ และ message ข้อความสำหรับบันทึกและมนุษย์
{
"error": {
"code": "no_credit",
"message": "Balance used up or trial expired."
}
}ตัดสินใจตามลำดับนี้ ดู HTTP status ก่อนเพราะมันบอกประเภทปัญหา ใช้ code เพื่อแยกกรณีที่มีสถานะเดียวกัน และอย่า parse message สำหรับ logic เพราะคำพูดอาจเปลี่ยน ดูรูปแบบของ response สำเร็จที่ API reference
รหัสสถานะ สาเหตุและการดำเนินการ
| สถานะ | รหัส | สาเหตุทั่วไป | ลองใหม่? | การดำเนินการ |
|---|---|---|---|---|
| 400 | คำขอไม่ถูกต้อง | JSON ผิดรูปแบบ หรือโทเคนพรอมต์รวมกับ max_tokens เกิน 100,000 | ไม่ | แก้ไข payload; ตัดประวัติหรือลด max_tokens |
| 401 | คีย์ไม่ถูกต้อง | ขาดเฮดเดอร์, พิมพ์ผิด หรือคีย์ที่ถูกสร้างใหม่ | ไม่ | โหลดคีย์จากที่เก็บรหัสผ่านของคุณ |
| 402 | no_credit | ยอดเงินหมดหรือเครดิตทดลองใช้หมดอายุ | ไม่ | เติมยอดเงินแบบเติมเงินล่วงหน้า จากนั้นดำเนินการต่อ |
| 403 | content_blocked | คำขอแตะเนื้อหาทางเพศที่มีบุคคลที่ยังไม่บรรลุนิติภาวะ | ไม่ | อย่าส่งซ้ำ; แสดงข้อความนโยบายของคุณเอง |
| 404 | ไม่พบ | เส้นทางผิด เช่น ขาด /v1 | ไม่ | ตรวจสอบ Base URL |
| 429 | ขีดจำกัดอัตรา | มากกว่า 300 คำขอในหนึ่งนาทีบนคีย์เดียว | ใช่ | Backoff พร้อม jitter; เพิ่ม throttling ด้านไคลเอนต์ |
| 503 | upstream_busy | แรงกดดันความจุชั่วคราว | ใช่ | ลองใหม่หลังจากผ่านไปไม่กี่วินาที |
403 deserves emphasis: เป็นการตัดสินใจนโยบาย ไม่ใช่ข้อผิดพลาดชั่วคราว การลองซ้ำคำขอเดิมเสีย quota และจะตอบกลับคำตอบเดิม กรณี 400 ที่ทำให้คนประหลาดใจที่สุดคือผลรวมบริบท พรอมต์ 60,000 token พร้อม max_tokens 8,000 ล้น 4,000 และล้มเหลวแม้ว่าแต่ละตัวเลขจะถูกต้องเมื่อใช้เดี่ยว
นโยบายการลองใหม่ที่ทำงานได้อย่างถูกต้อง
ใช้นโยบายขนาดเล็กที่มีขอบเขตจำกัด และใช้เฉพาะเมื่อการส่งซ้ำจะช่วยได้
- ทำรายการความล้มเหลวที่ส่งซ้ำได้: 429, 503, การรีเซ็ตการเชื่อมต่อ และไทม์เอาต์ สิ่งอื่นใดให้แจ้งทันที
- Exponential backoff with jitter: รอ 1, 2, 4, 8 วินาที บวกเศษวินาทีสุ่ม, จำกัดสูงสุดที่ 30 วินาที Jitter ป้องกัน worker หลายตัวลองใหม่พร้อมกัน
- จำกัดจำนวนครั้งที่ส่งซ้ำไว้ที่ห้าครั้ง หากเกินกว่านั้น โอกาสที่ระบบจะล่มมีมากกว่าโอกาสที่โชคร้าย และงานที่รอคิวควรล้มเหลวอย่างชัดเจน
- ปิดการลองซ้ำอัตโนมัติของ SDK (
max_retries=0) เมื่อคุณรันลูปของคุณ มิฉะนั้นจำนวนการลองจะคูณและตัวควบคุมการจำกัดความเร็วของคุณจะนับจำนวนผิด - ลองใหม่สตรีมตั้งแต่ต้น สตรีมที่ล้มเหลวกลางทางไม่สามารถทำต่อได้; ทิ้งข้อความบางส่วนและส่งคำขอใหม่ หรือเก็บไว้และขอการต่อ
สำหรับ 503 คำแนะนำคือรอไม่กี่วินาที ดังนั้นเริ่มการหน่วงครั้งแรกที่ประมาณหนึ่งหรือสองวินาทีแทนที่จะตีทันที สำหรับ 429 การแก้คือลดความเร็ว ซึ่งส่วนถัดไปทำให้เป็นอัตโนมัติ
การอยู่ภายใต้ 300 คำขอต่อนาที
ขีดจำกัดคือ 300 คำขอต่อนาทีต่อคีย์ ซึ่งคือห้าต่อวินาทีโดยเฉลี่ย token bucket บังคับตามนั้น: มันเก็บ token ได้สูงสุดเป็น burst เล็กๆ เติมที่ห้าต่อวินาที และทำให้แต่ละคำขอใช้หนึ่ง token หาก bucket ว่าง ผู้เรียกจะ sleep จนกว่า token จะมี
เวอร์ชัน Python ใช้ asyncio ภารกิจทั้งยี่สิบใช้ถังและล็อคร่วมกัน ดังนั้นโคRoutine ที่ทำงานพร้อมกันจึงไม่สามารถเบิกโทเคนเกินจำนวนได้ หมายเหตุว่าถังนับทุกการพยายาม รวมถึงการส่งซ้ำ
import asyncio, os, random, time
from openai import AsyncOpenAI, APIStatusError, APIConnectionError
class TokenBucket:
"""Allows `rate` acquisitions per `per` seconds, with a small burst."""
def __init__(self, rate=300, per=60.0, burst=10):
self.fill = rate / per # tokens added per second (5.0)
self.cap = burst
self.tokens = burst
self.stamp = time.monotonic()
self.lock = asyncio.Lock()
async def acquire(self):
async with self.lock:
while True:
now = time.monotonic()
self.tokens = min(self.cap, self.tokens + (now - self.stamp) * self.fill)
self.stamp = now
if self.tokens >= 1:
self.tokens -= 1
return
await asyncio.sleep((1 - self.tokens) / self.fill)
client = AsyncOpenAI(base_url="https://api.uncensoredapis.com/v1",
api_key=os.environ["API_KEY"], max_retries=0)
bucket = TokenBucket()
RETRYABLE = {429, 503}
async def ask(prompt, attempts=5):
for n in range(attempts):
await bucket.acquire()
try:
r = await client.chat.completions.create(
model="uncensored",
messages=[{"role": "user", "content": prompt}],
max_tokens=200,
)
return r.choices[0].message.content
except APIStatusError as e:
if e.status_code not in RETRYABLE or n == attempts - 1:
raise
except APIConnectionError:
if n == attempts - 1:
raise
await asyncio.sleep(min(30, 2 ** n) + random.uniform(0, 1))
async def main():
prompts = [f"Give one-line fact number {i} about tides." for i in range(1, 21)]
results = await asyncio.gather(*(ask(p) for p in prompts))
print(len(results), "answers; first:", results[0])
asyncio.run(main())รันด้วย API_KEY ที่ export แล้ว คำขอยี่สิบคำอยู่ใน burst บวกกับการเติมเล็กน้อย ดังนั้นคุณจะเห็นว่าเสร็จเร็ว; เพิ่มจำนวนเป็น 600 และการรันจะยืดออกไปประมาณสองนาที ซึ่งเป็นขีดจำกัดที่ทำหน้าที่ของมัน
เวอร์ชัน Node ใช้แพ็กเกจทางการในโมดูล ES โดยเชื่อมการรับโทเคนผ่านคิว promise เพื่อให้ผู้เรียกได้รับบริการตามลำดับ
import OpenAI from "openai";
class TokenBucket {
constructor(rate = 300, perMs = 60000, burst = 10) {
this.fillPerMs = rate / perMs;
this.cap = burst;
this.tokens = burst;
this.stamp = Date.now();
this.queue = Promise.resolve();
}
acquire() {
// Serialize callers so the bucket math never races.
this.queue = this.queue.then(async () => {
for (;;) {
const now = Date.now();
this.tokens = Math.min(this.cap, this.tokens + (now - this.stamp) * this.fillPerMs);
this.stamp = now;
if (this.tokens >= 1) { this.tokens -= 1; return; }
await new Promise((r) => setTimeout(r, Math.ceil((1 - this.tokens) / this.fillPerMs)));
}
});
return this.queue;
}
}
const client = new OpenAI({
baseURL: "https://api.uncensoredapis.com/v1",
apiKey: process.env.API_KEY,
maxRetries: 0,
});
const bucket = new TokenBucket();
async function ask(prompt, attempts = 5) {
for (let n = 0; n < attempts; n++) {
await bucket.acquire();
try {
const r = await client.chat.completions.create({
model: "uncensored",
messages: [{ role: "user", content: prompt }],
max_tokens: 200,
});
return r.choices[0].message.content;
} catch (err) {
const retryable = err.status === 429 || err.status === 503 || err.status === undefined;
if (!retryable || n === attempts - 1) throw err;
await new Promise((r) => setTimeout(r, Math.min(30000, 2 ** n * 1000) + Math.random() * 1000));
}
}
}
const prompts = Array.from({ length: 20 }, (_, i) => `Give one-line fact number ${i + 1} about tides.`);
const answers = await Promise.all(prompts.map((p) => ask(p)));
console.log(answers.length, "answers; first:", answers[0]);ปรับขนาด burst อย่างระมัดระวัง burst 10 อนุญาต spike สั้นขณะที่อัตราคงที่ยังคงอยู่ที่ห้าต่อวินาที หากกระบวนการหลายกระบวนการแบ่งปันคีย์เดียว แบ่งอัตราระหว่างพวกเขา เช่น 100 ต่อนาทีแต่ละคน across three workers หรือกำหนดเส้นทาง traffic ทั้งหมดผ่านคิวเดียว
การจัดการ 402 อย่างราบรื่นในผลิตภัณฑ์ของคุณ
402 พร้อมโค้ด no_credit หมายถึงยอดเงินแบบเติมเงินล่วงหน้าหมดหรือเครดิตทดลองใช้หมดอายุ เป็นข้อผิดพลาดเดียวที่ผู้ใช้ปลายทางของคุณไม่ควรเห็นเป็น stack trace และมักบ่งชี้ถึงสิ่งที่เฉพาะคุณเท่านั้นที่สามารถแก้ไขได้: การเติมเงิน
สร้างสามชั้น ก่อน detect มันส่วนกลางใน wrapper เดียวเพื่อให้ไม่มี call site ต้องจำ สอง translate มัน: บราวเซอร์หรือแอปมือถือรับสถานะที่เป็นกลางเช่น capacity_paused และแสดงหน้าจอที่เป็นมิตร ไม่ใช่ข้อความ upstream สาม alert ตัวเอง เพราะผู้ใช้ทุกคนได้รับผลกระทบพร้อมกัน; ส่งการแจ้งเตือนไปยังเจ้าของบัญชีและสลับ feature flag เพื่อให้ UI หยุดส่งคำขอ
# server side: translate the upstream 402 into a product state
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from openai import AsyncOpenAI, APIStatusError
import os
app = FastAPI()
client = AsyncOpenAI(base_url="https://api.uncensoredapis.com/v1", api_key=os.environ["API_KEY"])
@app.post("/api/reply")
async def reply(body: dict):
try:
r = await client.chat.completions.create(
model="uncensored", messages=body["messages"], max_tokens=400)
return {"text": r.choices[0].message.content}
except APIStatusError as e:
if e.status_code == 402:
# Tell the client app to show a "temporarily unavailable" screen,
# and page yourself to top up the prepaid balance.
return JSONResponse({"state": "capacity_paused"}, status_code=503)
if e.status_code == 429 or e.status_code == 503:
return JSONResponse({"state": "busy_retry"}, status_code=503)
raiseเพิ่มการป้องกันเชิงรุกด้วย รวม usage.total_tokens ต่อคำขอใน ledger ของคุณและเตือนที่ threshold ที่คุณเลือก เพื่อให้การเติมเงินเกิดขึ้นก่อนที่ผู้ใช้จะถูกบล็อก จำไว้ว่าเครดิตทดลองใช้คือ $0.50 และ lasts 7 days ดังนั้นโปรโตไทป์สามารถแตะ 402 จาก expiry เพียงอย่างเดียวด้วยเงินที่ยังไม่ได้ใช้ อัตราสำหรับการวางแผนอยู่ที่ pricing page
เมื่อยอดเงินเป็นบวกอีกครั้ง ให้เล่นงานที่คุณเก็บไว้ขณะหยุดทำงานชั่วคราว wrapper ของคุณควรล้างสถานะหยุดทำงานชั่วคราวเมื่อสำเร็จครั้งแรก
สิ่งที่ควรบันทึกและแจ้งเตือน
บันทึกที่ดีทำให้เหตุการณ์สั้นลง บันทึกต่อคำขอ: รหัสคำขอของคุณเอง, id การสร้างผลลัพธ์ id เมื่อมี, สถานะ HTTP, รหัสข้อผิดพลาด code, จำนวนครั้งที่ลอง และการใช้โทเคน อย่าบันทึกส่วนหัว Authorization หรือพรอมต์เต็มที่มีข้อมูลส่วนบุคคล
การแจ้งเตือนที่มีประโยชน์ ตามลำดับความสำคัญ: 402 ใดๆ (ทันที), a sustained run of 401s (คีย์ที่ถูกสร้างใหม่ไม่ได้ถูกปรับใช้ทุกที่), 429 rate สูงกว่าไม่กี่เปอร์เซ็นต์ของคำขอ (ตัวควบคุมของคุณมีขนาดผิด) และกลุ่ม 503 (ถอยห่างกว้างขึ้น พิจารณา queue) แดชบอร์ดของจำนวนสถานะต่อนาทีทำให้ทั้งสี่มองเห็นได้ทันที จัดการได้ดี รหัสเหล่านี้กลายเป็นสัญญาณปกติแทนที่จะเป็น downtime
การทดสอบเส้นทางความล้มเหลวของคุณก่อนเปิดตัว
การจัดการข้อผิดพลาดที่ไม่เคยทำงานคือการคาดเดา คุณสามารถทดสอบส่วนใหญ่ได้โดยไม่ต้องรอการหยุดทำงานจริง โดยใช้คำขอราคาถูกและตัวจำลองของคุณเอง
- 401: เรียก
GET /v1/modelsด้วยคีย์ที่ผิดโดยเจตนาและยืนยันว่าแอปของคุณแสดงข้อผิดพลาดการกำหนดค่าให้กับผู้ดำเนินการ ไม่ใช่ลูกค้า - 404: ส่งคำขอ
/v1/chat/completion(เอกพจน์) ครั้งเดียวและยืนยันว่าบรรทัดโลกระบุเส้นทาง ดังนั้นคำผิดใน base URL จะชัดเจนภายในไม่กี่นาที - 400: ส่ง
max_tokens32,000 พร้อมพรอมต์ที่เติมจนมีขนาดประมาณ 50,000 โทเคน ผลรวมเกิน 100,000 และคุณสามารถดูเส้นทางตรวจสอบการทำงาน - 429 และ 503: คุณไม่สามารถเรียกใช้พวกมันตามความต้องการได้ ดังนั้นสร้าง stub สำหรับพวกมัน ห่อ HTTP client ของคุณใน fake ที่ raise status สำหรับการเรียกสองครั้งแรก จากนั้นสำเร็จ และ assert ว่าลูปของคุณ sleep, retry และส่งคำตอบสุดท้าย
- 402: สร้าง stub สำหรับมันเช่นกัน และยืนยันว่า UI แสดงหน้าจอ paused, การแจ้งเตือนเกิดขึ้นและไม่มีอะไร retry ในลูปที่แน่น
เขียนสิ่งเหล่านี้เป็นการทดสอบอัตโนมัติที่รันใน continuous integration การทดสอบแบบตารางที่มีแถวเดียวต่อสถานะจากตารางข้างต้นใช้เวลาไม่กี่นาทีในการเขียนและปกป้องนโยบายจากการปรับโครงสร้างที่มีเจตนาดี สุดท้าย รัน load test สั้นๆ กับตัวควบคุมของคุณเองด้วย API stub: ยิง 1,000 acquisitions และ assert ว่าไม่มีหน้าต่าง 60 วินาทีที่มี grants มากกว่า 300 บวกกับ burst นั่นพิสูจน์คณิตศาสตร์ก่อนที่ traffic จริงจะขึ้นอยู่กับมัน เมื่อคุณอ่าน agent loop guide ในภายหลัง ให้ใช้ wrapper เดียวกันที่นั่น เนื่องจาก agents คูณจำนวนคำขอและแตะขีดจำกัดเหล่านี้ก่อน
ถาม-ตอบ
429 นับรวมในยอดเงินของฉันหรือไม่?
ตรวจสอบตัวเลขการใช้งานในบัญชีแยกประเภทของคุณเองแทนที่จะสันนิษฐาน ไม่ว่ากรณีใด วิธีแก้ไขคือการลดความเร็วและส่งซ้ำพร้อมการเพิ่มระยะเวลาคืนกลับ
ฉันควรส่งซ้ำการตอบสนอง 403 content_blocked หรือไม่?
ไม่ มันเป็นบล็อกนโยบายและคำขอเดียวกันจะส่งผลลัพธ์เดียวกัน แสดงข้อความของคุณเองและดำเนินการต่อ
คีย์ของฉันคืนค่า 401 ทันที มีการเปลี่ยนแปลงอะไร?
บ่อยที่สุดคีย์ถูกสร้างใหม่ ซึ่งทำให้คีย์เก่าไม่ถูกต้องทันที คopy คีย์ใหม่จากบัญชีของคุณและปรับใช้ใหม่
worker จำนวนกี่ตัวสามารถแบ่งปันคีย์เดียวได้
ทั้งหมดแบ่งปันขีดจำกัดอัตราคำขอ 300 คำขอต่อนาที แบ่งอัตราการใช้งานระหว่าง worker หรือส่งคำขอผ่านคิวเดียว
คีย์ของคุณอยู่ห่างแค่แบบฟอร์มเดียว
สร้างบัญชี คopy คีย์ เปลี่ยน base URL นั่นคือการตั้งค่าทั้งหมด