@@ -97,13 +97,17 @@ class Quota:
9797 fetched_at : str = ""
9898 error : str = ""
9999 windows : list [Window ] = field (default_factory = list )
100+ # 相手が言ったそのまま。**窓に直せたときも持つ** —— 画面に出るのは Chiezo が
101+ # 付けた名前と割合だけなので、元を当たれないと「この行は何か」に答えられない
102+ raw : str = ""
100103
101104 def as_dict (self ) -> dict :
102105 return {
103106 "supported" : self .supported ,
104107 "fetched_at" : self .fetched_at ,
105108 "error" : self .error ,
106109 "windows" : [w .as_dict () for w in self .windows ],
110+ "raw" : self .raw ,
107111 }
108112
109113
@@ -162,7 +166,7 @@ def _window_label(minutes: float | None, fallback: str) -> str:
162166# ---- 相手ごとの聞き方 -------------------------------------------------------
163167
164168
165- async def _elevenlabs (spec , credential : str ) -> list [Window ]:
169+ async def _elevenlabs (spec , credential : str ) -> tuple [ list [Window ], str ]:
166170 """ElevenLabs の枠。声・効果音・曲・絵・動画が同じ 1 つの残量を食う。
167171
168172 `GET /v1/user/subscription` は鍵だけで引ける —— 生成も会話もしないので、
@@ -198,15 +202,15 @@ async def _elevenlabs(spec, credential: str) -> list[Window]:
198202 used = used ,
199203 limit = limit ,
200204 unit = "クレジット" ,
201- )]
205+ )], res . text [: RAW_MAX ]
202206
203207
204208def _amount (value ) -> float | None :
205209 """数として読む。読めない値は None(0 と混ぜない)。"""
206210 return float (value ) if isinstance (value , int | float ) and not isinstance (value , bool ) else None
207211
208212
209- async def _openrouter (spec : providers .Provider , credential : str ) -> list [Window ]:
213+ async def _openrouter (spec : providers .Provider , credential : str ) -> tuple [ list [Window ], str ]:
210214 """OpenRouter のクレジット。使った額と、上限があれば残高。
211215
212216 上限が無い鍵もある(従量課金)ので、`limit` が null のときは使用額だけ出す ——
@@ -243,10 +247,10 @@ async def _openrouter(spec: providers.Provider, credential: str) -> list[Window]
243247 limit = limit_f ,
244248 unit = "USD" ,
245249 )
246- ]
250+ ], res . text [: RAW_MAX ]
247251
248252
249- async def _bridge (spec : providers .Provider ) -> list [Window ]:
253+ async def _bridge (spec : providers .Provider ) -> tuple [ list [Window ], str ]:
250254 """CLI ブリッジに聞く(ブリッジが CLI に聞く)。
251255
252256 ブリッジが立っていなければ取れない。 枠は CLI の中にしか無いので、
@@ -291,14 +295,21 @@ async def _bridge(spec: providers.Provider) -> list[Window]:
291295 raise UsageError (
292296 str (body .get ("reason" ) or "CLI が使用量を返しませんでした" )[:REASON_MAX ]
293297 )
294- return windows
298+ return windows , str ( body . get ( "raw" ) or "" )[: RAW_MAX ]
295299
296300
297301# 失敗の理由として持ち帰る長さ。**300 では足りなかった** —— 相手は人向けの報告を
298302# 返してくるので、頭で切ると「なぜ駄目だったか」が枠の外へ落ちる(実際にそうなった)。
299303# 絵と音の相手のエラー(`app/media_backends.py` の `remote_error`)と同じ長さにしてある。
300304REASON_MAX = 600
301305
306+ # 相手が言ったそのままを控える長さの上限。**読めたときにも控える** ——
307+ # 正規化した後の画面には Chiezo が付けた名前しか出ないので、元を当たれないと
308+ # 「この行は何なのか」に答えられない(実測: codex が同じ名前の窓を 2 つ返し、
309+ # 片方が何の制限なのか画面からは分からなかった)。
310+ # **整形も翻訳もしない** —— そのまま検索できることに値打ちがある。
311+ RAW_MAX = 4000
312+
302313
303314def _bridge_error (body : dict , status : int ) -> str :
304315 """ブリッジが返した理由を取り出す。
@@ -313,8 +324,13 @@ def _bridge_error(body: dict, status: int) -> str:
313324 return (str (reason ) if reason else f"HTTP { status } " )[:REASON_MAX ]
314325
315326
316- async def fetch (spec : providers .Provider ) -> list [Window ]:
317- """その相手の枠を取りに行く。取れなければ `UsageError`。"""
327+ async def fetch (spec : providers .Provider ) -> tuple [list [Window ], str ]:
328+ """その相手の枠と、**相手が言ったそのまま**を取りに行く。取れなければ `UsageError`。
329+
330+ 生のほうを一緒に持ち帰るのは、正規化で落ちるものがあるから —— 画面に出るのは
331+ Chiezo が付けた名前と割合だけで、**元の資料に当たる道が無い**と「この行は何か」に
332+ 答えられない(古い版は捨てていて、実際に答えられなかった)。
333+ """
318334 if not spec .usage :
319335 raise UsageError ("この相手は使用量を出しません" )
320336 credential = settings_store .load (spec .id ).credential
@@ -343,7 +359,7 @@ async def refresh(provider_id: str) -> Quota:
343359 if spec is None or not spec .usage :
344360 return Quota (supported = False )
345361 try :
346- windows = await fetch (spec )
362+ windows , raw = await fetch (spec )
347363 except UsageError as e :
348364 usage_store .save_quota (spec .id , [], str (e ))
349365 stored = usage_store .load_quota ().get (spec .id , {})
@@ -352,10 +368,13 @@ async def refresh(provider_id: str) -> Quota:
352368 fetched_at = stored .get ("fetched_at" , "" ),
353369 error = str (e ),
354370 windows = _windows_from (stored .get ("windows" , [])),
371+ # **失敗しても前の生の返事は残す**(窓と同じ扱い)—— 一時的に繋がらない
372+ # だけのことがあり、直前まで見えていたものが消えるほうが分かりにくい
373+ raw = str (stored .get ("raw" ) or "" ),
355374 )
356375 windows = arranged (windows )
357- usage_store .save_quota (spec .id , [asdict (w ) for w in windows ])
358- return Quota (supported = True , fetched_at = _now_iso (), windows = windows )
376+ usage_store .save_quota (spec .id , [asdict (w ) for w in windows ], raw = raw )
377+ return Quota (supported = True , fetched_at = _now_iso (), windows = windows , raw = raw )
359378
360379
361380def arranged (windows : list [Window ]) -> list [Window ]:
@@ -409,6 +428,7 @@ def _stored_quota(provider_id: str, spec_usage: str, stored: dict) -> Quota:
409428 fetched_at = row .get ("fetched_at" , "" ),
410429 error = row .get ("error" , "" ),
411430 windows = _windows_from (row .get ("windows" , [])),
431+ raw = str (row .get ("raw" ) or "" ),
412432 )
413433
414434
0 commit comments