โควตา CLI หมดกลางงานแปล: อ่าน Resets in แล้วหลับรอจนรีเซ็ต
งานแปลที่ต้องรันหลายวันบนโควตารายบัญชีไม่ใช่เรื่องของการยิงให้เร็ว แต่เป็นเรื่องของการรู้ว่าโดนหยุดตอนไหน รอถึงเมื่อไร และกลับมาทำต่อโดยไม่ซ้ำไม่ตกหล่น
สรุปใน 1 นาที
- อาการ/ปัญหา
- งานแปลชุดใหญ่บน CLI ของ Google ชนโควตา แล้วโควตามาเป็น error result ไม่ใช่ HTTP 429
- ใครควรอ่าน
- คนที่รันงานแบตช์ยาวหลายวันบนโควตารายบัญชีด้วย CLI แบบ headless
- ทางแก้แบบสั้น
- แยก error เป็นสามกลุ่ม parse ข้อความ Resets in แล้วหลับรอถึงเวลารีเซ็ต เก็บสถานะงานฝั่งเซิร์ฟเวอร์ให้ต่อได้โดยไม่ซ้ำไม่ตกหล่น
พอดีงานแปลเว็บหลายล้านตัวอักษรของลูกค้ารายหนึ่งกิน Claude เยอะจนโควตารายสัปดาห์ของบัญชีที่ผมต้องเก็บไว้ให้แชทบอทตอบลูกค้าลดลงเร็วผิดปกติ ผมเลยสลับเครื่องยนต์ของงานแปลไปใช้ agy (Antigravity CLI) ที่รัน Gemini บนสมาชิก Google ของผมเอง ไม่ใช้ API key ไม่มีบิลเพิ่ม และเพื่อให้ตรงข้อเท็จจริง มันเป็น CLI ทางการที่ล็อกอินด้วยบัญชีของตัวเอง ไม่ใช่การเจาะ API ที่ไม่เป็นทางการ (ส่วน Gemini CLI รุ่นเดิมไม่รับล็อกอินส่วนตัวแล้ว) ตอนล็อกอินผมกดเองในหน้าต่างเบราว์เซอร์ ไม่ได้ให้ Claude กรอกรหัสแทน
เรื่องโควตาของมันต่างจากที่ผมคุ้น โควตาเป็นรายบัญชี ใช้ร่วมกันทุกโมเดลของบัญชีนั้น และตอนหมดมันไม่ได้ตอบ HTTP 429 ให้เห็นเหมือน API ทั่วไป มาเป็น error result ที่ซ่อนอยู่ในผลลัพธ์ของเทิร์น เรื่องที่ต้องเขียนรันเนอร์ให้ฉลาดกว่า "ยิงแล้วลองใหม่" เลยเกิดขึ้นจากตรงนี้
เรื่องคุณภาพผลแปลกับคิวที่ตันอยู่ใน [คิวแปลเว็บขึ้น nothing to send ทั้งที่เหลือเป็นพัน](/article/llm-translate-site-stale-hash-code-checks-failed-cache) ส่วนบทความนี้พูดถึงการรันยาวบนโควตาเท่านั้น ถ้าเป็นการหน่วง API ตามอัตราก็ดู [batch API throttle](/article/batch-api-throttle-resume-verify) ซึ่งเป็นปัญหาคนละแบบ
กับดัก headless สามข้อก่อนจะได้คำตอบกลับมา
ก่อนจะไปถึงเรื่องโควตา ผมเสียเวลากับการทำให้ agy ตอบแบบไม่มีคนนั่งหน้าจอไปพักใหญ่ ได้กับดักมาสามข้อที่ตรวจแล้วว่าจริง
- flag ทุกตัวต้องมาก่อน -p= ถ้าเขียน -p "text" ตามด้วย --flag มันเอา flag ถัดไปไปเป็นพรอมต์ ผลคือได้คำตอบแปลกๆ ที่ไม่เกี่ยวกับสิ่งที่ส่ง
- รันใน cwd ที่เป็นโฟลเดอร์ว่าง เพราะเอเจนต์จะพยายามอ่านไฟล์ในโฟลเดอร์ปัจจุบัน แต่โหมด headless ปฏิเสธการอ่านโดยอัตโนมัติ ผลคือคำตอบกลับมาว่างเปล่า
- stdin แบบ stream-json ต้องมีฟิลด์ event ไม่งั้นมันรายงานว่า stream input message is missing the event field
คำสั่งที่ใช้ได้จริงหน้าตาแบบนี้ ใช้ --mode plan เพื่อไม่ให้มันแก้ไฟล์ และบอกในพรอมต์ด้วยว่าไม่ต้องเรียก tool ผมรันเป็น process เดียวสำหรับฉบับอังกฤษ
mkdir -p agy_ws && cd agy_ws # โฟลเดอร์ว่างเท่านั้น
agy --input-format stream-json --output-format stream-json \
--model <MODEL> --mode plan -p= # -p= ต้องอยู่ท้ายสุด
และข้อความที่ส่งทาง stdin เป็น JSON หนึ่งบรรทัดต่อหนึ่งเทิร์น ต่างจากของ Claude ที่ผมเล่าไว้ใน บทความ stream-json ของ Claude ตรงที่ต้องมี "event"
{"event":"user","message":{"role":"user","content":[{"type":"text","text":"แปลเป็นอังกฤษ ห้ามใช้เครื่องมือ: ..."}]}}
ฝั่ง stdout ระหว่างทางจะมี step_update (พวก agent_response ที่ไหลมาเป็น text_delta) แล้วจบเทิร์นด้วย {"event":"result"} หนึ่งครั้ง พร้อม usage คือ input_tokens, cache_read_tokens และ output_tokens ผมโยนทิ้งทุกอย่างยกเว้น result เพราะมันคือตัวเดียวที่บอกว่าเทิร์นนี้สำเร็จหรือไม่
ข้อควรจำอีกอย่างคือ system prompt ส่งเฉพาะเทิร์นแรกของ session และ session จะถูกรีสตาร์ตเมื่อ context สะสมเกินค่า rotate_tokens เพราะ input ที่ส่งซ้ำทุกเทิร์นนับเข้าโควตาของ Google ด้วย ค่าเริ่มต้นตอนนี้ 20,000 โทเคน (ค่าเดิมในโค้ดผมคือ 60,000) ส่วนตัวผมเลยตั้งให้ session สั้นเข้าไว้เป็นหลัก

