omnilane 0.34.0 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +71 -1
  4. package/README.ja.md +63 -33
  5. package/README.ko.md +63 -32
  6. package/README.md +147 -86
  7. package/README.zh-CN.md +61 -30
  8. package/README.zh-TW.md +124 -75
  9. package/VERSION +1 -1
  10. package/config/aa-model-policy.json +3046 -0
  11. package/docs/aa-model-coverage-2026-09-05.json +29204 -0
  12. package/docs/completion-wakeup.md +126 -0
  13. package/docs/model-capabilities-2026-09.md +380 -0
  14. package/docs/native-executor.md +264 -0
  15. package/docs/release-notes-0.42.1.md +32 -0
  16. package/hooks/routing-instruction.md +101 -40
  17. package/package.json +8 -2
  18. package/plugin.json +2 -2
  19. package/routing.local.yaml.example +8 -3
  20. package/routing.yaml +16 -16
  21. package/scripts/completion-wakeup.py +390 -0
  22. package/scripts/configure.sh +4 -4
  23. package/scripts/dispatch.sh +323 -32
  24. package/scripts/doctor.sh +55 -1
  25. package/scripts/jobs.sh +64 -17
  26. package/scripts/lib/aa_policy.py +473 -0
  27. package/scripts/lib/aa_retry.py +77 -0
  28. package/scripts/lib/common.sh +106 -1
  29. package/scripts/lib/job-worker.sh +314 -20
  30. package/scripts/lib/live-protocol.sh +147 -2
  31. package/scripts/lib/native.py +507 -0
  32. package/scripts/lib/normalize-claude-stream.py +72 -0
  33. package/scripts/lib/prepare-agy-mode.py +374 -0
  34. package/scripts/release-audit.sh +103 -0
  35. package/scripts/runners/run-claude.sh +81 -47
  36. package/scripts/runners/run-codex-live.py +462 -0
  37. package/scripts/runners/run-codex.sh +62 -3
  38. package/scripts/runners/run-gemini.sh +85 -10
  39. package/scripts/runners/run-grok-live.py +426 -0
  40. package/scripts/runners/run-grok.sh +117 -6
  41. package/scripts/runners/run-vote.sh +6 -3
  42. package/skills/omnilane/SKILL.md +217 -81
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env python3
2
+ """Caller-owned Codex heartbeat receipts; never invokes a model or scheduler.
3
+
4
+ Only CLI job metadata and exit markers are observed. Private outputs, prompts,
5
+ native state, inbox tails and Codex session files are deliberately not read.
6
+ """
7
+ import argparse
8
+ import contextlib
9
+ import fcntl
10
+ import hashlib
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+ import re
15
+ import secrets
16
+ import stat
17
+ import sys
18
+ import tempfile
19
+ import time
20
+
21
+ LIMIT = 128 * 1024
22
+ IDENTITY = ('lane', 'vendor', 'model', 'mode', 'workdir', 'foreman_session', 'started')
23
+
24
+
25
+ class Invalid(ValueError):
26
+ pass
27
+
28
+
29
+ def check(value, message):
30
+ if not value:
31
+ raise Invalid(message)
32
+
33
+
34
+ def identifier(value):
35
+ check(isinstance(value, str) and re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._:-]{0,255}', value),
36
+ 'invalid identifier')
37
+ return value
38
+
39
+
40
+ def no_links(path):
41
+ path = Path(os.path.abspath(path))
42
+ for part in (path, *path.parents):
43
+ check(not part.is_symlink(), 'symlink path rejected')
44
+ return path
45
+
46
+
47
+ def read_bytes(path, limit=LIMIT):
48
+ path = no_links(path)
49
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
50
+ with os.fdopen(fd, 'rb') as stream:
51
+ info = os.fstat(stream.fileno())
52
+ check(stat.S_ISREG(info.st_mode) and info.st_uid == os.getuid(), 'invalid record owner/type')
53
+ data = stream.read(limit + 1)
54
+ check(len(data) <= limit, 'record too large')
55
+ return data
56
+
57
+
58
+ def read_json(path):
59
+ value = json.loads(read_bytes(path))
60
+ check(isinstance(value, dict), 'expected JSON object')
61
+ return value
62
+
63
+
64
+ def digest(value):
65
+ return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
66
+
67
+
68
+ def write_json(path, value):
69
+ raw = (json.dumps(value, ensure_ascii=False, sort_keys=True) + '\n').encode()
70
+ check(len(raw) <= LIMIT, 'registry too large')
71
+ no_links(path)
72
+ fd, name = tempfile.mkstemp(prefix='.' + path.name + '-', dir=path.parent)
73
+ try:
74
+ with os.fdopen(fd, 'wb') as stream:
75
+ stream.write(raw); stream.flush(); os.fsync(stream.fileno())
76
+ os.replace(name, path)
77
+ finally:
78
+ if os.path.exists(name):
79
+ os.unlink(name)
80
+
81
+
82
+ @contextlib.contextmanager
83
+ def registry_lock(args):
84
+ home = no_links(args.home)
85
+ if args.action == 'prepare':
86
+ home.mkdir(mode=0o700, parents=True, exist_ok=True)
87
+ check(home.is_dir(), 'home missing')
88
+ root = no_links(home / 'wakeups')
89
+ if args.action == 'prepare':
90
+ root.mkdir(mode=0o700, exist_ok=True)
91
+ check(root.is_dir(), 'registry not prepared')
92
+ info = root.stat()
93
+ check(info.st_uid == os.getuid() and not info.st_mode & 0o077, 'wakeup store must be owner-only')
94
+ key = digest([args.host_id, args.thread_id])[:32]
95
+ path = root / (key + '.json')
96
+ fd = os.open(root / (key + '.lock'), os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600)
97
+ with os.fdopen(fd, 'r+') as lock:
98
+ info = os.fstat(lock.fileno())
99
+ check(stat.S_ISREG(info.st_mode) and info.st_uid == os.getuid(), 'invalid lock')
100
+ # Advisory kernel locks automatically release on crash; no stale PID cleanup.
101
+ fcntl.flock(lock, fcntl.LOCK_EX)
102
+ yield path
103
+
104
+
105
+ def identity(args, job, workdir, claims=None, permit_adoption=False):
106
+ identifier(job)
107
+ path = no_links(Path(args.home) / 'jobs' / job)
108
+ check(path.is_dir(), 'job missing')
109
+ check(not os.path.lexists(path / 'native.json'), 'unsupported-native: use native completion channel')
110
+ meta = read_json(path / 'meta.json')
111
+ check(all(isinstance(meta.get(k), str) for k in IDENTITY), 'invalid job metadata')
112
+ actual = no_links(meta['workdir'])
113
+ check(actual.is_dir() and str(actual.resolve()) == workdir, 'job workdir mismatch')
114
+ public = {k: meta[k] for k in IDENTITY}
115
+ public['workdir'] = workdir
116
+ # This is a Claude process/session binding, not a Codex app thread ID.
117
+ frozen = digest(public)
118
+ if meta['foreman_session']:
119
+ claims = {} if claims is None else claims
120
+ claim = claims.get(job)
121
+ if claim is None and permit_adoption and args.adoption_evidence:
122
+ receipt = read_json(args.adoption_evidence)
123
+ check(receipt.get('source') == 'operator_adoption'
124
+ and receipt.get('operator_confirmed') is True
125
+ and receipt.get('controller_thread_id') == args.thread_id
126
+ and receipt.get('controller_host_id') == args.host_id
127
+ and receipt.get('run_id') == args.run_id
128
+ and isinstance(receipt.get('job_identity_sha256'), dict)
129
+ and receipt['job_identity_sha256'].get(job) == frozen,
130
+ 'adoption receipt binding/identity mismatch')
131
+ claim = {'identity_sha256': frozen, 'receipt_path': str(no_links(args.adoption_evidence)),
132
+ 'receipt_sha256': digest(receipt), 'trust': 'operator adoption attested by caller'}
133
+ claims[job] = claim
134
+ # Reveal only public job id + fingerprint, not the original session value.
135
+ check(isinstance(claim, dict) and claim.get('identity_sha256') == frozen,
136
+ 'adoption_required:' + job + ':' + frozen)
137
+ # Freeze immutable identity, not mutable completion/provenance fields.
138
+ return frozen
139
+
140
+
141
+ def assert_binding(state, args):
142
+ check(state.get('schema_version') == 1, 'unsupported registry schema')
143
+ check(state.get('controller_thread_id') == args.thread_id
144
+ and state.get('controller_host_id') == args.host_id
145
+ and state.get('run_id') == args.run_id, 'controller binding mismatch')
146
+ check(state.get('home') == str(no_links(args.home)), 'home binding mismatch')
147
+ check(state.get('status') in ('pending', 'registered', 'closed'), 'invalid registry status')
148
+
149
+
150
+ def summary(state, path):
151
+ return {'schema_version': 1, 'registry': str(path), 'status': state['status'],
152
+ 'automation_id': state.get('automation_id'), 'tracked_jobs': sorted(state['jobs'])}
153
+
154
+
155
+ def command_prefix(args):
156
+ import shlex
157
+ return ' '.join(shlex.quote(x) for x in [sys.executable, str(Path(__file__).resolve()), 'poll',
158
+ '--home', str(no_links(args.home)), '--thread-id', args.thread_id,
159
+ '--host-id', args.host_id, '--run-id', args.run_id])
160
+
161
+
162
+ def assert_unused_historical_run(args, path):
163
+ """A stopped heartbeat must never become valid again through name reuse."""
164
+ history = no_links(path.parent / 'history')
165
+ if not history.exists():
166
+ return
167
+ check(history.is_dir(), 'invalid history store')
168
+ for archived in history.glob(path.stem + '-*.json'):
169
+ old = read_json(archived)
170
+ check(old.get('controller_thread_id') == args.thread_id
171
+ and old.get('controller_host_id') == args.host_id, 'history binding mismatch')
172
+ check(old.get('run_id') != args.run_id, 'historical run id cannot be reused')
173
+
174
+
175
+ def prepare(args, state, path, now):
176
+ check(args.workdir and args.job, 'prepare requires --workdir and --job')
177
+ work = no_links(args.workdir)
178
+ check(work.is_dir(), 'workdir missing')
179
+ work = str(work.resolve())
180
+ if state is None:
181
+ state = {'schema_version': 1, 'controller_thread_id': args.thread_id,
182
+ 'controller_host_id': args.host_id, 'run_id': args.run_id,
183
+ 'home': str(no_links(args.home)), 'workdir': work, 'status': 'pending',
184
+ 'created_at': now, 'expires_at': now + args.ttl_seconds,
185
+ 'requested_interval_seconds': args.interval_seconds, 'jobs': {}, 'events': {}}
186
+ else:
187
+ assert_binding(state, args)
188
+ check(state['status'] != 'closed' and now < state['expires_at'], 'registry closed/expired')
189
+ check(state['workdir'] == work, 'workdir binding mismatch')
190
+ for job in args.job:
191
+ frozen = identity(args, job, work, state.setdefault('adoptions', {}), permit_adoption=True)
192
+ check(job not in state['jobs'] or state['jobs'][job] == frozen, 'job identity changed')
193
+ state['jobs'][job] = frozen
194
+ check(len(state['jobs']) <= 100, 'job limit exceeded')
195
+ prompt = (f'檢查此任務已登錄的 omnilane 背景工作:執行 {command_prefix(args)}。'
196
+ '沒有新事件或需處理問題時保持安靜。只處理登錄的工作,確認主控綁定後以 claim token '
197
+ 'ack-delivered,再接續原任務驗收並以證據 ack-accepted;退出碼零不是驗收通過。'
198
+ '不要新建監看器或自動派工。needs_disable 時停用本 heartbeat,再以工具結果回填 closed。')
199
+ tool_args = {'mode': 'update' if state.get('automation_id') else 'create', 'kind': 'heartbeat',
200
+ 'name': 'omnilane 完成續驗', 'prompt': prompt, 'status': 'ACTIVE',
201
+ 'targetThreadId': args.thread_id}
202
+ if state.get('automation_id'):
203
+ tool_args['id'] = state['automation_id']
204
+ result = summary(state, path)
205
+ result.update(tool='mcp__codex_app__automation_update', tool_args=tool_args,
206
+ schedule_request={'interval_seconds': state['requested_interval_seconds'],
207
+ 'instruction': '主控使用工具支援的排程格式補入 rrule;保留既有通知偏好。'},
208
+ registration_required=state['status'] == 'pending')
209
+ return state, result
210
+
211
+
212
+ def automation_receipt(args, expected):
213
+ check(args.evidence and args.automation_id, 'automation id and evidence required')
214
+ receipt = read_json(args.evidence)
215
+ check(receipt.get('source') == 'automation_update'
216
+ and receipt.get('automation_id') == args.automation_id
217
+ and receipt.get('controller_thread_id') == args.thread_id
218
+ and receipt.get('controller_host_id') == args.host_id
219
+ and receipt.get('status') == expected, 'automation receipt binding/status mismatch')
220
+ return {'path': str(no_links(args.evidence)), 'sha256': digest(receipt),
221
+ 'effective_interval_seconds': receipt.get('effective_interval_seconds'),
222
+ 'trust': 'caller-attested tool result; not independent scheduler verification'}
223
+
224
+
225
+ def poll(args, state, path, now):
226
+ result = summary(state, path)
227
+ result.update(events=[], errors=[], needs_disable=False)
228
+ if state['status'] == 'closed':
229
+ return result
230
+ if now >= state['expires_at']:
231
+ result.update(status='expired', needs_disable=bool(state.get('automation_id')))
232
+ return result
233
+ if state['status'] != 'registered':
234
+ return result
235
+ accepted = 0
236
+ for job, frozen in sorted(state['jobs'].items()):
237
+ try:
238
+ check(identity(args, job, state['workdir'], state.get('adoptions', {})) == frozen, 'binding_changed')
239
+ marker = Path(args.home) / 'jobs' / job / 'exit'
240
+ if not os.path.lexists(marker):
241
+ continue
242
+ raw = read_bytes(marker, 32).decode().strip()
243
+ check(re.fullmatch(r'-?\d{1,5}', raw), 'invalid_exit')
244
+ rc = int(raw)
245
+ check(-32768 <= rc <= 32767, 'invalid_exit')
246
+ key = digest([args.run_id, job, frozen, rc])
247
+ # Changing an already observed terminal exit is not a fresh generation.
248
+ prior = [k for k, e in state['events'].items() if e['job_id'] == job]
249
+ check(not prior or prior == [key], 'terminal_changed')
250
+ event = state['events'].setdefault(key, {'job_id': job, 'exit_code': rc,
251
+ 'state': 'observed', 'observed_at': now})
252
+ if event['state'] == 'accepted':
253
+ accepted += 1
254
+ continue
255
+ if event.get('lease_until', 0) > now:
256
+ continue
257
+ event.update(claim_token=secrets.token_hex(16), lease_until=now + args.lease_seconds,
258
+ state='claimed')
259
+ result['events'].append({'event_key': key, 'job_id': job, 'exit_code': rc,
260
+ 'claim_token': event['claim_token'], 'lease_until': event['lease_until']})
261
+ except (Invalid, OSError, ValueError) as error:
262
+ # No arbitrary metadata, logs, paths or file contents in public errors.
263
+ code = str(error) if isinstance(error, Invalid) else 'invalid_job_record'
264
+ result['errors'].append({'job_id': job, 'code': code})
265
+ result['needs_disable'] = accepted == len(state['jobs']) and not result['errors']
266
+ state['last_observed_at'] = now
267
+ return result
268
+
269
+
270
+ def act(args):
271
+ for value in (args.thread_id, args.host_id, args.run_id):
272
+ identifier(value)
273
+ with registry_lock(args) as path:
274
+ state = read_json(path) if os.path.lexists(path) else None
275
+ now = time.time()
276
+ previous_run = None
277
+ if args.action == 'prepare':
278
+ assert_unused_historical_run(args, path)
279
+ if state and state.get('status') == 'closed' and state.get('run_id') != args.run_id:
280
+ # Preserve the closed run before publishing a new active binding.
281
+ # Validate every dimension except the intentionally new run ID.
282
+ old_run_id = state.get('run_id')
283
+ identifier(old_run_id)
284
+ state_for_binding = dict(state, run_id=args.run_id)
285
+ assert_binding(state_for_binding, args)
286
+ previous_run, state = state, None
287
+ state, result = prepare(args, state, path, now)
288
+ else:
289
+ check(state is not None, 'registry not prepared')
290
+ assert_binding(state, args)
291
+ result = summary(state, path)
292
+ if args.action == 'poll':
293
+ result = poll(args, state, path, now)
294
+ elif args.action == 'record-registration':
295
+ check(state['status'] != 'closed' and now < state['expires_at'], 'registry closed/expired')
296
+ identifier(args.automation_id)
297
+ check(not state.get('automation_id') or state['automation_id'] == args.automation_id,
298
+ 'automation id mismatch')
299
+ receipt = automation_receipt(args, 'ACTIVE')
300
+ state.update(status='registered', automation_id=args.automation_id,
301
+ registration_receipt=receipt, registered_at=now)
302
+ result = summary(state, path)
303
+ elif args.action == 'closed':
304
+ check(state.get('automation_id') == args.automation_id, 'automation id mismatch')
305
+ receipt = automation_receipt(args, 'PAUSED')
306
+ if args.close_reason == 'completed':
307
+ check(len(state['events']) == len(state['jobs'])
308
+ and all(e['state'] == 'accepted' for e in state['events'].values()),
309
+ 'unaccepted jobs remain; use explicit stopped/expired reason')
310
+ if args.close_reason == 'expired':
311
+ check(now >= state['expires_at'], 'registry not expired')
312
+ state.update(status='closed', closed_at=now, closure_receipt=receipt,
313
+ close_reason=args.close_reason)
314
+ result = summary(state, path)
315
+ else:
316
+ check(state['status'] == 'registered' and now < state['expires_at'], 'inactive registry')
317
+ event = state['events'].get(args.event_key)
318
+ check(event and event.get('claim_token') == args.claim_token
319
+ and event.get('lease_until', 0) > now, 'invalid/expired claim')
320
+ check(identity(args, event['job_id'], state['workdir'], state.get('adoptions', {}))
321
+ == state['jobs'][event['job_id']],
322
+ 'binding_changed')
323
+ current_exit = read_bytes(Path(args.home) / 'jobs' / event['job_id'] / 'exit', 32)
324
+ check(current_exit.decode().strip() == str(event['exit_code']), 'terminal_changed')
325
+ if args.action == 'ack-delivered':
326
+ check(event['state'] in ('claimed', 'delivered', 'accepted'), 'invalid delivery state')
327
+ if event['state'] == 'claimed':
328
+ event.update(state='delivered', delivered_at=now)
329
+ else:
330
+ check(event['state'] in ('delivered', 'accepted'), 'delivery required before acceptance')
331
+ check(args.result and args.evidence, 'acceptance result/evidence required')
332
+ data = read_bytes(args.evidence)
333
+ check(data.strip(), 'empty acceptance evidence')
334
+ evidence = {'path': str(no_links(args.evidence)),
335
+ 'sha256': hashlib.sha256(data).hexdigest()}
336
+ if event['state'] == 'accepted':
337
+ check(event['result'] == args.result and event['evidence'] == evidence,
338
+ 'acceptance receipt conflict')
339
+ else:
340
+ event.update(state='accepted', accepted_at=now, result=args.result, evidence=evidence)
341
+ result.update(event_key=args.event_key, event_state=event['state'])
342
+ if previous_run is not None:
343
+ history = no_links(path.parent / 'history')
344
+ history.mkdir(mode=0o700, exist_ok=True)
345
+ info = history.stat()
346
+ check(info.st_uid == os.getuid() and not info.st_mode & 0o077, 'history must be owner-only')
347
+ archived = history / (path.stem + '-' + digest(previous_run) + '.json')
348
+ if os.path.lexists(archived):
349
+ check(read_json(archived) == previous_run, 'history conflict')
350
+ else:
351
+ write_json(archived, previous_run)
352
+ state['previous_run_history'] = str(archived)
353
+ result['previous_run_history'] = str(archived)
354
+ write_json(path, state)
355
+ return result
356
+
357
+
358
+ def main():
359
+ parser = argparse.ArgumentParser(description=__doc__)
360
+ parser.add_argument('action', choices=['prepare', 'record-registration', 'poll',
361
+ 'ack-delivered', 'ack-accepted', 'closed'])
362
+ parser.add_argument('--home', default=os.environ.get('OMNILANE_HOME', str(Path.home() / '.omnilane')))
363
+ for name in ('thread-id', 'host-id', 'run-id'):
364
+ parser.add_argument('--' + name, required=True)
365
+ parser.add_argument('--workdir')
366
+ parser.add_argument('--job', action='append')
367
+ parser.add_argument('--automation-id')
368
+ parser.add_argument('--evidence')
369
+ parser.add_argument('--adoption-evidence')
370
+ parser.add_argument('--event-key')
371
+ parser.add_argument('--claim-token')
372
+ parser.add_argument('--close-reason', choices=['completed', 'stopped', 'expired'], default='completed')
373
+ parser.add_argument('--result', choices=['PASS', 'FAIL', 'PARTIAL', 'BLOCKED'])
374
+ parser.add_argument('--ttl-seconds', type=int, default=86400)
375
+ parser.add_argument('--interval-seconds', type=int, default=60)
376
+ parser.add_argument('--lease-seconds', type=int, default=900)
377
+ args = parser.parse_args()
378
+ try:
379
+ check(1 <= args.ttl_seconds <= 604800 and 30 <= args.interval_seconds <= 86400
380
+ and 1 <= args.lease_seconds <= 86400, 'invalid time limit')
381
+ print(json.dumps(act(args), ensure_ascii=False, sort_keys=True))
382
+ return 0
383
+ except (Invalid, OSError, ValueError, TypeError, KeyError) as error:
384
+ message = str(error) if isinstance(error, Invalid) else type(error).__name__
385
+ print(json.dumps({'error': message}), file=sys.stderr)
386
+ return 2
387
+
388
+
389
+ if __name__ == '__main__':
390
+ sys.exit(main())
@@ -149,11 +149,11 @@ esac
149
149
 
