Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- """Stable repro for GLM-5.3-Flash tool-call drop on /alpha.
- Thesis (validated on capture #15 of the logging proxy):
- with an OpenAI-style agent history (assistant.tool_calls + role=tool),
- the model emits tool-call syntax the server's glm47 parser cannot turn
- into tool_calls: finish=stop, tool_calls=null, sometimes raw markup
- leaking into content. The same request rebuilt in the model's native
- dialect (assistant XML calls, tool results as user observations)
- returns finish=tool_calls. Bug = raw runs all drop, control runs all
- produce tool_calls.
- Tags are built at runtime to keep markup literals out of source text.
- """
- import argparse
- import glob
- import json
- import re
- import sys
- import urllib.request
- LT = chr(60)
- GT = chr(62)
- T_OPEN = LT + 'tool_call' + GT
- T_CLOSE = LT + '/tool_call' + GT
- K_OPEN = LT + 'arg_key' + GT
- K_CLOSE = LT + '/arg_key' + GT
- V_OPEN = LT + 'arg_value' + GT
- V_CLOSE = LT + '/arg_value' + GT
- O_OPEN = LT + 'observation' + GT
- O_CLOSE = LT + '/observation' + GT
- MARKUP_RE = re.compile('|'.join(re.escape(LT + t + GT) for t in
- ('tool_call', '/tool_call', 'arg_key', 'arg_value')))
- CALL_INTENT_RE = re.compile(
- r'(read|чита|посмотр|загруз|open|view|файл).{0,60}(файл|file|js|core|main|now|сейчас|дочит)'
- r'|(let me|надо|нужно).{0,40}(read|чита)', re.I | re.S)
- def call_to_xml(name: str, arguments: str) -> str:
- try:
- args = json.loads(arguments or '{}')
- except Exception:
- args = {}
- kv = ''.join(
- K_OPEN + k + K_CLOSE + V_OPEN
- + (v if isinstance(v, str) else json.dumps(v, ensure_ascii=False)) + V_CLOSE
- for k, v in args.items())
- return T_OPEN + name + kv + T_CLOSE
- def to_native_xml(messages):
- out = []
- for m in messages:
- if m.get('role') == 'assistant' and m.get('tool_calls'):
- txt = (m.get('content') or '') + ''.join(
- call_to_xml(tc['function']['name'], tc['function']['arguments'])
- for tc in m['tool_calls'])
- out.append({'role': 'assistant', 'content': txt})
- elif m.get('role') == 'tool':
- c = m['content'] if isinstance(m['content'], str) else json.dumps(m['content'], ensure_ascii=False)
- out.append({'role': 'user', 'content': O_OPEN + c + O_CLOSE})
- else:
- out.append(m)
- return out
- def post(base: str, token: str, body: dict, stream: bool, timeout=300):
- b = dict(body, stream=stream)
- req = urllib.request.Request(
- base.rstrip('/') + '/chat/completions',
- data=json.dumps(b).encode(),
- headers={'content-type': 'application/json',
- 'authorization': 'Bearer ' + token,
- 'x-glm-native-xml': '0'})
- if not stream:
- return json.loads(urllib.request.urlopen(req, timeout=timeout).read())
- acc = {'content': '', 'reasoning': '', 'tool_calls': {}, 'finish': None, 'usage': None, 'model': None}
- with urllib.request.urlopen(req, timeout=timeout) as r:
- for line in r:
- s = line.decode('utf8', 'replace').strip()
- if not s.startswith('data:'):
- continue
- data = s[5:].strip()
- if data == '[DONE]':
- break
- try:
- ev = json.loads(data)
- except Exception:
- continue
- if ev.get('usage'):
- acc['usage'] = ev['usage']
- if ev.get('model'):
- acc['model'] = ev['model']
- ch = (ev.get('choices') or [{}])[0]
- if ch.get('finish_reason'):
- acc['finish'] = ch['finish_reason']
- d = ch.get('delta') or {}
- if isinstance(d.get('content'), str):
- acc['content'] += d['content']
- if isinstance(d.get('reasoning_content'), str):
- acc['reasoning'] += d['reasoning_content']
- for tc in d.get('tool_calls') or []:
- slot = acc['tool_calls'].setdefault(tc.get('index', 0), {'name': '', 'arguments': ''})
- f = tc.get('function') or {}
- slot['name'] += f.get('name') or ''
- slot['arguments'] += f.get('arguments') or ''
- return acc
- def norm(resp, stream):
- if stream:
- tcs = [{'function': {'name': v['name'], 'arguments': v['arguments']}}
- for _, v in sorted(resp['tool_calls'].items()) if v['name'] or v['arguments']]
- return {'finish': resp['finish'], 'content': resp['content'],
- 'reasoning': resp['reasoning'], 'tool_calls': tcs or None, 'usage': resp['usage']}
- ch = (resp.get('choices') or [{}])[0]
- m = ch.get('message') or {}
- return {'finish': ch.get('finish_reason'), 'content': m.get('content') or '',
- 'reasoning': m.get('reasoning_content') or '',
- 'tool_calls': m.get('tool_calls'), 'usage': resp.get('usage')}
- def verdict(n):
- if n['tool_calls']:
- return 'HAS_TOOL_CALLS'
- if MARKUP_RE.search(n['content']):
- return 'BROKEN:markup_leak_in_content'
- if CALL_INTENT_RE.search((n['reasoning'] or '')[-300:]) or CALL_INTENT_RE.search(n['content'][-200:]):
- return 'BROKEN:call_intent_but_no_tool_calls'
- return 'NO_CALL_AMBIGUOUS'
- def main():
- ap = argparse.ArgumentParser()
- ap.add_argument('--base', default='http://127.0.0.1:8787/v1')
- ap.add_argument('--token', default='token')
- ap.add_argument('--capture', default='~/llm-proxy/captures/00015-*')
- ap.add_argument('--runs', type=int, default=2)
- ap.add_argument('--max-tokens', type=int, default=800)
- a = ap.parse_args()
- path = sorted(glob.glob(a.capture))[0]
- body = json.load(open(path))['req']['body']
- native = dict(body, messages=to_native_xml(body['messages']))
- for k in ('temperature', 'top_p', 'top_k', 'repetition_penalty'):
- body.pop(k, None)
- native.pop(k, None)
- body['max_tokens'] = native['max_tokens'] = a.max_tokens
- n_tool_msgs = sum(1 for m in body['messages'] if m.get('role') == 'tool')
- print(f'repro: {path}\n msgs={len(body["messages"])} tool_msgs={n_tool_msgs} '
- f'model={body.get("model")!r} max_tokens={a.max_tokens}\n'
- f' base={a.base} (x-glm-native-xml: 0 -> no proxy rewrite)\n')
- raw_results, ctrl_results = [], []
- for i in range(1, a.runs + 1):
- for stream in (False, True):
- mode = 'stream' if stream else 'nonstream'
- try:
- n = norm(post(a.base, a.token, body, stream), stream)
- v = verdict(n)
- raw_results.append(v)
- print(f'[raw #{i} {mode:9s}] finish={n["finish"]} '
- f'tool_calls={"-" if not n["tool_calls"] else n["tool_calls"][0]["function"]["name"]} '
- f'compl={(n["usage"] or {}).get("completion_tokens")} -> {v}')
- if 'markup' in v:
- print(' leaked: ' + repr(MARKUP_RE.findall(n['content'])[:2]) + ' | ' + repr(n['content'][:100]))
- except Exception as e:
- raw_results.append('ERROR:' + str(e))
- print(f'[raw #{i} {mode:9s}] ERROR {e}')
- try:
- n = norm(post(a.base, a.token, native, False), False)
- v = verdict(n)
- ctrl_results.append(v)
- name = n['tool_calls'][0]['function']['name'] if n['tool_calls'] else '-'
- print(f'[ctrl #{i} native-xml] finish={n["finish"]} tool_calls={name} '
- f'compl={(n["usage"] or {}).get("completion_tokens")} -> {v}')
- except Exception as e:
- ctrl_results.append('ERROR:' + str(e))
- print(f'[ctrl #{i} native-xml] ERROR {e}')
- broken = [v for v in raw_results if v.startswith('BROKEN')]
- ctrl_ok = [v for v in ctrl_results if v == 'HAS_TOOL_CALLS']
- print(f'\nraw dropped tool calls: {len(broken)}/{len(raw_results)} | '
- f'native-xml control produced tool_calls: {len(ctrl_ok)}/{len(ctrl_results)} '
- f'(informational: history is contaminated by earlier malformed calls, '
- f'so control rate estimates P(call | native dialect) < 1)')
- if len(broken) == len(raw_results):
- print('VERDICT: BUG REPRODUCED - every raw OpenAI-style run announced a tool call '
- 'and returned finish=stop with tool_calls=null')
- sys.exit(0)
- print('VERDICT: NOT STABLY REPRODUCED - inspect above')
- sys.exit(2)
- if __name__ == '__main__':
- main()
Advertisement
Add Comment
Please, Sign In to add comment