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.
@@ -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" =~ ^(codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|exec|off)$ ]]; then
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" =~ ^(codex|claude|grok|gemini|kimi|qwen|opencode|openrouter)$ ]]; then
493
- echo "omnilane: invalid vendor (codex|claude|grok|gemini|kimi|qwen|opencode|openrouter)" >&2
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":"(codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|exec)"(,|})'
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\":\"(codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|exec)\",\"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\"\\}$"
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":"(codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|exec)","model":"([^"\\]*)","effort":"([^"\\]*)","timeout":([1-9][0-9]*),"job_timeout":([1-9][0-9]*|null),"mode":"(advise|work)","workdir":"([^"\\]*)",'
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
- metadata_path="$job_dir/meta.json"
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
- [[ $# -eq 1 ]] || usage
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
- done < <(printf '%s\n' "${ids[@]}" | LC_ALL=C sort -r | sed -n '1,20p')
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"
@@ -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
- # "openrouter" is direct-API (no CLI): available iff curl and a key exist.
86
- if [[ "$1" == "openrouter" ]]; then
87
- command -v curl >/dev/null 2>&1 && [[ -n "${OPENROUTER_API_KEY:-}" ]]
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
- # omnilane runner: OpenRouter direct API (no CLI dependency)
5
- #
6
- # Usage: run-openrouter.sh MODE WORKDIR MODEL EFFORT PROMPT_FILE OUTPUT_FILE
7
- # MODE = advise only. This vendor is pure inference over HTTPS — it has no
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" "$@"