omnilane 0.8.3 → 0.9.0
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.
- package/CHANGELOG.md +50 -1
- package/README.ja.md +26 -6
- package/README.ko.md +25 -5
- package/README.md +40 -8
- package/README.zh-CN.md +19 -8
- package/README.zh-TW.md +31 -6
- package/VERSION +1 -1
- package/bin/omnilane +4 -3
- package/bin/omnilane-mcp +291 -0
- package/completions/omnilane.fish +50 -0
- package/package.json +1 -1
- package/scripts/check.sh +125 -0
- package/scripts/configure.sh +149 -1
- package/scripts/dispatch.sh +4 -4
- package/scripts/doctor.sh +38 -0
- package/scripts/jobs.sh +132 -10
- package/scripts/lib/common.sh +57 -3
- package/scripts/runners/run-cerebras.sh +7 -0
- package/scripts/runners/run-deepseek.sh +7 -0
- package/scripts/runners/run-groq.sh +7 -0
- package/scripts/runners/run-mistral.sh +7 -0
- package/scripts/runners/run-openai-compat.sh +102 -0
- package/scripts/runners/run-openrouter.sh +5 -84
- package/scripts/runners/run-zai.sh +7 -0
package/scripts/dispatch.sh
CHANGED
|
@@ -52,7 +52,7 @@ flags:
|
|
|
52
52
|
before any provider call or job state
|
|
53
53
|
--mode advise|work advise (read-only, default) or work (may edit files)
|
|
54
54
|
--workdir DIR working directory handed to the vendor CLI
|
|
55
|
-
--vendor V pin one configured vendor (codex|claude|grok|gemini|kimi|qwen|opencode|openrouter)
|
|
55
|
+
--vendor V pin one configured vendor (codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|deepseek|zai|mistral|groq|cerebras)
|
|
56
56
|
--model M override the routed model
|
|
57
57
|
--effort E override the routed effort
|
|
58
58
|
--timeout SECONDS cap each CLI call (default 600)
|
|
@@ -363,7 +363,7 @@ validate_routing() {
|
|
|
363
363
|
lane_invalid=1
|
|
364
364
|
break
|
|
365
365
|
fi
|
|
366
|
-
if ! [[ "$vendor" =~ ^(
|
|
366
|
+
if ! [[ "$vendor" =~ ^(${OMNILANE_VENDOR_ALT}|off)$ ]]; then
|
|
367
367
|
printf 'FAIL %s unknown-vendor=%s\n' "$lane" "$vendor"
|
|
368
368
|
lane_invalid=1
|
|
369
369
|
break
|
|
@@ -489,8 +489,8 @@ TASK="$2"
|
|
|
489
489
|
# A typo like --mode advice must not fall through to the write-enabled branch.
|
|
490
490
|
[[ "$MODE" == "advise" || "$MODE" == "work" ]] || { echo "omnilane: invalid --mode (advise|work)" >&2; exit 2; }
|
|
491
491
|
if [[ -n "$OVERRIDE_VENDOR" ]] &&
|
|
492
|
-
! [[ "$OVERRIDE_VENDOR" =~ ^(
|
|
493
|
-
echo "omnilane: invalid vendor (
|
|
492
|
+
! [[ "$OVERRIDE_VENDOR" =~ ^(${OMNILANE_OVERRIDE_VENDOR_ALT})$ ]]; then
|
|
493
|
+
echo "omnilane: invalid vendor (${OMNILANE_OVERRIDE_VENDOR_ALT})" >&2
|
|
494
494
|
exit 2
|
|
495
495
|
fi
|
|
496
496
|
|
package/scripts/doctor.sh
CHANGED
|
@@ -140,6 +140,44 @@ else
|
|
|
140
140
|
report FAIL watchdog "timeout, gtimeout, and perl are all unavailable"
|
|
141
141
|
fi
|
|
142
142
|
|
|
143
|
+
# Vendor CLI availability. Runners resolve each vendor's binary through a *_BIN
|
|
144
|
+
# override (default name otherwise) and source local.sh, so probe the same way
|
|
145
|
+
# in a subshell — set +u because a machine-local local.sh may reference its own
|
|
146
|
+
# unset vars, and the subshell keeps any side effects out of this report.
|
|
147
|
+
vendor_line="$(
|
|
148
|
+
set +u
|
|
149
|
+
[[ -f "$OMNILANE_HOME/local.sh" ]] && . "$OMNILANE_HOME/local.sh" 2>/dev/null
|
|
150
|
+
present=""; absent=""
|
|
151
|
+
for spec in "codex:${CODEX_BIN:-codex}" "claude:${CLAUDE_BIN:-claude}" \
|
|
152
|
+
"grok:${GROK_BIN:-grok}" "gemini:${AGY_BIN:-agy}" \
|
|
153
|
+
"kimi:${KIMI_BIN:-kimi}" "qwen:${QWEN_BIN:-qwen}" \
|
|
154
|
+
"opencode:${OPENCODE_BIN:-opencode}"; do
|
|
155
|
+
name="${spec%%:*}"; bin="${spec#*:}"
|
|
156
|
+
if command -v "$bin" >/dev/null 2>&1; then present="$present $name"; else absent="$absent $name"; fi
|
|
157
|
+
done
|
|
158
|
+
# Direct-API vendors (OpenAI-compatible, no CLI): reachable iff curl exists
|
|
159
|
+
# and the matching API key is set. Keep aligned with
|
|
160
|
+
# OMNILANE_DIRECT_API_VENDORS in lib/common.sh.
|
|
161
|
+
if command -v curl >/dev/null 2>&1; then
|
|
162
|
+
for spec in "openrouter:${OPENROUTER_API_KEY:-}" "deepseek:${DEEPSEEK_API_KEY:-}" \
|
|
163
|
+
"zai:${ZAI_API_KEY:-}" "mistral:${MISTRAL_API_KEY:-}" \
|
|
164
|
+
"groq:${GROQ_API_KEY:-}" "cerebras:${CEREBRAS_API_KEY:-}"; do
|
|
165
|
+
name="${spec%%:*}"; key="${spec#*:}"
|
|
166
|
+
if [[ -n "$key" ]]; then present="$present $name"; else absent="$absent $name"; fi
|
|
167
|
+
done
|
|
168
|
+
else
|
|
169
|
+
absent="$absent openrouter deepseek zai mistral groq cerebras"
|
|
170
|
+
fi
|
|
171
|
+
printf '%s|%s' "${present# }" "${absent# }"
|
|
172
|
+
)"
|
|
173
|
+
vendor_present="${vendor_line%%|*}"
|
|
174
|
+
vendor_absent="${vendor_line#*|}"
|
|
175
|
+
if [[ -n "$vendor_present" ]]; then
|
|
176
|
+
report PASS vendors "present: $vendor_present${vendor_absent:+; missing: $vendor_absent}"
|
|
177
|
+
else
|
|
178
|
+
report WARN vendors "no vendor CLI reachable${vendor_absent:+ (missing: $vendor_absent)}; every lane degrades to off"
|
|
179
|
+
fi
|
|
180
|
+
|
|
143
181
|
if command -v python3 >/dev/null 2>&1; then
|
|
144
182
|
if python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)' \
|
|
145
183
|
>/dev/null 2>&1; then
|
package/scripts/jobs.sh
CHANGED
|
@@ -48,7 +48,7 @@ die() {
|
|
|
48
48
|
exit "$rc"
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
USAGE_TEXT="usage: jobs.sh [--json] list|status ID|result ID|tail ID [--lines N]|retry ID [--background]|stats [--last N]|wait ID [--timeout N]|audit [--last N]|prune [--keep N] [--older-than DAYS] [--apply]|help"
|
|
51
|
+
USAGE_TEXT="usage: jobs.sh [--json] list [--lane L] [--vendor V] [--status running|done]|status ID|result ID|tail ID [--lines N]|retry ID [--background]|stats [--last N] [--lane L] [--vendor V]|wait ID [--timeout N]|cancel ID|rm ID|audit [--last N]|prune [--keep N] [--older-than DAYS] [--apply]|help"
|
|
52
52
|
|
|
53
53
|
usage() {
|
|
54
54
|
die 2 "$USAGE_TEXT"
|
|
@@ -132,7 +132,7 @@ read_job_pid() {
|
|
|
132
132
|
|
|
133
133
|
parse_stats_metadata() {
|
|
134
134
|
local value="$1"
|
|
135
|
-
local metadata_re='^\{"lane":"([a-z][a-z0-9-]*)","vendor":"(
|
|
135
|
+
local metadata_re='^\{"lane":"([a-z][a-z0-9-]*)","vendor":"('"$OMNILANE_VENDOR_ALT"')"(,|})'
|
|
136
136
|
STATS_LANE=""
|
|
137
137
|
STATS_VENDOR=""
|
|
138
138
|
[[ "$value" =~ $metadata_re ]] || return 1
|
|
@@ -143,7 +143,7 @@ parse_stats_metadata() {
|
|
|
143
143
|
parse_audit_metadata() {
|
|
144
144
|
local value="$1" metadata_re
|
|
145
145
|
local json_string='([^"\\]|\\["\\/bfnrt]|\\u[0-9a-fA-F]{4})*'
|
|
146
|
-
metadata_re="^\\{\"lane\":\"[a-z][a-z0-9-]*\",\"vendor\":\"(
|
|
146
|
+
metadata_re="^\\{\"lane\":\"[a-z][a-z0-9-]*\",\"vendor\":\"(${OMNILANE_VENDOR_ALT})\",\"model\":\"${json_string}\",\"effort\":\"${json_string}\",\"timeout\":(0|[1-9][0-9]*),\"job_timeout\":(null|0|[1-9][0-9]*),\"mode\":\"(advise|work)\",\"workdir\":\"${json_string}\",\"candidate\":\"[1-9][0-9]*/[1-9][0-9]*\",\"started\":\"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\"\\}$"
|
|
147
147
|
[[ "$value" =~ $metadata_re ]]
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -252,7 +252,7 @@ case "${1:-}" in
|
|
|
252
252
|
read_public_metadata "$JOB_DIR/meta.json" || {
|
|
253
253
|
die 1 "cannot retry: unreadable job metadata"
|
|
254
254
|
}
|
|
255
|
-
retry_re='^\{"lane":"([a-z][a-z0-9-]*)","vendor":"(
|
|
255
|
+
retry_re='^\{"lane":"([a-z][a-z0-9-]*)","vendor":"('"$OMNILANE_VENDOR_ALT"')","model":"([^"\\]*)","effort":"([^"\\]*)","timeout":([1-9][0-9]*),"job_timeout":([1-9][0-9]*|null),"mode":"(advise|work)","workdir":"([^"\\]*)",'
|
|
256
256
|
[[ "$PUBLIC_METADATA" =~ $retry_re ]] || {
|
|
257
257
|
die 1 "cannot retry: job metadata is not safely parseable"
|
|
258
258
|
}
|
|
@@ -420,18 +420,27 @@ case "${1:-}" in
|
|
|
420
420
|
[[ "$findings" -eq 0 ]] || exit 1 ;;
|
|
421
421
|
stats)
|
|
422
422
|
limit=100
|
|
423
|
+
filter_lane=""; filter_vendor=""
|
|
423
424
|
shift
|
|
424
425
|
while [[ $# -gt 0 ]]; do
|
|
425
426
|
case "$1" in
|
|
426
427
|
--last)
|
|
427
428
|
[[ $# -ge 2 ]] || usage
|
|
428
429
|
limit="$2"; shift 2 ;;
|
|
430
|
+
--lane)
|
|
431
|
+
[[ $# -ge 2 ]] || usage
|
|
432
|
+
filter_lane="$2"; shift 2 ;;
|
|
433
|
+
--vendor)
|
|
434
|
+
[[ $# -ge 2 ]] || usage
|
|
435
|
+
filter_vendor="$2"; shift 2 ;;
|
|
429
436
|
*) usage ;;
|
|
430
437
|
esac
|
|
431
438
|
done
|
|
432
439
|
[[ "$limit" =~ ^[1-9][0-9]{0,3}$ && "$limit" -le 10000 ]] || {
|
|
433
440
|
die 2 "invalid --last value (want 1..10000)"
|
|
434
441
|
}
|
|
442
|
+
[[ -z "$filter_lane" || "$filter_lane" =~ ^[a-z][a-z0-9-]*$ ]] || die 2 "invalid --lane value"
|
|
443
|
+
[[ -z "$filter_vendor" ]] || omnilane_known_vendor "$filter_vendor" || die 2 "invalid --vendor value"
|
|
435
444
|
sampled=0
|
|
436
445
|
succeeded=0
|
|
437
446
|
failed=0
|
|
@@ -451,8 +460,21 @@ case "${1:-}" in
|
|
|
451
460
|
fi
|
|
452
461
|
if [[ ${#ids[@]} -gt 0 ]]; then
|
|
453
462
|
while IFS= read -r id; do
|
|
454
|
-
sampled=$((sampled + 1))
|
|
455
463
|
job_dir="$JOBS/$id"
|
|
464
|
+
# Resolve lane/vendor first so a --lane/--vendor filter can exclude a job
|
|
465
|
+
# before it counts toward any aggregate.
|
|
466
|
+
meta_ok=0
|
|
467
|
+
metadata_path="$job_dir/meta.json"
|
|
468
|
+
if read_public_metadata "$metadata_path" &&
|
|
469
|
+
parse_stats_metadata "$PUBLIC_METADATA"; then
|
|
470
|
+
meta_ok=1
|
|
471
|
+
fi
|
|
472
|
+
if [[ -n "$filter_lane" || -n "$filter_vendor" ]]; then
|
|
473
|
+
[[ "$meta_ok" -eq 1 ]] || continue
|
|
474
|
+
[[ -z "$filter_lane" || "$filter_lane" == "$STATS_LANE" ]] || continue
|
|
475
|
+
[[ -z "$filter_vendor" || "$filter_vendor" == "$STATS_VENDOR" ]] || continue
|
|
476
|
+
fi
|
|
477
|
+
sampled=$((sampled + 1))
|
|
456
478
|
exit_path="$job_dir/exit"
|
|
457
479
|
if [[ -e "$exit_path" || -L "$exit_path" ]]; then
|
|
458
480
|
if read_exit_code "$exit_path"; then
|
|
@@ -467,9 +489,7 @@ case "${1:-}" in
|
|
|
467
489
|
else
|
|
468
490
|
running=$((running + 1))
|
|
469
491
|
fi
|
|
470
|
-
|
|
471
|
-
if read_public_metadata "$metadata_path" &&
|
|
472
|
-
parse_stats_metadata "$PUBLIC_METADATA"; then
|
|
492
|
+
if [[ "$meta_ok" -eq 1 ]]; then
|
|
473
493
|
lanes+=("$STATS_LANE")
|
|
474
494
|
vendors+=("$STATS_VENDOR")
|
|
475
495
|
else
|
|
@@ -501,7 +521,22 @@ case "${1:-}" in
|
|
|
501
521
|
fi
|
|
502
522
|
fi ;;
|
|
503
523
|
list)
|
|
504
|
-
|
|
524
|
+
filter_lane=""; filter_vendor=""; filter_status=""
|
|
525
|
+
shift
|
|
526
|
+
while [[ $# -gt 0 ]]; do
|
|
527
|
+
case "$1" in
|
|
528
|
+
--lane) [[ $# -ge 2 ]] || usage; filter_lane="$2"; shift 2 ;;
|
|
529
|
+
--vendor) [[ $# -ge 2 ]] || usage; filter_vendor="$2"; shift 2 ;;
|
|
530
|
+
--status) [[ $# -ge 2 ]] || usage; filter_status="$2"; shift 2 ;;
|
|
531
|
+
*) usage ;;
|
|
532
|
+
esac
|
|
533
|
+
done
|
|
534
|
+
[[ -z "$filter_lane" || "$filter_lane" =~ ^[a-z][a-z0-9-]*$ ]] || die 2 "invalid --lane value"
|
|
535
|
+
[[ -z "$filter_vendor" ]] || omnilane_known_vendor "$filter_vendor" || die 2 "invalid --vendor value"
|
|
536
|
+
case "$filter_status" in
|
|
537
|
+
""|running|done) ;;
|
|
538
|
+
*) die 2 "invalid --status value (want running or done)" ;;
|
|
539
|
+
esac
|
|
505
540
|
if [[ ! -d "$JOBS" ]]; then
|
|
506
541
|
[[ "$JSON_MODE" -eq 0 ]] || printf '{"schema_version":1,"command":"list","ok":true,"jobs":[]}\n'
|
|
507
542
|
exit 0
|
|
@@ -519,6 +554,7 @@ case "${1:-}" in
|
|
|
519
554
|
fi
|
|
520
555
|
[[ "$JSON_MODE" -eq 0 ]] || printf '{"schema_version":1,"command":"list","ok":true,"jobs":['
|
|
521
556
|
json_first=1
|
|
557
|
+
shown=0
|
|
522
558
|
while IFS= read -r id; do
|
|
523
559
|
state="running"
|
|
524
560
|
exit_json="null"
|
|
@@ -555,6 +591,21 @@ case "${1:-}" in
|
|
|
555
591
|
elif [[ -e "$metadata_path" || -L "$metadata_path" ]]; then
|
|
556
592
|
metadata_status="invalid"
|
|
557
593
|
fi
|
|
594
|
+
if [[ -n "$filter_status" ]]; then
|
|
595
|
+
case "$json_state" in
|
|
596
|
+
done) [[ "$filter_status" == "done" ]] || continue ;;
|
|
597
|
+
running) [[ "$filter_status" == "running" ]] || continue ;;
|
|
598
|
+
*) continue ;;
|
|
599
|
+
esac
|
|
600
|
+
fi
|
|
601
|
+
if [[ -n "$filter_lane" || -n "$filter_vendor" ]]; then
|
|
602
|
+
if [[ "$metadata_status" == "valid" ]] && parse_stats_metadata "$metadata"; then
|
|
603
|
+
[[ -z "$filter_lane" || "$filter_lane" == "$STATS_LANE" ]] || continue
|
|
604
|
+
[[ -z "$filter_vendor" || "$filter_vendor" == "$STATS_VENDOR" ]] || continue
|
|
605
|
+
else
|
|
606
|
+
continue
|
|
607
|
+
fi
|
|
608
|
+
fi
|
|
558
609
|
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
559
610
|
[[ "$json_first" -eq 1 ]] || printf ','
|
|
560
611
|
json_first=0
|
|
@@ -563,7 +614,9 @@ case "${1:-}" in
|
|
|
563
614
|
else
|
|
564
615
|
printf '%s %s %s\n' "$id" "$state" "$metadata"
|
|
565
616
|
fi
|
|
566
|
-
|
|
617
|
+
shown=$((shown + 1))
|
|
618
|
+
if [[ "$shown" -ge 20 ]]; then break; fi
|
|
619
|
+
done < <(printf '%s\n' "${ids[@]}" | LC_ALL=C sort -r)
|
|
567
620
|
[[ "$JSON_MODE" -eq 0 ]] || printf ']}\n' ;;
|
|
568
621
|
status)
|
|
569
622
|
[[ $# -eq 2 ]] || usage
|
|
@@ -672,6 +725,75 @@ case "${1:-}" in
|
|
|
672
725
|
fi
|
|
673
726
|
sleep 1
|
|
674
727
|
done ;;
|
|
728
|
+
cancel)
|
|
729
|
+
[[ "$JSON_MODE" -eq 0 && $# -eq 2 ]] || usage
|
|
730
|
+
select_job "$2"
|
|
731
|
+
exit_path="$JOB_DIR/exit"
|
|
732
|
+
if [[ -e "$exit_path" || -L "$exit_path" ]]; then
|
|
733
|
+
if read_exit_code "$exit_path"; then
|
|
734
|
+
echo "already finished (exit $RECORDED_EXIT)"
|
|
735
|
+
else
|
|
736
|
+
echo "already finished (invalid exit metadata)"
|
|
737
|
+
fi
|
|
738
|
+
exit 0
|
|
739
|
+
fi
|
|
740
|
+
pid=""; pid_state="missing"
|
|
741
|
+
if [[ -e "$JOB_DIR/pid" || -L "$JOB_DIR/pid" ]]; then
|
|
742
|
+
if read_job_pid "$JOB_DIR/pid"; then pid="$RECORDED_PID"; pid_state="valid"; else pid_state="invalid"; fi
|
|
743
|
+
fi
|
|
744
|
+
if [[ "$pid_state" != "valid" ]] || ! kill -0 "$pid" 2>/dev/null; then
|
|
745
|
+
echo "not running (no live worker to cancel)"
|
|
746
|
+
exit 0
|
|
747
|
+
fi
|
|
748
|
+
# Background workers run as their own process-group leader (dispatch `set -m`),
|
|
749
|
+
# and that leader traps SIGTERM to record a best-effort exit code. Signal the
|
|
750
|
+
# whole group so the vendor CLI child dies too and the worker writes its exit.
|
|
751
|
+
cancel_grp="$pid"
|
|
752
|
+
cancel_pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d '[:space:]' || true)"
|
|
753
|
+
[[ "$cancel_pgid" =~ ^[1-9][0-9]*$ ]] && cancel_grp="$cancel_pgid"
|
|
754
|
+
kill -TERM "-$cancel_grp" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
|
|
755
|
+
cancel_waited=0
|
|
756
|
+
while [[ "$cancel_waited" -lt 5 ]]; do
|
|
757
|
+
if [[ -e "$exit_path" || -L "$exit_path" ]]; then break; fi
|
|
758
|
+
if ! kill -0 "$pid" 2>/dev/null; then break; fi
|
|
759
|
+
sleep 1; cancel_waited=$((cancel_waited + 1))
|
|
760
|
+
done
|
|
761
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
762
|
+
# The TERM trap did not fire in time; SIGKILL cannot be trapped, so no exit
|
|
763
|
+
# gets recorded — force the group down and record the terminal state below.
|
|
764
|
+
kill -KILL "-$cancel_grp" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
|
|
765
|
+
sleep 1
|
|
766
|
+
fi
|
|
767
|
+
if [[ -e "$exit_path" || -L "$exit_path" ]]; then
|
|
768
|
+
if read_exit_code "$exit_path"; then
|
|
769
|
+
echo "cancelled (exit $RECORDED_EXIT)"
|
|
770
|
+
else
|
|
771
|
+
echo "cancelled (invalid exit metadata)"
|
|
772
|
+
fi
|
|
773
|
+
elif kill -0 "$pid" 2>/dev/null; then
|
|
774
|
+
die 1 "cancel could not terminate worker pid $pid"
|
|
775
|
+
else
|
|
776
|
+
(umask 077; printf '137\n' > "$exit_path")
|
|
777
|
+
echo "cancelled (exit 137)"
|
|
778
|
+
fi
|
|
779
|
+
exit 0 ;;
|
|
780
|
+
rm)
|
|
781
|
+
[[ "$JSON_MODE" -eq 0 && $# -eq 2 ]] || usage
|
|
782
|
+
select_job "$2"
|
|
783
|
+
exit_path="$JOB_DIR/exit"
|
|
784
|
+
# Refuse to delete a job whose worker is still alive: removing the directory
|
|
785
|
+
# out from under a running worker would strand it writing into a deleted path.
|
|
786
|
+
# A recorded exit (finished) or a dead pid (crashed worker) is safe to remove.
|
|
787
|
+
if [[ ! -e "$exit_path" && ! -L "$exit_path" ]]; then
|
|
788
|
+
if [[ -e "$JOB_DIR/pid" || -L "$JOB_DIR/pid" ]] &&
|
|
789
|
+
read_job_pid "$JOB_DIR/pid" && kill -0 "$RECORDED_PID" 2>/dev/null; then
|
|
790
|
+
die 1 "job is running (pid $RECORDED_PID); cancel it first"
|
|
791
|
+
fi
|
|
792
|
+
fi
|
|
793
|
+
# select_job already proved $JOB_DIR is a real child dir, never a symlink.
|
|
794
|
+
/bin/rm -rf "$JOB_DIR"
|
|
795
|
+
echo "removed $2"
|
|
796
|
+
exit 0 ;;
|
|
675
797
|
result)
|
|
676
798
|
[[ $# -eq 2 ]] || usage
|
|
677
799
|
select_job "$2"
|
package/scripts/lib/common.sh
CHANGED
|
@@ -64,6 +64,57 @@ write_current_pid_file() {
|
|
|
64
64
|
mv "$tmp" "$path"
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
# ---- Vendor registry (single source of truth) ---------------------------
|
|
68
|
+
# Two vendor kinds. CLI-agent vendors run a local binary (see vendor_bin).
|
|
69
|
+
# Direct-API vendors are OpenAI-compatible /chat/completions endpoints driven
|
|
70
|
+
# by run-openai-compat.sh (curl + an API key, no CLI). To add an
|
|
71
|
+
# OpenAI-compatible vendor: add its name to OMNILANE_DIRECT_API_VENDORS, add a
|
|
72
|
+
# vendor_api_spec row, add a runners/run-<name>.sh wrapper, and a model row in
|
|
73
|
+
# configure.sh. Every validation site derives from these lists.
|
|
74
|
+
OMNILANE_CLI_VENDORS="codex claude grok gemini kimi qwen opencode"
|
|
75
|
+
OMNILANE_DIRECT_API_VENDORS="openrouter deepseek zai mistral groq cerebras"
|
|
76
|
+
|
|
77
|
+
# Pipe-joined vendor alternations for =~ checks and JSON metadata regexes.
|
|
78
|
+
# _OVERRIDE covers what --vendor may pin (no exec/off); _ALT adds the "exec"
|
|
79
|
+
# bring-your-own gate. Built from the lists above — the single source of truth.
|
|
80
|
+
OMNILANE_OVERRIDE_VENDOR_ALT=""
|
|
81
|
+
for _omnilane_v in $OMNILANE_CLI_VENDORS $OMNILANE_DIRECT_API_VENDORS; do
|
|
82
|
+
OMNILANE_OVERRIDE_VENDOR_ALT="${OMNILANE_OVERRIDE_VENDOR_ALT:+$OMNILANE_OVERRIDE_VENDOR_ALT|}$_omnilane_v"
|
|
83
|
+
done
|
|
84
|
+
unset _omnilane_v
|
|
85
|
+
# shellcheck disable=SC2034 # consumed by jobs.sh/dispatch.sh which source this file
|
|
86
|
+
OMNILANE_VENDOR_ALT="$OMNILANE_OVERRIDE_VENDOR_ALT|exec"
|
|
87
|
+
|
|
88
|
+
# Direct-API vendor -> "BASE_URL_DEFAULT|KEY_ENV|MODEL_HINT". Base URL is
|
|
89
|
+
# overridable per vendor via <VENDOR>_BASE_URL (e.g. DEEPSEEK_BASE_URL).
|
|
90
|
+
vendor_api_spec() {
|
|
91
|
+
case "$1" in
|
|
92
|
+
openrouter) echo "https://openrouter.ai/api/v1|OPENROUTER_API_KEY|anthropic/claude-sonnet-4" ;;
|
|
93
|
+
deepseek) echo "https://api.deepseek.com|DEEPSEEK_API_KEY|deepseek-chat" ;;
|
|
94
|
+
zai) echo "https://api.z.ai/api/openai/v1|ZAI_API_KEY|glm-4.6" ;;
|
|
95
|
+
mistral) echo "https://api.mistral.ai/v1|MISTRAL_API_KEY|devstral-latest" ;;
|
|
96
|
+
groq) echo "https://api.groq.com/openai/v1|GROQ_API_KEY|qwen/qwen3.6-27b" ;;
|
|
97
|
+
cerebras) echo "https://api.cerebras.ai/v1|CEREBRAS_API_KEY|qwen-3-32b" ;;
|
|
98
|
+
*) echo "" ;;
|
|
99
|
+
esac
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
vendor_is_direct_api() { # 0 if vendor is an OpenAI-compatible direct-API vendor
|
|
103
|
+
local _v
|
|
104
|
+
# Exact token match — a substring `case` would accept adjacent-pair strings
|
|
105
|
+
# like "openrouter deepseek".
|
|
106
|
+
for _v in $OMNILANE_DIRECT_API_VENDORS; do [[ "$1" == "$_v" ]] && return 0; done
|
|
107
|
+
return 1
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
omnilane_known_vendor() { # 0 if vendor is dispatchable (CLI, direct-API, or exec)
|
|
111
|
+
local _v
|
|
112
|
+
for _v in $OMNILANE_CLI_VENDORS $OMNILANE_DIRECT_API_VENDORS exec; do
|
|
113
|
+
[[ "$1" == "$_v" ]] && return 0
|
|
114
|
+
done
|
|
115
|
+
return 1
|
|
116
|
+
}
|
|
117
|
+
|
|
67
118
|
vendor_bin() { # vendor -> binary (honors local.sh overrides)
|
|
68
119
|
case "$1" in
|
|
69
120
|
codex) echo "${CODEX_BIN:-codex}" ;;
|
|
@@ -82,9 +133,12 @@ vendor_available() {
|
|
|
82
133
|
# checked at dispatch time (the chain resolver cannot see it here).
|
|
83
134
|
# "vote" is the built-in multi-model panel: needs >=2 voters, checked at dispatch.
|
|
84
135
|
[[ "$1" == "exec" || "$1" == "vote" ]] && return 0
|
|
85
|
-
#
|
|
86
|
-
if
|
|
87
|
-
|
|
136
|
+
# Direct-API vendors have no CLI: available iff curl and their API key exist.
|
|
137
|
+
if vendor_is_direct_api "$1"; then
|
|
138
|
+
local spec key_env
|
|
139
|
+
spec="$(vendor_api_spec "$1")"
|
|
140
|
+
key_env="${spec#*|}"; key_env="${key_env%%|*}"
|
|
141
|
+
command -v curl >/dev/null 2>&1 && [[ -n "${!key_env:-}" ]]
|
|
88
142
|
return
|
|
89
143
|
fi
|
|
90
144
|
local b; b="$(vendor_bin "$1")"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# omnilane runner: Cerebras (OpenAI-compatible direct API). Thin wrapper over
|
|
4
|
+
# run-openai-compat.sh; endpoint and default model live in vendor_api_spec.
|
|
5
|
+
# Override the endpoint with CEREBRAS_BASE_URL; needs CEREBRAS_API_KEY.
|
|
6
|
+
export OMNILANE_OAI_VENDOR=cerebras
|
|
7
|
+
exec "$(dirname "${BASH_SOURCE[0]}")/run-openai-compat.sh" "$@"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# omnilane runner: DeepSeek (OpenAI-compatible direct API). Thin wrapper over
|
|
4
|
+
# run-openai-compat.sh; endpoint and default model live in vendor_api_spec.
|
|
5
|
+
# Override the endpoint with DEEPSEEK_BASE_URL; needs DEEPSEEK_API_KEY.
|
|
6
|
+
export OMNILANE_OAI_VENDOR=deepseek
|
|
7
|
+
exec "$(dirname "${BASH_SOURCE[0]}")/run-openai-compat.sh" "$@"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# omnilane runner: Groq (OpenAI-compatible direct API). Thin wrapper over
|
|
4
|
+
# run-openai-compat.sh; endpoint and default model live in vendor_api_spec.
|
|
5
|
+
# Override the endpoint with GROQ_BASE_URL; needs GROQ_API_KEY.
|
|
6
|
+
export OMNILANE_OAI_VENDOR=groq
|
|
7
|
+
exec "$(dirname "${BASH_SOURCE[0]}")/run-openai-compat.sh" "$@"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# omnilane runner: Mistral (OpenAI-compatible direct API). Thin wrapper over
|
|
4
|
+
# run-openai-compat.sh; endpoint and default model live in vendor_api_spec.
|
|
5
|
+
# Override the endpoint with MISTRAL_BASE_URL; needs MISTRAL_API_KEY.
|
|
6
|
+
export OMNILANE_OAI_VENDOR=mistral
|
|
7
|
+
exec "$(dirname "${BASH_SOURCE[0]}")/run-openai-compat.sh" "$@"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# omnilane runner: any OpenAI-compatible /chat/completions endpoint.
|
|
5
|
+
#
|
|
6
|
+
# Usage: run-openai-compat.sh MODE WORKDIR MODEL EFFORT PROMPT_FILE OUTPUT_FILE
|
|
7
|
+
# Driven by run-<vendor>.sh wrappers that export OMNILANE_OAI_VENDOR. Like the
|
|
8
|
+
# openrouter runner this is pure inference over HTTPS — no agentic loop, so
|
|
9
|
+
# work mode is a hard error, not a silent read-only downgrade.
|
|
10
|
+
#
|
|
11
|
+
# Base URL, API-key env var, and a model hint come from vendor_api_spec. The
|
|
12
|
+
# base URL is overridable per vendor via <VENDOR>_BASE_URL. EFFORT is accepted
|
|
13
|
+
# for interface parity only. Needs curl and the vendor's API key; python3
|
|
14
|
+
# builds/parses the JSON so prompt content never needs shell escaping.
|
|
15
|
+
|
|
16
|
+
source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
|
|
17
|
+
|
|
18
|
+
MODE="$1"; WORKDIR="$2"; MODEL="$3"; EFFORT="$4"; PROMPT_FILE="$5"; OUTPUT_FILE="$6"
|
|
19
|
+
: "$WORKDIR" "$EFFORT" # parity with the uniform runner interface
|
|
20
|
+
|
|
21
|
+
fail() { echo "omnilane: $1" > "${OUTPUT_FILE}.stderr.log"; exit 2; }
|
|
22
|
+
|
|
23
|
+
VENDOR="${OMNILANE_OAI_VENDOR:?run-openai-compat.sh must be invoked via a run-<vendor>.sh wrapper}"
|
|
24
|
+
SPEC="$(vendor_api_spec "$VENDOR")"
|
|
25
|
+
[[ -n "$SPEC" ]] || fail "unknown OpenAI-compatible vendor '$VENDOR'"
|
|
26
|
+
BASE_DEFAULT="${SPEC%%|*}"
|
|
27
|
+
MODEL_HINT="${SPEC##*|}"
|
|
28
|
+
KEY_ENV="${SPEC#*|}"; KEY_ENV="${KEY_ENV%%|*}"
|
|
29
|
+
|
|
30
|
+
VENDOR_UPPER="$(printf '%s' "$VENDOR" | tr '[:lower:]' '[:upper:]')"
|
|
31
|
+
BASE_OVERRIDE_VAR="${VENDOR_UPPER}_BASE_URL"
|
|
32
|
+
BASE_URL="${!BASE_OVERRIDE_VAR:-$BASE_DEFAULT}"
|
|
33
|
+
API_KEY="${!KEY_ENV:-}"
|
|
34
|
+
RUN_TIMEOUT="${OMNILANE_TIMEOUT:-600}"
|
|
35
|
+
|
|
36
|
+
[[ "$MODE" == "advise" ]] || fail "$VENDOR is inference-only: use --mode advise (work mode needs an agentic CLI vendor like opencode)"
|
|
37
|
+
[[ -n "$API_KEY" ]] || fail "$KEY_ENV is not set"
|
|
38
|
+
[[ -n "$MODEL" && "$MODEL" != "-" ]] || fail "$VENDOR needs an explicit model slug (e.g. $MODEL_HINT)"
|
|
39
|
+
command -v curl >/dev/null 2>&1 || fail "curl not found"
|
|
40
|
+
command -v python3 >/dev/null 2>&1 || fail "python3 not found"
|
|
41
|
+
|
|
42
|
+
truncate_payload "$PROMPT_FILE" 140000
|
|
43
|
+
|
|
44
|
+
REQUEST_FILE="${OUTPUT_FILE}.request.json"
|
|
45
|
+
RESPONSE_FILE="${OUTPUT_FILE}.response.json"
|
|
46
|
+
# Keep the API key out of the process argument list (ps-visible): curl reads the
|
|
47
|
+
# Authorization header from a 0600 file, not a -H command-line flag.
|
|
48
|
+
HEADER_FILE="${OUTPUT_FILE}.headers"
|
|
49
|
+
cleanup() { rm "$REQUEST_FILE" "$RESPONSE_FILE" "$HEADER_FILE" 2>/dev/null || true; }
|
|
50
|
+
trap cleanup EXIT
|
|
51
|
+
( umask 077; printf 'Authorization: Bearer %s\n' "$API_KEY" > "$HEADER_FILE" )
|
|
52
|
+
|
|
53
|
+
python3 - "$MODEL" "$PROMPT_FILE" > "$REQUEST_FILE" <<'PY'
|
|
54
|
+
import json, sys
|
|
55
|
+
model, prompt_file = sys.argv[1], sys.argv[2]
|
|
56
|
+
with open(prompt_file, encoding="utf-8", errors="replace") as f:
|
|
57
|
+
prompt = f.read()
|
|
58
|
+
json.dump({"model": model, "messages": [{"role": "user", "content": prompt}]},
|
|
59
|
+
sys.stdout)
|
|
60
|
+
PY
|
|
61
|
+
|
|
62
|
+
set +e
|
|
63
|
+
run_with_timeout "$RUN_TIMEOUT" curl -sS --fail-with-body \
|
|
64
|
+
--max-time "$RUN_TIMEOUT" \
|
|
65
|
+
-H "@$HEADER_FILE" \
|
|
66
|
+
-H "Content-Type: application/json" \
|
|
67
|
+
-X POST "$BASE_URL/chat/completions" \
|
|
68
|
+
--data-binary "@$REQUEST_FILE" \
|
|
69
|
+
> "$RESPONSE_FILE" 2> "${OUTPUT_FILE}.stderr.log"
|
|
70
|
+
RC=$?
|
|
71
|
+
set -e
|
|
72
|
+
|
|
73
|
+
if [[ "$RC" -ne 0 ]]; then
|
|
74
|
+
# Surface the API error body (never the key) alongside curl's own stderr.
|
|
75
|
+
[[ -s "$RESPONSE_FILE" ]] && cat "$RESPONSE_FILE" >> "${OUTPUT_FILE}.stderr.log"
|
|
76
|
+
exit "$RC"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
set +e
|
|
80
|
+
python3 - "$VENDOR" "$RESPONSE_FILE" > "${OUTPUT_FILE}.tmp" 2>> "${OUTPUT_FILE}.stderr.log" <<'PY'
|
|
81
|
+
import json, sys
|
|
82
|
+
vendor = sys.argv[1]
|
|
83
|
+
with open(sys.argv[2], encoding="utf-8", errors="replace") as f:
|
|
84
|
+
data = json.load(f)
|
|
85
|
+
if "error" in data:
|
|
86
|
+
print(f"omnilane: {vendor} API error: {data['error']}", file=sys.stderr)
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
content = data["choices"][0]["message"]["content"]
|
|
89
|
+
sys.stdout.write(content if isinstance(content, str) else json.dumps(content))
|
|
90
|
+
PY
|
|
91
|
+
RC=$?
|
|
92
|
+
set -e
|
|
93
|
+
|
|
94
|
+
# Empty output is a failure, not a silent rc=0 success.
|
|
95
|
+
if ! grep -q '[^[:space:]]' "${OUTPUT_FILE}.tmp" 2>/dev/null; then
|
|
96
|
+
echo "omnilane: $VENDOR produced no output" >> "${OUTPUT_FILE}.stderr.log"
|
|
97
|
+
if [[ "$RC" -eq 0 ]]; then RC=1; fi
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
[[ -f "${OUTPUT_FILE}.tmp" ]] && mv "${OUTPUT_FILE}.tmp" "$OUTPUT_FILE"
|
|
101
|
+
[[ -s "${OUTPUT_FILE}.stderr.log" ]] || rm "${OUTPUT_FILE}.stderr.log" 2>/dev/null || true
|
|
102
|
+
exit "$RC"
|
|
@@ -1,86 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
# agentic loop and cannot edit files, so work mode is a hard error rather
|
|
9
|
-
# than a silent read-only downgrade.
|
|
10
|
-
#
|
|
11
|
-
# MODEL is an OpenRouter model slug (e.g. anthropic/claude-sonnet-4). EFFORT
|
|
12
|
-
# is accepted for interface parity only. Needs OPENROUTER_API_KEY and curl;
|
|
13
|
-
# python3 builds/parses the JSON so prompt content never needs shell escaping.
|
|
14
|
-
|
|
15
|
-
source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
|
|
16
|
-
|
|
17
|
-
MODE="$1"; WORKDIR="$2"; MODEL="$3"; EFFORT="$4"; PROMPT_FILE="$5"; OUTPUT_FILE="$6"
|
|
18
|
-
: "$WORKDIR" "$EFFORT" # parity with the uniform runner interface
|
|
19
|
-
|
|
20
|
-
OPENROUTER_BASE_URL="${OPENROUTER_BASE_URL:-https://openrouter.ai/api/v1}"
|
|
21
|
-
RUN_TIMEOUT="${OMNILANE_TIMEOUT:-600}"
|
|
22
|
-
|
|
23
|
-
fail() { echo "omnilane: $1" > "${OUTPUT_FILE}.stderr.log"; exit 2; }
|
|
24
|
-
|
|
25
|
-
[[ "$MODE" == "advise" ]] || fail "openrouter is inference-only: use --mode advise (work mode needs an agentic CLI vendor like opencode)"
|
|
26
|
-
[[ -n "${OPENROUTER_API_KEY:-}" ]] || fail "OPENROUTER_API_KEY is not set"
|
|
27
|
-
[[ -n "$MODEL" && "$MODEL" != "-" ]] || fail "openrouter needs an explicit model slug (e.g. anthropic/claude-sonnet-4)"
|
|
28
|
-
command -v curl >/dev/null 2>&1 || fail "curl not found"
|
|
29
|
-
command -v python3 >/dev/null 2>&1 || fail "python3 not found"
|
|
30
|
-
|
|
31
|
-
truncate_payload "$PROMPT_FILE" 140000
|
|
32
|
-
|
|
33
|
-
REQUEST_FILE="${OUTPUT_FILE}.request.json"
|
|
34
|
-
RESPONSE_FILE="${OUTPUT_FILE}.response.json"
|
|
35
|
-
cleanup() { rm "$REQUEST_FILE" "$RESPONSE_FILE" 2>/dev/null || true; }
|
|
36
|
-
trap cleanup EXIT
|
|
37
|
-
|
|
38
|
-
python3 - "$MODEL" "$PROMPT_FILE" > "$REQUEST_FILE" <<'PY'
|
|
39
|
-
import json, sys
|
|
40
|
-
model, prompt_file = sys.argv[1], sys.argv[2]
|
|
41
|
-
with open(prompt_file, encoding="utf-8", errors="replace") as f:
|
|
42
|
-
prompt = f.read()
|
|
43
|
-
json.dump({"model": model, "messages": [{"role": "user", "content": prompt}]},
|
|
44
|
-
sys.stdout)
|
|
45
|
-
PY
|
|
46
|
-
|
|
47
|
-
set +e
|
|
48
|
-
run_with_timeout "$RUN_TIMEOUT" curl -sS --fail-with-body \
|
|
49
|
-
--max-time "$RUN_TIMEOUT" \
|
|
50
|
-
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
|
|
51
|
-
-H "Content-Type: application/json" \
|
|
52
|
-
-X POST "$OPENROUTER_BASE_URL/chat/completions" \
|
|
53
|
-
--data-binary "@$REQUEST_FILE" \
|
|
54
|
-
> "$RESPONSE_FILE" 2> "${OUTPUT_FILE}.stderr.log"
|
|
55
|
-
RC=$?
|
|
56
|
-
set -e
|
|
57
|
-
|
|
58
|
-
if [[ "$RC" -ne 0 ]]; then
|
|
59
|
-
# Surface the API error body (never the key) alongside curl's own stderr.
|
|
60
|
-
[[ -s "$RESPONSE_FILE" ]] && cat "$RESPONSE_FILE" >> "${OUTPUT_FILE}.stderr.log"
|
|
61
|
-
exit "$RC"
|
|
62
|
-
fi
|
|
63
|
-
|
|
64
|
-
set +e
|
|
65
|
-
python3 - "$RESPONSE_FILE" > "${OUTPUT_FILE}.tmp" 2>> "${OUTPUT_FILE}.stderr.log" <<'PY'
|
|
66
|
-
import json, sys
|
|
67
|
-
with open(sys.argv[1], encoding="utf-8", errors="replace") as f:
|
|
68
|
-
data = json.load(f)
|
|
69
|
-
if "error" in data:
|
|
70
|
-
print(f"omnilane: openrouter API error: {data['error']}", file=sys.stderr)
|
|
71
|
-
sys.exit(1)
|
|
72
|
-
content = data["choices"][0]["message"]["content"]
|
|
73
|
-
sys.stdout.write(content if isinstance(content, str) else json.dumps(content))
|
|
74
|
-
PY
|
|
75
|
-
RC=$?
|
|
76
|
-
set -e
|
|
77
|
-
|
|
78
|
-
# Empty output is a failure, not a silent rc=0 success.
|
|
79
|
-
if ! grep -q '[^[:space:]]' "${OUTPUT_FILE}.tmp" 2>/dev/null; then
|
|
80
|
-
echo "omnilane: openrouter produced no output" >> "${OUTPUT_FILE}.stderr.log"
|
|
81
|
-
[[ "$RC" -eq 0 ]] && RC=1
|
|
82
|
-
fi
|
|
83
|
-
|
|
84
|
-
[[ -f "${OUTPUT_FILE}.tmp" ]] && mv "${OUTPUT_FILE}.tmp" "$OUTPUT_FILE"
|
|
85
|
-
[[ -s "${OUTPUT_FILE}.stderr.log" ]] || rm "${OUTPUT_FILE}.stderr.log" 2>/dev/null || true
|
|
86
|
-
exit "$RC"
|
|
3
|
+
# omnilane runner: OpenRouter (OpenAI-compatible direct API). Thin wrapper over
|
|
4
|
+
# run-openai-compat.sh; endpoint and default model live in vendor_api_spec.
|
|
5
|
+
# Override the endpoint with OPENROUTER_BASE_URL; needs OPENROUTER_API_KEY.
|
|
6
|
+
export OMNILANE_OAI_VENDOR=openrouter
|
|
7
|
+
exec "$(dirname "${BASH_SOURCE[0]}")/run-openai-compat.sh" "$@"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# omnilane runner: Z.ai GLM (OpenAI-compatible direct API). Thin wrapper over
|
|
4
|
+
# run-openai-compat.sh; endpoint and default model live in vendor_api_spec.
|
|
5
|
+
# Override the endpoint with ZAI_BASE_URL; needs ZAI_API_KEY.
|
|
6
|
+
export OMNILANE_OAI_VENDOR=zai
|
|
7
|
+
exec "$(dirname "${BASH_SOURCE[0]}")/run-openai-compat.sh" "$@"
|