Guest User

Untitled

a guest
Sep 4th, 2026
53
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.51 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. """Stable repro for GLM-5.3-Flash tool-call drop on /alpha.
  3.  
  4. Thesis (validated on capture #15 of the logging proxy):
  5. with an OpenAI-style agent history (assistant.tool_calls + role=tool),
  6. the model emits tool-call syntax the server's glm47 parser cannot turn
  7. into tool_calls: finish=stop, tool_calls=null, sometimes raw markup
  8. leaking into content. The same request rebuilt in the model's native
  9. dialect (assistant XML calls, tool results as user observations)
  10. returns finish=tool_calls. Bug = raw runs all drop, control runs all
  11. produce tool_calls.
  12.  
  13. Tags are built at runtime to keep markup literals out of source text.
  14. """
  15. import argparse
  16. import glob
  17. import json
  18. import re
  19. import sys
  20. import urllib.request
  21.  
  22. LT = chr(60)
  23. GT = chr(62)
  24. T_OPEN = LT + 'tool_call' + GT
  25. T_CLOSE = LT + '/tool_call' + GT
  26. K_OPEN = LT + 'arg_key' + GT
  27. K_CLOSE = LT + '/arg_key' + GT
  28. V_OPEN = LT + 'arg_value' + GT
  29. V_CLOSE = LT + '/arg_value' + GT
  30. O_OPEN = LT + 'observation' + GT
  31. O_CLOSE = LT + '/observation' + GT
  32. MARKUP_RE = re.compile('|'.join(re.escape(LT + t + GT) for t in
  33. ('tool_call', '/tool_call', 'arg_key', 'arg_value')))
  34.  
  35. CALL_INTENT_RE = re.compile(
  36. r'(read|чита|посмотр|загруз|open|view|файл).{0,60}(файл|file|js|core|main|now|сейчас|дочит)'
  37. r'|(let me|надо|нужно).{0,40}(read|чита)', re.I | re.S)
  38.  
  39.  
  40. def call_to_xml(name: str, arguments: str) -> str:
  41. try:
  42. args = json.loads(arguments or '{}')
  43. except Exception:
  44. args = {}
  45. kv = ''.join(
  46. K_OPEN + k + K_CLOSE + V_OPEN
  47. + (v if isinstance(v, str) else json.dumps(v, ensure_ascii=False)) + V_CLOSE
  48. for k, v in args.items())
  49. return T_OPEN + name + kv + T_CLOSE
  50.  
  51.  
  52. def to_native_xml(messages):
  53. out = []
  54. for m in messages:
  55. if m.get('role') == 'assistant' and m.get('tool_calls'):
  56. txt = (m.get('content') or '') + ''.join(
  57. call_to_xml(tc['function']['name'], tc['function']['arguments'])
  58. for tc in m['tool_calls'])
  59. out.append({'role': 'assistant', 'content': txt})
  60. elif m.get('role') == 'tool':
  61. c = m['content'] if isinstance(m['content'], str) else json.dumps(m['content'], ensure_ascii=False)
  62. out.append({'role': 'user', 'content': O_OPEN + c + O_CLOSE})
  63. else:
  64. out.append(m)
  65. return out
  66.  
  67.  
  68. def post(base: str, token: str, body: dict, stream: bool, timeout=300):
  69. b = dict(body, stream=stream)
  70. req = urllib.request.Request(
  71. base.rstrip('/') + '/chat/completions',
  72. data=json.dumps(b).encode(),
  73. headers={'content-type': 'application/json',
  74. 'authorization': 'Bearer ' + token,
  75. 'x-glm-native-xml': '0'})
  76. if not stream:
  77. return json.loads(urllib.request.urlopen(req, timeout=timeout).read())
  78. acc = {'content': '', 'reasoning': '', 'tool_calls': {}, 'finish': None, 'usage': None, 'model': None}
  79. with urllib.request.urlopen(req, timeout=timeout) as r:
  80. for line in r:
  81. s = line.decode('utf8', 'replace').strip()
  82. if not s.startswith('data:'):
  83. continue
  84. data = s[5:].strip()
  85. if data == '[DONE]':
  86. break
  87. try:
  88. ev = json.loads(data)
  89. except Exception:
  90. continue
  91. if ev.get('usage'):
  92. acc['usage'] = ev['usage']
  93. if ev.get('model'):
  94. acc['model'] = ev['model']
  95. ch = (ev.get('choices') or [{}])[0]
  96. if ch.get('finish_reason'):
  97. acc['finish'] = ch['finish_reason']
  98. d = ch.get('delta') or {}
  99. if isinstance(d.get('content'), str):
  100. acc['content'] += d['content']
  101. if isinstance(d.get('reasoning_content'), str):
  102. acc['reasoning'] += d['reasoning_content']
  103. for tc in d.get('tool_calls') or []:
  104. slot = acc['tool_calls'].setdefault(tc.get('index', 0), {'name': '', 'arguments': ''})
  105. f = tc.get('function') or {}
  106. slot['name'] += f.get('name') or ''
  107. slot['arguments'] += f.get('arguments') or ''
  108. return acc
  109.  
  110.  
  111. def norm(resp, stream):
  112. if stream:
  113. tcs = [{'function': {'name': v['name'], 'arguments': v['arguments']}}
  114. for _, v in sorted(resp['tool_calls'].items()) if v['name'] or v['arguments']]
  115. return {'finish': resp['finish'], 'content': resp['content'],
  116. 'reasoning': resp['reasoning'], 'tool_calls': tcs or None, 'usage': resp['usage']}
  117. ch = (resp.get('choices') or [{}])[0]
  118. m = ch.get('message') or {}
  119. return {'finish': ch.get('finish_reason'), 'content': m.get('content') or '',
  120. 'reasoning': m.get('reasoning_content') or '',
  121. 'tool_calls': m.get('tool_calls'), 'usage': resp.get('usage')}
  122.  
  123.  
  124. def verdict(n):
  125. if n['tool_calls']:
  126. return 'HAS_TOOL_CALLS'
  127. if MARKUP_RE.search(n['content']):
  128. return 'BROKEN:markup_leak_in_content'
  129. if CALL_INTENT_RE.search((n['reasoning'] or '')[-300:]) or CALL_INTENT_RE.search(n['content'][-200:]):
  130. return 'BROKEN:call_intent_but_no_tool_calls'
  131. return 'NO_CALL_AMBIGUOUS'
  132.  
  133.  
  134. def main():
  135. ap = argparse.ArgumentParser()
  136. ap.add_argument('--base', default='http://127.0.0.1:8787/v1')
  137. ap.add_argument('--token', default='token')
  138. ap.add_argument('--capture', default='~/llm-proxy/captures/00015-*')
  139. ap.add_argument('--runs', type=int, default=2)
  140. ap.add_argument('--max-tokens', type=int, default=800)
  141. a = ap.parse_args()
  142.  
  143. path = sorted(glob.glob(a.capture))[0]
  144. body = json.load(open(path))['req']['body']
  145. native = dict(body, messages=to_native_xml(body['messages']))
  146. for k in ('temperature', 'top_p', 'top_k', 'repetition_penalty'):
  147. body.pop(k, None)
  148. native.pop(k, None)
  149. body['max_tokens'] = native['max_tokens'] = a.max_tokens
  150. n_tool_msgs = sum(1 for m in body['messages'] if m.get('role') == 'tool')
  151. print(f'repro: {path}\n msgs={len(body["messages"])} tool_msgs={n_tool_msgs} '
  152. f'model={body.get("model")!r} max_tokens={a.max_tokens}\n'
  153. f' base={a.base} (x-glm-native-xml: 0 -> no proxy rewrite)\n')
  154.  
  155. raw_results, ctrl_results = [], []
  156. for i in range(1, a.runs + 1):
  157. for stream in (False, True):
  158. mode = 'stream' if stream else 'nonstream'
  159. try:
  160. n = norm(post(a.base, a.token, body, stream), stream)
  161. v = verdict(n)
  162. raw_results.append(v)
  163. print(f'[raw #{i} {mode:9s}] finish={n["finish"]} '
  164. f'tool_calls={"-" if not n["tool_calls"] else n["tool_calls"][0]["function"]["name"]} '
  165. f'compl={(n["usage"] or {}).get("completion_tokens")} -> {v}')
  166. if 'markup' in v:
  167. print(' leaked: ' + repr(MARKUP_RE.findall(n['content'])[:2]) + ' | ' + repr(n['content'][:100]))
  168. except Exception as e:
  169. raw_results.append('ERROR:' + str(e))
  170. print(f'[raw #{i} {mode:9s}] ERROR {e}')
  171. try:
  172. n = norm(post(a.base, a.token, native, False), False)
  173. v = verdict(n)
  174. ctrl_results.append(v)
  175. name = n['tool_calls'][0]['function']['name'] if n['tool_calls'] else '-'
  176. print(f'[ctrl #{i} native-xml] finish={n["finish"]} tool_calls={name} '
  177. f'compl={(n["usage"] or {}).get("completion_tokens")} -> {v}')
  178. except Exception as e:
  179. ctrl_results.append('ERROR:' + str(e))
  180. print(f'[ctrl #{i} native-xml] ERROR {e}')
  181.  
  182. broken = [v for v in raw_results if v.startswith('BROKEN')]
  183. ctrl_ok = [v for v in ctrl_results if v == 'HAS_TOOL_CALLS']
  184. print(f'\nraw dropped tool calls: {len(broken)}/{len(raw_results)} | '
  185. f'native-xml control produced tool_calls: {len(ctrl_ok)}/{len(ctrl_results)} '
  186. f'(informational: history is contaminated by earlier malformed calls, '
  187. f'so control rate estimates P(call | native dialect) < 1)')
  188. if len(broken) == len(raw_results):
  189. print('VERDICT: BUG REPRODUCED - every raw OpenAI-style run announced a tool call '
  190. 'and returned finish=stop with tool_calls=null')
  191. sys.exit(0)
  192. print('VERDICT: NOT STABLY REPRODUCED - inspect above')
  193. sys.exit(2)
  194.  
  195.  
  196. if __name__ == '__main__':
  197. main()
  198.  
Advertisement
Add Comment
Please, Sign In to add comment