โควตามาเป็น error result ที่มีคำว่า Resets in
พอโควตาบัญชีหมด เทิร์นนั้นไม่ได้ล้มด้วยสถานะ HTTP แต่ result มีสถานะที่ไม่ใช่ SUCCESS หรือคำตอบว่างเปล่า ข้อความข้างในมีเวลาที่จะรีเซ็ตมาให้ด้วย หน้าตาประมาณ "Resets in 2h52m56s" ผมแยกชนิด error ออกเป็นสามอย่าง
ต้องล็อกอินใหม่
ข้อความบอกให้ sign in รันเนอร์หยุดทันทีแล้วแจ้งคน ห้ามหลับรอ เพราะรอเท่าไรก็ไม่หาย ตัวตรวจราคาถูกคือคำสั่ง agy models ถ้าขึ้น Please sign in to view available models แปลว่าล็อกเอาต์
โควตาหมด
ข้อความมีคำว่า quota, resource_exhausted หรือ limit และมี Resets in XhYmZs รันเนอร์ parse เวลา แล้วหลับถึงรีเซ็ตบวกเผื่อราว 3 นาที ไม่ยิงซ้ำระหว่างรอ
อย่างที่สามคือ RuntimeError สำหรับทุกอย่างที่เหลือ ซึ่งผมให้ล้มเสียงดังแล้วหยุด ไม่ลองใหม่เงียบๆ ตรงนี้ผมเรียนมาจากเรื่อง ทุกชั้นรายงานสำเร็จทั้งที่ผลเป็นขยะ เหมือนกัน
import json, re
RESET = re.compile(r'Resets in\s*(?:(\d+)h)?\s*(?:(\d+)m)?\s*(?:(\d+)s)?')
MARGIN = 180 # เผื่อ 3 นาทีหลังเวลารีเซ็ต อย่ายิงทันทีที่ถึง
class SignedOut(Exception): pass
class QuotaStop(Exception):
def __init__(self, wait): self.wait = wait
def seconds_until_reset(raw):
m = RESET.search(raw)
if not m or not any(m.groups()):
# อ่านเวลาไม่ออก: ให้หยุดแล้วแจ้งคน ดีกว่าเดาแล้วยิงรัว
raise RuntimeError('quota stop but no reset time: ' + raw[:200])
h, mi, s = (int(x or 0) for x in m.groups())
return h * 3600 + mi * 60 + s + MARGIN
def classify(result, answer):
if result.get('status') == 'SUCCESS' and answer.strip():
return answer
raw = json.dumps(result, ensure_ascii=False)
low = raw.lower()
if 'sign in' in low or 'log in' in low:
raise SignedOut(raw[:200])
if any(k in low for k in ('quota', 'resource_exhausted', 'limit')):
raise QuotaStop(seconds_until_reset(raw))
raise RuntimeError(raw[:200])
ผมค้นหาข้อความทั้งก้อนของ result ด้วย regex แทนที่จะเจาะฟิลด์เฉพาะ เพราะไม่อยากผูกกับตำแหน่งของข้อความ error ซึ่งเปลี่ยนตามรุ่นของเครื่องมือได้ง่าย
ทำไมหลับแล้วต่อได้เสมอ: สถานะอยู่ฝั่งเซิร์ฟเวอร์
รันเนอร์ของผมไม่เก็บสถานะงานไว้ในโปรเซสเลย ฝั่งเซิร์ฟเวอร์เป็นคนรู้ว่าฟิลด์ไหนขาด ไหน stale (API ส่งมาให้เฉพาะที่ขาด) ดังนั้นการต่อหลังหลับก็แค่ขอคิวใหม่ และถ้าโปรเซสตายกลางทางด้วยเหตุอะไรก็ตาม รันคำสั่งเดิมซ้ำได้เลย ไม่มีขั้น "กู้สถานะ" ให้ต้องทำ
อีกเรื่องที่เป็นกับดักของคนใช้ Windows คือ shell เบื้องหน้าตายเมื่อหมดเวลา งานหลับสามชั่วโมงในหน้าต่าง SSH จะไม่รอด ผมเลยรันแบบ detached ผ่านสคริปต์ที่ห่อคำสั่งเดิมไว้ เรื่องนั้นผมเล่าไว้แล้วใน รัน job แยก process ผ่าน SSH บน Windows ฝั่ง Linux ง่ายกว่านั้นมาก
#!/bin/bash
# run_phase.sh รันแยกจาก shell เบื้องหน้า จะตายตอน SSH หลุดก็ไม่เป็นไร
cd "$(dirname "$0")" || exit 1
mkdir -p logs
nohup setsid python3 translate.py --locale en --engine agy \
>> logs/en.log 2>&1 &
echo $! > logs/en.pid
ผมแปลตรงจากไทยเป็นอังกฤษ ไม่ผ่านภาษากลางหรือแปลซ้ำหลายทอด เพราะเพิ่มขั้นเท่ากับเพิ่มโอกาสเพี้ยนและเพิ่มโทเคน
เลือกโมเดลด้วยการวัด ไม่ใช่ตามชื่อ
ตอนแรกผมคิดว่ารุ่นใหญ่ย่อมดีกว่าเสมอ แต่พอวัดเทียบ ตัวเลขที่ผมวัดบนสมาชิกของผมเองคือ รุ่น Flash ระดับ Low ถึง Medium ใช้ output ราว 1.1 โทเคนต่อตัวอักษรไทย ส่วนรุ่น Pro ระดับ Low ใช้ราว 3.9 ซึ่งส่วนใหญ่เป็น thinking ที่ไม่ได้ออกมาเป็นข้อความ แปลว่าต่อโควตาเท่ากัน Flash ได้ข้อความมากกว่าราวสามเท่าครึ่ง สำหรับงานแปลที่ต้นฉบับชัดเจนอยู่แล้ว ผมเลยเลือก Flash
ตัวเลขชุดนี้วัดบนสมาชิกของผมเองและเป็นลำดับขนาด ตัวเลขฝั่ง Pro ผมจดมาจากบันทึกการวัดครั้งก่อน ไม่ได้อยู่ในล็อกของงานนี้ ส่วนชื่อรุ่น ณ ตุลาคม 2026 ค่าเริ่มต้นในโค้ดของผมคือ gemini-3.8-flash-medium ชื่อรุ่นเปลี่ยนเร็วมาก ใครจะวัดซ้ำก็วัดบนบัญชีของตัวเองกับงานจริงของตัวเอง
เทียบสามรูปแบบโควตา รู้ล่วงหน้าได้แค่ไหน
เรื่องที่ทำให้ผมเข้าใจอะไรชัดขึ้นคือ ตอนอยู่ฝั่ง Claude ผมมีตัวคุมโควตา (QuotaGuard) ที่อ่านระดับการใช้ของบัญชีได้ล่วงหน้า แต่ฝั่ง Gemini ผ่าน agy ไม่มีทางนั้น ผมรู้ขีดจำกัดต่อเมื่อชนแล้วเท่านั้น
ฝั่ง Claude ตัวคุมนี้อ่านค่า utilization รายสัปดาห์ (seven_day) กับค่า 5 ชั่วโมงจาก endpoint ใช้งานของบัญชี ก่อนเริ่มและทุก 5 เทิร์น แล้วหยุดที่เพดาน 90% เป็นค่าเริ่มต้น มีโหมด "endgame" ที่ยกเพดานเป็น 95% ในไม่กี่ชั่วโมงสุดท้ายก่อนรีเซ็ตรายสัปดาห์ เพราะของที่เหลือไม่ใช้ก็หายไปเปล่าๆ และมีสมุดบัญชีโทเคนต่อหน้าต่าง 5 ชั่วโมงเก็บไว้ด้วย ข้อสรุปของผมคือ ถ้าเลือกได้ ระบบที่อ่านระดับการใช้ล่วงหน้าได้นิ่งกว่าระบบที่ต้องชนก่อน แต่เมื่อไม่มีทางเลือก การหลับรอก็ใช้ได้ดี ตามใจชอบว่าจะรอหรือสลับเครื่องยนต์