150
150
  # Native CLI catalogs are pinned from each installed CLI's live model surface.
151
151
  # Dynamic/API catalogs stay curated — "c" always accepts an exact model ID.
152
- CODEX_MODELS=("gpt-5.6" "gpt-5.6-sol" "gpt-5.6-terra" "gpt-5.6-luna" "gpt-5.5" "gpt-5.4" "gpt-5.4-mini" "gpt-5.3-codex-spark")
152
+ CODEX_MODELS=("gpt-6-astra" "gpt-5.6" "gpt-5.6-sol" "gpt-5.6-terra" "gpt-5.6-luna" "gpt-5.5" "gpt-5.4" "gpt-5.4-mini" "gpt-5.3-codex-spark")
153
153
  CODEX_EFFORTS=("xhigh" "max" "ultra" "high" "medium" "low" "minimal" "none")
154
154
  CLAUDE_MODELS=("default" "best" "fable" "opus" "sonnet" "haiku" "opus[1m]" "sonnet[1m]" "opusplan" "claude-fable-5" "claude-fable-5-1" "claude-opus-5" "claude-sonnet-5" "claude-opus-4-8" "claude-opus-4-7" "claude-opus-4-6" "claude-opus-4-5-20251101" "claude-sonnet-4-6" "claude-sonnet-4-5-20250929" "claude-haiku-4-5" "claude-haiku-4-5-20251001")
