{"record":{"id":"0fd2fd2940a61efd","repo":"binary-husky/gpt_academic","slug":"error-0fd2fd","errorCode":null,"errorMessage":"用户取消了程序。","messagePattern":"用户取消了程序。","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"request_llms/bridge_openrouter.py","lineNumber":199,"sourceCode":"                raise RuntimeError(\"OpenAI拒绝了请求：\" + error_msg)\n        if ('data: [DONE]' in chunk_decoded): break # api2d 正常完成\n        # 提前读取一些信息 （用于判断异常）\n        json_data = chunkjson['choices'][0]\n        delta = json_data[\"delta\"]\n        if len(delta) == 0: break\n        if (not has_content) and has_role: continue\n        if (not has_content) and (not has_role): continue # raise RuntimeError(\"发现不标准的第三方接口：\"+delta)\n        if has_content: # has_role = True/False\n            result += delta[\"content\"]\n            if not console_silence: print(delta[\"content\"], end='')\n            if observe_window is not None:\n                # 观测窗，把已经获取的数据显示出去\n                if len(observe_window) >= 1:\n                    observe_window[0] += delta[\"content\"]\n                # 看门狗，如果超过期限没有喂狗，则终止\n                if len(observe_window) >= 2:\n                    if (time.time()-observe_window[1]) > watch_dog_patience:\n                        raise RuntimeError(\"用户取消了程序。\")\n        else: raise RuntimeError(\"意外Json结构：\"+delta)\n    if json_data and json_data['finish_reason'] == 'content_filter':\n        raise RuntimeError(\"由于提问含不合规内容被Azure过滤。\")\n    if json_data and json_data['finish_reason'] == 'length':\n        raise ConnectionAbortedError(\"正常结束，但显示Token不足，导致输出不完整，请削减单次输入的文本量。\")\n    return result\n\n\ndef predict(inputs:str, llm_kwargs:dict, plugin_kwargs:dict, chatbot:ChatBotWithCookies,\n            history:list=[], system_prompt:str='', stream:bool=True, additional_fn:str=None):\n    \"\"\"\n    发送至chatGPT，流式获取输出。\n    用于基础的对话功能。\n    inputs 是本次问询的输入\n    top_p, temperature是chatGPT的内部调优参数\n    history 是之前的对话列表（注意无论是inputs还是history，内容太长了都会触发token数量溢出的错误）\n    chatbot 为WebUI中显示的对话列表，修改它，然后yield出去，可以直接修改对话界面内容\n    additional_fn代表点击的哪个按钮，按钮见functional.py","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/binary-husky/gpt_academic/blob/d6bde0fa54373309bd05823a49bda8da019d2c77/request_llms/bridge_openrouter.py#L181-L217","documentation":"Watchdog RuntimeError inside bridge_openrouter.predict_no_ui_long_connection: observe_window[1] holds the caller's last heartbeat timestamp, and if it is older than watch_dog_patience seconds the loop aborts with '用户取消了程序。' (user cancelled). It is a cooperative-cancellation mechanism - any caller that stops feeding the dog causes it, intentional or not.","triggerScenarios":"Caller stops refreshing observe_window[1] (user pressed Stop, or the monitoring thread died); network stall so no delta arrives while the watchdog compares against an unfed timestamp; plugin using predict_no_ui_long_connection without a live feeder thread.","commonSituations":"Gradio Stop button during a long OpenRouter generation; plugin author forgetting the feeder-thread contract; slow upstream where chunks pause longer than the patience value.","solutions":["If the user cancelled, treat this as normal termination and discard partial state","Ensure the consumer thread updates observe_window[1] = time.time() each poll iteration","Increase watch_dog_patience for slow providers","Wrap the call in try/except RuntimeError and distinguish cancellation from provider errors before retrying"],"exampleFix":"# consumer contract\nimport threading, time\n\ndef feed(ow):\n    while streaming[0]:\n        ow[1] = time.time()\n        time.sleep(1)","handlingStrategy":"try-catch","validationCode":"import time\nassert len(observe_window) >= 2, 'watchdog requires observe_window[1] heartbeat'\now[1] = time.time()  # prime the dog before first chunk","typeGuard":null,"tryCatchPattern":"try:\n    result = predict_no_ui_long_connection(...)\nexcept RuntimeError as e:\n    if str(e) == '用户取消了程序。':\n        cancelled.set(); return ow[0]\n    raise","preventionTips":["Implement the feeder thread as a reusable helper so no call site forgets it","Refresh the heartbeat in the same loop that reads observe_window[0]","Document that this message means cancellation by contract across all bridges"],"tags":["openrouter","watchdog","cancellation","streaming","concurrency"],"backgroundTag":null,"analyzedSha":"d6bde0fa54373309bd05823a49bda8da019d2c77","analyzedAt":"2026-08-14T22:48:35.038Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}