ถ้าอินพุตไม่น่าเชื่อ ให้ห่อด้วย sandbox
งานแปลเว็บที่ผมทำ ต้นฉบับเป็นของลูกค้าเอง เลยเสี่ยงต่ำ แต่ถ้าอินพุตมาจากที่ที่เราไม่ควบคุม เช่นข้อความจากภายนอก ผมจะห่อ agy ด้วย bwrap ให้ HOME เป็น tmpfs ว่าง เว้นไว้เฉพาะโฟลเดอร์ล็อกอินของมันเอง และซ่อน socket ของฐานข้อมูลออก agent ที่โดนสั่งให้ทำอะไรแปลกๆ จะได้เห็นแค่กล่องเปล่า
มีข้อสังเกตอีกอย่างจากบันทึกการใช้งานของผมที่ยังไม่ได้ตรวจซ้ำกับโค้ดชุดนี้ คือ ถ้า import requests ก่อนเรียก truststore.inject_into_ssl() เคยได้ RecursionError บนเครือข่ายที่มี TLS inspection เป็นข้อสังเกตที่ ยังไม่ยืนยัน ใครเจอก็ลองสลับลำดับ import ดู
ทดสอบรันเนอร์โดยไม่ต้องรอโควตาหมดจริง
ผมไม่รอให้โควตาหมดจริงเพื่อทดสอบ ทำสองอย่างคือส่งข้อความสั้นเทิร์นเดียวเพื่อดูว่า result ตรงรูปแบบที่ parser คาดไว้ และจำลองข้อความ Resets in ใส่ฟังก์ชันคำนวณเวลา
# จำลองข้อความโควตาหมด แล้วดูว่าเวลาหลับที่คำนวณถูกไหม
fake = 'RESOURCE_EXHAUSTED: quota reached. Resets in 2h52m56s'
assert seconds_until_reset(fake) == 2*3600 + 52*60 + 56 + 180 # = 10556 วินาที
try:
classify({'status': 'ERROR', 'message': fake}, '')
except QuotaStop as e:
print('sleep', e.wait, 'seconds') # sleep 10556 seconds
ส่งเทิร์นสั้นแล้วได้ result ตรงรูปแบบ, จำลองข้อความ Resets in แล้วเวลาหลับถูก, agy models ตอบรายการโมเดลแปลว่ายังล็อกอินอยู่, รันแบบ detached แล้วปิดหน้าต่าง SSH ได้, และจำนวนฟิลด์ที่ขาดฝั่งเซิร์ฟเวอร์ลดลงจริงหลังรันไปสักพัก
สิ่งที่ผมจะทำเหมือนเดิมถ้าต้องย้ายเครื่องยนต์อีกครั้งมีสามข้อ ข้อแรก แยกชนิด error เป็นสามกลุ่มก่อนเขียนอย่างอื่น ข้อสอง หลับรอตามเวลาที่ระบบบอกแทนการยิงซ้ำ ข้อสาม เก็บสถานะงานไว้ฝั่งเซิร์ฟเวอร์เสมอ ให้การรีสตาร์ตกลายเป็นเรื่องธรรมดาที่ไม่ต้องกลัว ส่วนเรื่องที่ผมยังไม่ได้ลองคือรันสองงานคู่กันบนบัญชีเดียวว่าโควตาถูกแบ่งกันอย่างไร จดไว้เฉยๆ ยังไม่มีตัวเลข เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย