omnilane 0.8.2 → 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/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" "$@"