155
155
  CLAUDE_EFFORTS=("max" "xhigh" "high" "medium" "low" "-")
156
- GEMINI_MODELS=("gemini-3.7-flash-high" "gemini-3.7-flash-medium" "gemini-3.7-flash-low" "gemini-3.6-flash-high" "gemini-3.6-flash-medium" "gemini-3.6-flash-low" "gemini-3.1-pro-high" "gemini-3.1-pro-low" "claude-sonnet-4-6" "claude-opus-4-6-thinking" "gpt-oss-120b-medium")
156
+ GEMINI_MODELS=("gemini-3.8-flash-high" "gemini-3.8-flash-medium" "gemini-3.8-flash-low" "gemini-3.7-flash-high" "gemini-3.7-flash-medium" "gemini-3.7-flash-low" "gemini-3.6-flash-high" "gemini-3.6-flash-medium" "gemini-3.6-flash-low" "gemini-3.1-pro-high" "gemini-3.1-pro-low" "claude-sonnet-4-6" "claude-opus-4-6-thinking" "gpt-oss-120b-medium")
157
157
  GROK_MODELS=("grok-4.6" "headroom-grok-build" "grok-4.3-official")
158
158
  KIMI_MODELS=("kimi-k3" "kimi-k2.7-code" "kimi-k2.5")
