omnilane 0.8.1 → 0.8.3

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.
@@ -18,6 +18,9 @@ GEMINI_MODELS=("Gemini 3.1 Pro (High)" "Gemini 3.1 Pro (Low)" "Gemini 3.5 Flash
18
18
  GROK_MODELS=("grok-4.5" "grok-4.3")
19
19
  KIMI_MODELS=("kimi-k3" "kimi-k2.7-code")
20
20
  QWEN_MODELS=("qwen3-coder-plus" "qwen3-coder-flash")
21
+ # OpenCode models use provider/model form; OpenRouter models are catalog slugs.
22
+ OPENCODE_MODELS=("openrouter/anthropic/claude-sonnet-5" "openrouter/openai/gpt-5.6-sol" "opencode/default (leave model to opencode)")
23
+ OPENROUTER_MODELS=("anthropic/claude-sonnet-5" "openai/gpt-5.6-sol" "moonshotai/kimi-k3" "qwen/qwen3-coder-plus")
21
24
 
22
25
  custom_value_is_safe() {
23
26
  case "$1" in
@@ -76,7 +79,7 @@ while true; do
76
79
  echo "$(msgf cfg_pick_range "${#LANES[@]}")"; continue
77
80
  fi
78
81
  lane="${LANES[$((n - 1))]}"
79
- vendor="$(pick "$(msgf cfg_vendor_for "$lane")" codex claude grok gemini kimi qwen "vote (multi-model panel)" "exec (your own script/gate)" off)"
82
+ vendor="$(pick "$(msgf cfg_vendor_for "$lane")" codex claude grok gemini kimi qwen opencode openrouter "vote (multi-model panel)" "exec (your own script/gate)" off)"
80
83
  [[ "$vendor" == exec* ]] && vendor="exec"
81
84
  [[ "$vendor" == vote* ]] && vendor="vote"
82
85
  if [[ "$vendor" == "off" ]]; then
@@ -90,6 +93,9 @@ while true; do
90
93
  grok) model="$(pick "$(msg cfg_model)" "${GROK_MODELS[@]}")"; effort="-" ;;
91
94
  kimi) model="$(pick "$(msg cfg_model)" "${KIMI_MODELS[@]}")"; effort="-" ;;
92
95
  qwen) model="$(pick "$(msg cfg_model)" "${QWEN_MODELS[@]}")"; effort="-" ;;
96
+ opencode) model="$(pick "$(msg cfg_model)" "${OPENCODE_MODELS[@]}")"; effort="-"
97
+ [[ "$model" == "opencode/default"* ]] && model="-" ;;
98
+ openrouter) model="$(pick "$(msg cfg_model)" "${OPENROUTER_MODELS[@]}")"; effort="-" ;;
93
99
  vote) while true; do
94
100
  count="$(pick "$(msg cfg_voters_count)" "1" "2" "3" "4")"
95
101
  [[ "$count" =~ ^[1-4]$ ]] && break
@@ -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)
55
+ --vendor V pin one configured vendor (codex|claude|grok|gemini|kimi|qwen|opencode|openrouter)
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|exec|off)$ ]]; then
366
+ if ! [[ "$vendor" =~ ^(codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|exec|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)$ ]]; then
493
- echo "omnilane: invalid vendor (codex|claude|grok|gemini|kimi|qwen)" >&2
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
494
494
  exit 2
495
495
  fi
496
496
 
package/scripts/jobs.sh CHANGED
@@ -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|exec)"(,|})'
135
+ local metadata_re='^\{"lane":"([a-z][a-z0-9-]*)","vendor":"(codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|exec)"(,|})'
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|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\":\"(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\"\\}$"
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|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":"(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":"([^"\\]*)",'
256
256
  [[ "$PUBLIC_METADATA" =~ $retry_re ]] || {
257
257
  die 1 "cannot retry: job metadata is not safely parseable"
258
258
  }
@@ -72,6 +72,7 @@ vendor_bin() { # vendor -> binary (honors local.sh overrides)
72
72
  gemini) echo "${AGY_BIN:-agy}" ;;
73
73
  kimi) echo "${KIMI_BIN:-kimi}" ;;
74
74
  qwen) echo "${QWEN_BIN:-qwen}" ;;
75
+ opencode) echo "${OPENCODE_BIN:-opencode}" ;;
75
76
  *) echo "" ;;
