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.
- package/CHANGELOG.md +23 -1
- package/README.ja.md +68 -14
- package/README.ko.md +64 -13
- package/README.md +65 -15
- package/README.zh-CN.md +59 -11
- package/README.zh-TW.md +59 -11
- package/VERSION +1 -1
- package/bin/omnilane +8 -0
- package/bin/omnilane-mcp +465 -0
- package/completions/_omnilane +1 -1
- package/completions/omnilane.bash +1 -1
- package/package.json +1 -1
- package/routing.yaml +4 -2
- package/scripts/configure.sh +7 -1
- package/scripts/dispatch.sh +4 -4
- package/scripts/jobs.sh +3 -3
- package/scripts/lib/common.sh +6 -0
- package/scripts/runners/run-opencode.sh +51 -0
- package/scripts/runners/run-openrouter.sh +86 -0
package/scripts/configure.sh
CHANGED
|
@@ -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
|
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)
|
|
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
|
}
|
package/scripts/lib/common.sh
CHANGED
|
@@ -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"
|