159
159
  QWEN_MODELS=("qwen3.7-max" "qwen3.7-plus" "qwen3.6-plus" "qwen3.5-plus" "qwen3-max-2026-01-23" "qwen3-coder-next" "qwen3-coder-plus" "qwen3-coder-flash")
@@ -163,8 +163,8 @@ OPENROUTER_MODELS=("anthropic/claude-opus-5" "anthropic/claude-fable-5" "anthrop
163
163
  # Direct-API OpenAI-compatible vendors (curl + <VENDOR>_API_KEY); slugs are
164
164
  # suggestions — "c" free text covers anything each provider's /models lists.
165
165
  DEEPSEEK_MODELS=("deepseek-v4-pro" "deepseek-v4-flash")
166
- ZAI_MODELS=("glm-5.1" "glm-5" "glm-5-turbo" "glm-4.7" "glm-4.7-flashx" "glm-4.7-flash" "glm-4.6")
167
- MISTRAL_MODELS=("devstral-latest" "devstral-small-latest" "codestral-latest" "mistral-medium-latest" "mistral-large-latest")
166
+ ZAI_MODELS=("glm-5.3" "glm-5.1" "glm-5" "glm-5-turbo" "glm-4.7" "glm-4.7-flashx" "glm-4.7-flash" "glm-4.6")
167
+ MISTRAL_MODELS=("devstral-latest" "devstral-small-latest" "codestral-latest" "mistral-medium-3-5" "mistral-medium-latest" "mistral-large-latest")
168
168
  GROQ_MODELS=("groq/compound" "groq/compound-mini" "openai/gpt-oss-120b" "openai/gpt-oss-20b" "qwen/qwen3.6-27b" "llama-3.3-70b-versatile" "llama-3.1-8b-instant")
169
169
  CEREBRAS_MODELS=("zai-glm-4.7" "gpt-oss-120b" "qwen-3-235b-a22b-instruct-2507" "qwen-3-32b" "llama3.1-8b")
170
170