76
77
  esac
77
78
  }
@@ -81,6 +82,11 @@ vendor_available() {
81
82
  # checked at dispatch time (the chain resolver cannot see it here).
82
83
  # "vote" is the built-in multi-model panel: needs >=2 voters, checked at dispatch.
83
84
  [[ "$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:-}" ]]
88
+ return
89
+ fi
84
90
  local b; b="$(vendor_bin "$1")"
85
91
  [[ -n "$b" ]] && command -v "$b" >/dev/null 2>&1
86
92
  }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # omnilane runner: OpenCode CLI (multi-provider aggregator)
5
+ #
6
+ # Usage: run-opencode.sh MODE WORKDIR MODEL EFFORT PROMPT_FILE OUTPUT_FILE
7
+ # MODE = advise (built-in read-only plan agent) | work (may edit files)
8
+ #
9
+ # MODEL is OpenCode's provider/model form (e.g. openrouter/moonshotai/kimi-k2);
10
+ # "-" lets OpenCode use its own configured default. OpenCode's --variant flag
11
+ # is provider-specific, so EFFORT maps onto it only when set.
12
+
13
+ source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
14
+
15
+ MODE="$1"; WORKDIR="$2"; MODEL="$3"; EFFORT="$4"; PROMPT_FILE="$5"; OUTPUT_FILE="$6"
16
+
17
+ OPENCODE_BIN="${OPENCODE_BIN:-opencode}"
18
+ RUN_TIMEOUT="${OMNILANE_TIMEOUT:-600}"
19
+
20
+ truncate_payload "$PROMPT_FILE" 140000
21
+
22
+ # --agent plan is OpenCode's built-in restricted agent (edits/bash gated);
23
+ # --auto only in work mode, and only for permissions not explicitly denied.
24
+ ARGS=(run "$(cat "$PROMPT_FILE")" --format default)
25
+ [[ -n "$MODEL" && "$MODEL" != "-" ]] && ARGS+=(-m "$MODEL")
26
+ [[ -n "$EFFORT" && "$EFFORT" != "-" ]] && ARGS+=(--variant "$EFFORT")
27
+ if [[ "$MODE" == "advise" ]]; then
28
+ ARGS+=(--agent plan)
29
+ else
30
+ ARGS+=(--auto)
31
+ fi
32
+
33
+ set +e
34
+ (
35
+ cd "$WORKDIR" || exit 127
36
+ OMNILANE_DEPTH=1 run_with_timeout "$RUN_TIMEOUT" \
37
+ "$OPENCODE_BIN" "${ARGS[@]}" \
38
+ > "${OUTPUT_FILE}.tmp" 2> "${OUTPUT_FILE}.stderr.log"
39
+ )
40
+ RC=$?
41
+ set -e
42
+
43
+ # Empty output is a failure, not a silent rc=0 success.
44
+ if ! grep -q '[^[:space:]]' "${OUTPUT_FILE}.tmp" 2>/dev/null; then
45
+ echo "omnilane: opencode produced no output" >> "${OUTPUT_FILE}.stderr.log"
46
+ [[ "$RC" -eq 0 ]] && RC=1
47
+ fi
48
+
49
+ [[ -f "${OUTPUT_FILE}.tmp" ]] && mv "${OUTPUT_FILE}.tmp" "$OUTPUT_FILE"
50
+ [[ -s "${OUTPUT_FILE}.stderr.log" ]] || rm "${OUTPUT_FILE}.stderr.log" 2>/dev/null || true
51
+ exit "$RC"
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
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"