Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ logger.info("Task {} finished in {} ms", taskId, cost) // placeholders, never c
- Default port: **8182**
- Config files: `application.properties` → `application-*.properties` → `application-private.properties` (git-ignored, secrets here or env vars)
- Key properties: `openrouter.api.key`, `browser.profile.mode` (DEFAULT|SYSTEM_DEFAULT|SEQUENTIAL|TEMPORARY), `browser.display.mode` (GUI|HEADLESS|SUPERVISED)
- LLM providers configured via env vars: `DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `VOLCENGINE_API_KEY`, `OPENAI_API_KEY`
- LLM providers configured via env vars: `DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `ORCAROUTER_API_KEY`, `VOLCENGINE_API_KEY`, `OPENAI_API_KEY`

## Development Patterns

Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ RUN apk update && apk upgrade && \
# Ignore BROWSER_CONTEXT_NUMBER, BROWSER_MAX_OPEN_TABS if BROWSER_CONTEXT_MODE is set to DEFAULT
ENV JAVA_OPTS="-Xms2G -Xmx10G -XX:+UseG1GC" \
OPENROUTER_API_KEY=${OPENROUTER_API_KEY} \
ORCAROUTER_API_KEY=${ORCAROUTER_API_KEY} \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files:\n'
git ls-files | rg '(^|/)Dockerfile(\.[^/]+)?$|(^|/)docs/TESTING.md$' || true

printf '\nDockerfile occurrences:\n'
for f in Dockerfile Dockerfile.fast Dockerfile.native; do
  if [ -f "$f" ]; then
    echo "--- $f"
    nl -ba "$f" | sed -n '50,75p' | tee /tmp/dockerfile_section
  fi
done

printf '\nAll ORCAROUTER_API_KEY and OPENROUTER_API_KEY references in Dockerfiles:\n'
rg -n 'ORCAROUTER_API_KEY|OPENROUTER_API_KEY' Dockerfile Dockerfile.fast Dockerfile.native || true

printf '\nExisting .dockerignore / build context hints:\n'
git ls-files | rg '(^|/)\.dockerignore$|(^|/)docker-compose\.ya?ml$|(^|/)compose\.ya?ml$|(^|/)Dockerfile\.fast$|(^|/)Dockerfile\.native$' || true
for f in .dockerignore docker-compose.yml docker-compose.yaml compose.yml compose.yaml; do
  [ -f "$f" ] && { echo "--- $f"; nl -ba "$f" | sed -n '1,120p'; }
done

printf '\nProject docs about secret/env handling:\n'
if [ -f docs/TESTING.md ]; then
  rg -n 'secret|API_KEY|Docker|docker|env|ENV|environment' docs/TESTING.md docs Dockerfile Dockerfile.fast Dockerfile.native 2>/dev/null | head -n 200 || true
else
  echo "docs/TESTING.md missing"
fi

Repository: platonai/Browser4

Length of output: 370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Behavioral/source check: parse Dockerfile ENV/ARG for the referenced keys and show whether runtime CMD/entrypoint propagates them.
python3 - <<'PY'
from pathlib import Path
import re, sys

dfiles = [Path("Dockerfile"), Path("Dockerfile.fast"), Path("Dockerfile.native")]
keys = {"ORCAROUTER_API_KEY", "OPENROUTER_API_KEY"}

for p in dfiles:
    if not p.exists():
        continue
    lines = p.read_text().splitlines()
    envs = []
    args = []
    runtime_vars = []
    for i, line in enumerate(lines, 1):
        s = line.strip()
        if s.upper().startswith("ENV "):
            parts = re.split(r'[;\s]+', s[4:], 1)
            envs.append((i, parts[0]))
        elif s.upper().startswith("ARG "):
            args.append((i, s[5:].split("=",1)[0]))
        elif s.upper().startswith(("CMD ", "ENTRYPOINT ", "ENV ")):
            runtime_vars.append((i, s))
    print(f"--- {p}")
    print("env:", [(i,k) for i,k in envs if k in keys], flush=True)
    print("arg:", [(i,k) for i,k in args if k in keys], flush=True)
    print("runtime_cmd/entrypoint/env:", runtime_vars, flush=True)
    for k in keys:
        print("image_config", k, "=>", any(k in env or (env.upper().startswith(k+"=") if isinstance(env,str) else False) for _,env in envs))
PY

Repository: platonai/Browser4

Length of output: 772


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Environment variable definitions and references:\n'
for f in Dockerfile Dockerfile.fast Dockerfile.native; do
  if [ -f "$f" ]; then
    echo "--- $f"
    grep -n -E 'ENV|ARG|ORCAROUTER_API_KEY|OPENROUTER_API_KEY|JAVA_OPTS|ENTRYPOINT|RUBIKS_ROUTER|ORCA|ROUTER|openrouter|ocarouter' "$f" || true
  fi
done

printf '\nRelevant file slices:\n'
for f in Dockerfile Dockerfile.fast Dockerfile.native; do
  if [ -f "$f" ]; then
    echo "--- $f lines"
    # shellcheck disable=SC2035
    grep -n -E 'ENV|ARG|ORCAROUTER_API_KEY|OPENROUTER_API_KEY|ENTRYPOINT|JAVA_OPTS|CMD' "$f" | head -n 40 | while read -r match; do
      line="${match%%:*}"
      sed -n "$((line>3?line-3:1)),$((line+6))p" "$f"
      echo
    done
  fi
done

printf '\nOther repository references to ORCAROUTER_API_KEY / OPENROUTER_API_KEY:\n'
grep -RIn -E "ORCAROUTER_API_KEY|OPENROUTER_API_KEY" . || true

Repository: platonai/Browser4

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Docker image instructions mentioning secrets-API keys across tracked files:\n'
git ls-files | grep -E '(^|/)(Dockerfile|.*Dockerfile[^/]*)$' | while read -r f; do
  echo "--- $f"
  grep -n -E 'ENV|ARG|ORCAROUTER_API_KEY|OPENROUTER_API_KEY|JAVA_OPTS|ENTRYPOINT|CMD' "$f" || true
done

printf '\nRead-only Dockerfile source scan for EnvInstruction / ArgInstruction around keys:\n'
python3 - "$@" <<'PY'
from pathlib import Path
import re, sys
for path in [Path("Dockerfile"), Path("Dockerfile.fast"), Path("Dockerfile.native")]:
    if not path.exists():
        continue
    print(f"--- {path}")
    for i,line in enumerate(path.read_text().splitlines(),1):
        s=line.strip()
        if not s: continue
        first=s.split(None,1)[0].upper() if s else ""
        if first in ("ENV","ARG") or "ORCAROUTER_API_KEY" in s or "OPENROUTER_API_KEY" in s:
            print("{:d}: {}".format(i,line))
PY

Repository: platonai/Browser4

Length of output: 2023


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Dockerfile image configuration inspection:\n'
for f in Dockerfile Dockerfile.fast Dockerfile.native; do
  if [ -f "$f" ]; then
    echo "--- $f"
    python3 - "$f" <<'PY'
import sys, re, textwrap
from pathlib import Path
keys = {"ORCAROUTER_API_KEY", "OPENROUTER_API_KEY"}
path = Path(sys.argv[1])
lines = path.read_text().splitlines()
for i,l in enumerate(lines,1):
    s = l.strip()
    cmd = s.split(None,1)[0].upper() if s else ""
    instr = s[len(cmd):].strip() if s.startswith(cmd + " ") else (s[len(cmd):].strip() if s.startswith(cmd + "\t") else "")
    if cmd in ("ENV", "ARG") or any(k in s for k in keys):
        print(f"{i:4}: |{instr}|")
    if cmd in ("CMD", "ENTRYPOINT"):
        print(f"{i:4}: {cmd} -> {instr}")
PY
  fi
done

Repository: platonai/Browser4

Length of output: 839


Keep provider API keys out of Docker image configuration.

Dockerfile, Dockerfile.fast, and Dockerfile.native all use OPENROUTER_API_KEY=${OPENROUTER_API_KEY} and ORCAROUTER_API_KEY=${ORCAROUTER_API_KEY}, so a build-time value is baked into image layers and remains exposed unless the key is unset before build. Pass these keys at container startup and remove the image-level secret entries; apply the same treatment to OPENROUTER_API_KEY.

  • Dockerfile#L67-L68: remove OPENROUTER_API_KEY and ORCAROUTER_API_KEY.
  • Dockerfile.fast#L35-L36: remove OPENROUTER_API_KEY and ORCAROUTER_API_KEY.
  • Dockerfile.native#L66-L67: remove OPENROUTER_API_KEY and ORCAROUTER_API_KEY.
📍 Affects 3 files
  • Dockerfile#L68-L68 (this comment)
  • Dockerfile.fast#L36-L36
  • Dockerfile.native#L67-L67
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` at line 68, Remove the OPENROUTER_API_KEY and ORCAROUTER_API_KEY
image-level environment entries from Dockerfile lines 67-68, Dockerfile.fast
lines 35-36, and Dockerfile.native lines 66-67. Keep both keys supplied only at
container startup rather than through build-time configuration.

PROXY_ROTATION_URL=${PROXY_ROTATION_URL} \
BROWSER_CONTEXT_MODE=DEFAULT \
BROWSER_CONTEXT_NUMBER=2 \
Expand Down
1 change: 1 addition & 0 deletions Dockerfile.fast
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ RUN apk update && apk upgrade && \
# Ignore BROWSER_CONTEXT_NUMBER, BROWSER_MAX_OPEN_TABS if BROWSER_CONTEXT_MODE is set to DEFAULT
ENV JAVA_OPTS="-Xms2G -Xmx10G -XX:+UseG1GC" \
OPENROUTER_API_KEY=${OPENROUTER_API_KEY} \
ORCAROUTER_API_KEY=${ORCAROUTER_API_KEY} \
PROXY_ROTATION_URL=${PROXY_ROTATION_URL} \
BROWSER_CONTEXT_MODE=DEFAULT \
BROWSER_CONTEXT_NUMBER=2 \
Expand Down
1 change: 1 addition & 0 deletions Dockerfile.native
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ RUN apk update && apk upgrade && \
# -Xmx (the native image supports a subset of JVM flags).
ENV JAVA_OPTS="-Xms2G -Xmx10G -XX:+UseG1GC" \
OPENROUTER_API_KEY=${OPENROUTER_API_KEY} \
ORCAROUTER_API_KEY=${ORCAROUTER_API_KEY} \
PROXY_ROTATION_URL=${PROXY_ROTATION_URL} \
BROWSER_CONTEXT_MODE=DEFAULT \
BROWSER_CONTEXT_NUMBER=2 \
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ AI-powered commands such as `extract`, `summarize`, `chat`, `agent run`, and X-S
|---|---|
| DeepSeek | `DEEPSEEK_API_KEY` |
| OpenRouter | `OPENROUTER_API_KEY`, `OPENROUTER_MODEL_NAME`, `OPENROUTER_BASE_URL` |
| OrcaRouter | `ORCAROUTER_API_KEY`, `ORCAROUTER_MODEL_NAME`, `ORCAROUTER_BASE_URL` |
| Volcengine | `VOLCENGINE_API_KEY`, `VOLCENGINE_MODEL_NAME`, `VOLCENGINE_BASE_URL` |
| OpenAI-compatible | `OPENAI_API_KEY`, `OPENAI_MODEL_NAME`, `OPENAI_BASE_URL` |
| Aliyun Qwen | `OPENAI_API_KEY`, `OPENAI_MODEL_NAME`, `OPENAI_BASE_URL` |
Expand Down
1 change: 1 addition & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ browser4-cli pdf --filename page.pdf
|---|---|
| DeepSeek | `DEEPSEEK_API_KEY` |
| OpenRouter | `OPENROUTER_API_KEY`, `OPENROUTER_MODEL_NAME`, `OPENROUTER_BASE_URL` |
| OrcaRouter | `ORCAROUTER_API_KEY`, `ORCAROUTER_MODEL_NAME`, `ORCAROUTER_BASE_URL` |
| Volcengine | `VOLCENGINE_API_KEY`, `VOLCENGINE_MODEL_NAME`, `VOLCENGINE_BASE_URL` |
| OpenAI-compatible | `OPENAI_API_KEY`, `OPENAI_MODEL_NAME`, `OPENAI_BASE_URL` |
| Aliyun Qwen | `OPENAI_API_KEY`, `OPENAI_MODEL_NAME`, `OPENAI_BASE_URL` |
Expand Down
13 changes: 12 additions & 1 deletion application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ spring.main.allow-bean-definition-overriding=true
# ---------------------------------------------------------------------------
# Set ONE of these depending on which LLM provider you use.
# Priority order (in AgentToolExecutor): LLM_API_KEY → DEEPSEEK_API_KEY →
# OPENROUTER_API_KEY → VOLCENGINE_API_KEY → OPENAI_API_KEY
# OPENROUTER_API_KEY → ORCAROUTER_API_KEY → VOLCENGINE_API_KEY → OPENAI_API_KEY
# You can also set these as system properties (e.g. -Dllm.api.key=...)

# Generic LLM API key (fallback for all providers)
Expand All @@ -133,6 +133,12 @@ spring.main.allow-bean-definition-overriding=true
# OpenRouter
# OPENROUTER_API_KEY=sk-or-v1-...

# OrcaRouter (OpenAI-compatible routing gateway, https://www.orcarouter.ai)
# A single ORCAROUTER_API_KEY reaches many models (including free models) via
# the smart-routing `orcarouter/auto` alias. Registered at startup so Browser4
# treats it like any other named provider.
# ORCAROUTER_API_KEY=sk-orca-...

# DeepSeek
# DEEPSEEK_API_KEY=sk-...

Expand All @@ -155,6 +161,11 @@ spring.main.allow-bean-definition-overriding=true
# volcengine.model.name=doubao-seed-2-0-pro-260215
# volcengine.base.url=https://ark.cn-beijing.volces.com/api/v3

## OrcaRouter (https://www.orcarouter.ai)
#orcarouter.api.key=...
#orcarouter.model.name=orcarouter/auto
#orcarouter.base.url=https://api.orcarouter.ai/v1

## DeepSeek
# deepseek.api.key=sk-...
# deepseek.model.name=deepseek-v4-pro
Expand Down
1 change: 1 addition & 0 deletions bin/env-manage.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ $KnownVars = @(
@{Name='LLM_API_KEY'; Category='llm'; Default=''; Desc='Generic LLM API key (fallback for all providers)'; Sensitive=$true}
@{Name='OPENAI_API_KEY'; Category='llm'; Default=''; Desc='OpenAI API key'; Sensitive=$true}
@{Name='OPENROUTER_API_KEY'; Category='llm'; Default=''; Desc='OpenRouter API key'; Sensitive=$true}
@{Name='ORCAROUTER_API_KEY'; Category='llm'; Default=''; Desc='OrcaRouter API key (OpenAI-compatible routing gateway)'; Sensitive=$true}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
fd -a 'env-manage\.ps1$' . || true

echo "== file byte-header =="
if [ -f bin/env-manage.ps1 ]; then
  xxd -l 48 bin/env-manage.ps1 || true
  python3 - <<'PY'
from pathlib import Path
p=Path('bin/env-manage.ps1')
b=p.read_bytes()
print("length", len(b))
print("startswith_bom", b.startswith(b'\xef\xbb\xbf'))
print("contains_non_ascii_bytes", any(x > 127 for x in b))
if len(b) > 0: print("first_bytes", b[:48].hex())
# check UTF-8 decodability and non-ascii chars
try:
    t=b.decode('utf-8')
    print("utf8_decode_ok", True)
    chars=[c for i,c in enumerate(t) if ord(c)>127]
    print("non_ascii_char_count", len(chars))
    first_occ={ord(c): (i,c) for i,c in enumerate(t) if ord(c)>127 and ord(c) not in first_occ}
    print("sample_non_ascii_chars", first_occ[:20])
except UnicodeDecodeError as e:
    print("utf8_decode_error", e)
PY
fi

echo "== PSScriptAnalyzer availability/version =="
pwsh -NoProfile -Command 'if (Get-Module -ListAvailable PSStyleModule) { PSStyleModule | Format-Table Name, Path, Version }' || true
pwsh -NoProfile -Command 'try { Show-Command ErrorViewErrorView } catch { Write-Host "Show-Command unavailable" }' || true
pwsh -NoProfile -Command 'Get-Module PSScriptAnalyzer -ListAvailable | Select-Object -ExpandProperty Version | Sort-Object -Descending | Select-Object -First 5' || true

echo "== run PSScriptAnalyzer on target if available =="
pwsh -NoProfile -Command '
if (!(Get-Module PSScriptAnalyzer -ListAvailable)) { exit 0 }
Import-Module PSScriptAnalyzer -Force
$rules=(Get-ScriptAnalyzerRule -Name PSUseBOMForUnicodeEncodedFile)
if (-not $rules) { exit 0 }
$all=Get-ScriptAnalyzerRule
$arr=Invoke-ScriptAnalyzer -Path bin/env-manage.ps1 -Severity Error,Warning,Information,Verbose -Include $rules -Verbose:$false
if (-not $arr) { exit 0 }
$arr | Format-Table Line, Column, RuleName, Severity, Message -AutoSize
' || true

Repository: platonai/Browser4

Length of output: 647


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== environment =="
uname -a || true
pwsh -NoProfile -Command '$PSVersionTable.PSVersion' || true

echo "== PSScriptAnalyzer availability/version =="
pwsh -NoProfile -Command 'Get-Module PSScriptAnalyzer -ListAvailable | Select-Object Name, Version, InstalledLocation | Format-Table -AutoSize;' || true
pwsh -NoProfile -Command 'try { $rules=Get-ScriptAnalyzerRule -Name PSUseBOMForUnicodeEncodedFile -ErrorAction Stop; $rules | Format-List -AutoSize } catch { $_.FullyQualifiedErrorId }; exit $LASTEXITCODE' || true

echo "== run only PSUseBOMForUnicodeEncodedFile if rule is available =="
pwsh -NoProfile -Command '
Import-Module PSScriptAnalyzer -ErrorAction Stop -Force -Verbose:$false
Get-ScriptAnalyzerRule -Name PSUseBOMForUnicodeEncodedFile -ErrorAction Stop | Out-Null
$arr=Invoke-ScriptAnalyzer -Path bin/env-manage.ps1 -IncludeRule PSUseBOMForUnicodeEncodedFile -Verbose:$false
$arr
' || true

echo "== inspect non-ASCII occurrences =="
python3 - <<'PY'
from pathlib import Path
p=Path('bin/env-manage.ps1')
text=p.read_text(encoding='utf-8')
samples=[]
for idx,ch in enumerate(text):
    if ord(ch)>127:
        if (idx, text[max(0,idx-80):idx+80]) not in samples:
            samples.append((idx, text[max(0,idx-80):min(len(text),idx+80)]))
        if len(samples)>=8:
            break
for idx, context in samples:
    print(idx, repr(context).replace('\\n',' '))
PY

Repository: platonai/Browser4

Length of output: 2628


Save bin/env-manage.ps1 with a UTF-8 BOM.

PSUseBOMForUnicodeEncodedFile flags this non-ASCII file as missing BOM encoding. Windows PowerShell can misread the non-ASCII output strings without a BOM. Re-save the file with a UTF-8 BOM.

🧰 Tools
🪛 PSScriptAnalyzer (1.25.0)

[warning] Missing BOM encoding for non-ASCII encoded file 'env-manage.ps1'

(PSUseBOMForUnicodeEncodedFile)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/env-manage.ps1` at line 72, Re-save bin/env-manage.ps1 using UTF-8
encoding with a BOM, preserving all existing script content and the non-ASCII
output strings.

Source: Linters/SAST tools

@{Name='DEEPSEEK_API_KEY'; Category='llm'; Default=''; Desc='DeepSeek API key'; Sensitive=$true}
@{Name='VOLCENGINE_API_KEY'; Category='llm'; Default=''; Desc='Volcengine (ByteDance) API key'; Sensitive=$true}

Expand Down
1 change: 1 addition & 0 deletions bin/env-manage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ KNOWN_VARS=(
"LLM_API_KEY|llm||Generic LLM API key (fallback for all providers)|1"
"OPENAI_API_KEY|llm||OpenAI API key|1"
"OPENROUTER_API_KEY|llm||OpenRouter API key|1"
"ORCAROUTER_API_KEY|llm||OrcaRouter API key (OpenAI-compatible routing gateway)|1"
"DEEPSEEK_API_KEY|llm||DeepSeek API key|1"
"VOLCENGINE_API_KEY|llm||Volcengine (ByteDance) API key|1"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class AgentToolExecutor : AbstractToolExecutor() {
?: System.getenv("LLM_API_KEY")
?: System.getenv("DEEPSEEK_API_KEY")
?: System.getenv("OPENROUTER_API_KEY")
?: System.getenv("ORCAROUTER_API_KEY")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

fd -t f -i 'test|spec' . | xargs -r rg -n -C 3 \
  'ORCAROUTER_API_KEY|requireLLMConfigured|LLM API key is not configured'

Repository: platonai/Browser4

Length of output: 1670


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== locate AgentToolExecutor.kt =="
fd -t f '^AgentToolExecutor\.kt$' . || true

echo "== inspect target file around lines 40-85 =="
agent_file="$(fd -t f '^AgentToolExecutor\.kt$' . | head -n 1 || true)"
if [ -n "${agent_file:-}" ]; then
  wc -l "$agent_file"
  sed -n '1,120p' "$agent_file" | nl -ba -v1
fi

echo "== candidate tests/modules with relevant identifiers =="
test_files=$(fd -t f . -i 'test|spec' . 2>/dev/null | grep -Ei '(^|/)(browser4-agentic|agentic).*test|/test/|/specs?/' || true)
echo "test file candidates: ${test_files:-none}"

rg -n -C 3 \
  'ORCAROUTER_API_KEY|requireLLMConfigured|LLM API key is not configured|AgentToolExecutor|OrcaRouter|orca.*router|ORCA' \
  --glob '!browser4-tests/**' --glob '!coworker/**' --glob '!build/**' --glob '!target/**' . \
  || true

Repository: platonai/Browser4

Length of output: 469


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

target="./browser4-agentic/src/main/kotlin/ai/platon/pulsar/agentic/tools/builtin/AgentToolExecutor.kt"

echo "== target file lines 1-120 =="
awk '{printf "%4d\t%s\n", NR, $0}' "$target" | sed -n '1,130p'

echo "== focus area lines 45-80 =="
awk '{printf "%4d\t%s\n", NR, $0}' "$target" | sed -n '45,85p'

echo "== repository test files with relevant search terms only =="
rg -n -C 3 \
  'ORCAROUTER_API_KEY|requireLLMConfigured|LLM API key is not configured|AgentToolExecutor|OrcaRouter|orca.*router|ORCA' \
  --glob '!browser4-tests/**' --glob '!coworker/**' --glob '!build/**' --glob '!target/**' \
  --glob '!**/report/**' --glob '!**/report_*/**' . \
  || true

echo "== list tracked test/spec-looking files under browser4-agentic only =="
git ls-files browser4-agentic | awk 'tolower($0) ~ /(test|spec|spec_)|test(s)?`@/` { print }' | head -n 200 || true

Repository: platonai/Browser4

Length of output: 50375


Add focused tests for the new OrcaRouter LLM fallback.

requireLLMConfigured accepts a non-blank ORCAROUTER_API_KEY and lists it in the missing-key error, but there is no targeted AgentToolExecutor test for this case. Add the smallest relevant tests for non-blank env acceptance and the updated error guidance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@browser4-agentic/src/main/kotlin/ai/platon/pulsar/agentic/tools/builtin/AgentToolExecutor.kt`
at line 58, Add focused tests for AgentToolExecutor.requireLLMConfigured
covering acceptance of a non-blank ORCAROUTER_API_KEY and inclusion of
ORCAROUTER_API_KEY in the missing-key error guidance. Keep the tests minimal and
isolated to the new OrcaRouter fallback behavior, preserving existing
configuration cases.

Source: Coding guidelines

?: System.getenv("VOLCENGINE_API_KEY")
?: System.getenv("OPENAI_API_KEY")

Expand All @@ -68,6 +69,7 @@ class AgentToolExecutor : AbstractToolExecutor() {
"LLM API key is not configured. To use extract/summarize/agent commands, set one of:\n" +
" - Environment variable: DEEPSEEK_API_KEY=sk-...\n" +
" - Environment variable: OPENROUTER_API_KEY=...\n" +
" - Environment variable: ORCAROUTER_API_KEY=...\n" +
" - Environment variable: VOLCENGINE_API_KEY=...\n" +
" - Environment variable: OPENAI_API_KEY=sk-...\n" +
" - Or set LLM_PROVIDER, LLM_NAME, LLM_API_KEY system properties\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class Browser4BundleApplication(
"LLM is configured, you can use LLM commands."
} else {
"LLM is not configured, you can only use non-LLM commands. X-SQL is still available. " +
"It is highly recommended to set OPENROUTER_API_KEY or other LLM keys to enable LLM features."
"It is highly recommended to set OPENROUTER_API_KEY, ORCAROUTER_API_KEY or other LLM keys to enable LLM features."
}
} catch (e: Exception) {
logger.warn("Failed to check LLM configuration", e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class Browser4StandaloneApplication(
"LLM is configured, you can use LLM commands."
} else {
"LLM is not configured, you can only use non-LLM commands. X-SQL is still available. " +
"It is highly recommended to set OPENROUTER_API_KEY or other LLM keys to enable LLM features."
"It is highly recommended to set OPENROUTER_API_KEY, ORCAROUTER_API_KEY or other LLM keys to enable LLM features."
}
} catch (e: Exception) {
logger.warn("Failed to check LLM configuration", e)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,48 @@
package ai.platon.pulsar.boot.autoconfigure

import ai.platon.pulsar.common.Systems
import ai.platon.pulsar.external.ApiProtocol
import ai.platon.pulsar.external.ChatModelFactory
import ai.platon.pulsar.external.ProviderConfig
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationContextInitializer
import org.springframework.context.support.AbstractApplicationContext

class PulsarContextInitializer : ApplicationContextInitializer<AbstractApplicationContext> {
override fun initialize(applicationContext: AbstractApplicationContext) {
Systems.setPropertyIfAbsent("app.name", "browser4")
registerOrcaRouterProvider()
}

companion object {
private val logger = LoggerFactory.getLogger(PulsarContextInitializer::class.java)

/**
* Register OrcaRouter (https://www.orcarouter.ai) as a named OpenAI-compatible
* LLM provider.
*
* OrcaRouter is an OpenAI-compatible routing gateway: a single ORCAROUTER_API_KEY
* reaches many models (including free models via the `orcarouter/auto` smart-routing
* alias) behind one endpoint, https://api.orcarouter.ai/v1. Registering it here makes
* [ChatModelFactory] recognize ORCAROUTER_API_KEY just like OPENROUTER_API_KEY, so
* Browser4's agentic layer can route LLM requests through OrcaRouter with no further
* changes. The registration is safe (and a no-op) when no key is set.
*/
fun registerOrcaRouterProvider() {
runCatching {
ChatModelFactory.registerProvider(
ProviderConfig(
apiKeyName = "ORCAROUTER_API_KEY",
modelNameKey = "ORCAROUTER_MODEL_NAME",
baseUrlKey = "ORCAROUTER_BASE_URL",
defaultModel = "orcarouter/auto",
defaultBaseUrl = "https://api.orcarouter.ai/v1",
providerName = "orcarouter",
)
)
}.onFailure { e ->
logger.warn("Failed to register OrcaRouter provider (non-fatal): {}", e.message)
}
Comment on lines +31 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 5 --glob '*Test.kt' --glob '*IT.kt' --glob '*Spec.kt' \
  'ORCAROUTER|registerOrcaRouterProvider|ProviderConfig' \
  browser4-boot browser4-agentic

Repository: platonai/Browser4

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate PulsarContextInitializer.kt =="
fd -a 'PulsarContextInitializer\.kt$' . || true

echo "== file outline/contents =="
if [ -f browser4-boot/src/main/kotlin/ai/platon/pulsar/boot/autoconfigure/PulsarContextInitializer.kt ]; then
  wc -l browser4-boot/src/main/kotlin/ai/platon/pulsar/boot/autoconfigure/PulsarContextInitializer.kt
  cat -n browser4-boot/src/main/kotlin/ai/platon/pulsar/boot/autoconfigure/PulsarContextInitializer.kt
fi

echo "== related chat model/provider config usages =="
rg -n -C 4 --glob '!**/build/**' --glob '!**/target/**' \
  'ChatModelFactory|registerProvider|ProviderConfig|registerOrcaRouterProvider|ORCAROUTER' \
  . || true

echo "== Kotlin test/config files under relevant modules =="
git ls-files | rg '(^|/)(browser4-boot|browser4-agentic)/.*(Test|IT|Spec)\.kt$|docs/TESTING\.md$|README.*\.md$' || true

echo "== focused search test files only across repo for OrcaRouter terms =="
git ls-files '*Test.kt' '*IT.kt' '*Spec.kt' | xargs rg -n -C 5 'ORCAROUTER|registerOrcaRouterProvider|ProviderConfig' || true

Repository: platonai/Browser4

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euv

echo "== test references for OrcaRouter exact terms =="
git ls-files '*Test.kt' '*IT.kt' '*Spec.kt' | \
  xargs rg -n 'ORCAROUTER|OrcaRouter|registerOrcaRouterProvider|orcarouter/auto' \
  --no-heading \
  --glob '*.kt' | head -n 20 || true

echo "== provider config/registration method definitions =="
fd -a '\.kt$' browser4-core browser4-skeleton browser4-common browser4 | \
  xargs rg -n --no-heading -C 3 'class ProviderConfig|record ProviderConfig|data class ProviderConfig|fun registerProvider|fun isModelConfigured|orcarouter|ORCAROUTER' \
  --glob '*.kt' | head -n 200 || true

echo "== README docs mentioning OrcaRouter property/env defaults =="
rg -n 'ORCAROUTER_MODEL_NAME|ORCAROUTER_BASE_URL|orcarouter/auto|orcarouter.model.name|orcarouter.base.url' README.md README.zh.md cli/README.md docs/config.md application.properties --glob '*.md' --glob '*.properties' || true

echo "== docs/test coverage target excerpts =="
sed -n '1,160p' docs/TESTING.md 2>/dev/null || true

Repository: platonai/Browser4

Length of output: 4569


Add focused tests for OrcaRouter startup coverage.

registerOrcaRouterProvider() now runs during startup and documents orcarouter/auto and https://api.orcarouter.ai/v1, but existing tests do not cover this provider. Add Unit/Fast coverage for default model/base URL, explicit environment or property overrides, startup without an OrcaRouter key, and registration failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@browser4-boot/src/main/kotlin/ai/platon/pulsar/boot/autoconfigure/PulsarContextInitializer.kt`
around lines 31 - 45, Add focused Unit/Fast tests for
PulsarContextInitializer.registerOrcaRouterProvider() covering the default model
“orcarouter/auto” and base URL, explicit environment or property overrides,
startup when ORCAROUTER_API_KEY is absent, and provider-registration failure.
Verify startup remains non-fatal and logs the expected warning when registration
throws, while preserving existing provider behavior.

Source: Coding guidelines

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class DoctorController(
fun llmStatus(): ResponseEntity<Map<String, Any?>> {
val envKeyNames = listOf(
"OPENROUTER_API_KEY",
"ORCAROUTER_API_KEY",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat blank API-key values as not configured.

System.getenv(it) != null and System.getProperty(it) != null treat empty values as valid keys. A blank ORCAROUTER_API_KEY or orcarouter.api.key therefore makes /api/doctor/llm-status report configured=true, while AgentToolExecutor.requireLLMConfigured rejects blank values at Line 62.

Use isNullOrBlank() for both checks and add tests for blank and non-blank OrcaRouter values.

Proposed fix
-        val foundEnvVars = envKeyNames.filter { System.getenv(it) != null }
-        val foundProperties = propertyKeyNames.filter { System.getProperty(it) != null }
+        val foundEnvVars = envKeyNames.filter { !System.getenv(it).isNullOrBlank() }
+        val foundProperties = propertyKeyNames.filter { !System.getProperty(it).isNullOrBlank() }

Also applies to: 136-136

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@browser4-rest/src/main/kotlin/ai/platon/pulsar/rest/api/controller/DoctorController.kt`
at line 127, Update the API-key configuration checks in the DoctorController
logic for ORCAROUTER_API_KEY and orcarouter.api.key to use isNullOrBlank(), so
only non-blank environment and system-property values count as configured. Add
tests covering blank and non-blank OrcaRouter values, matching
AgentToolExecutor.requireLLMConfigured behavior.

Source: Coding guidelines

"DEEPSEEK_API_KEY",
"VOLCENGINE_API_KEY",
"OPENAI_API_KEY",
Expand All @@ -132,6 +133,7 @@ class DoctorController(
val propertyKeyNames = listOf(
"llm.api.key",
"openrouter.api.key",
"orcarouter.api.key",
"volcengine.api.key",
"deepseek.api.key",
"openai.api.key",
Expand Down Expand Up @@ -161,7 +163,7 @@ class DoctorController(
} else {
"LLM is not configured, you can only use non-LLM commands. " +
"X-SQL is still available. " +
"It is highly recommended to set OPENROUTER_API_KEY or other LLM keys to enable LLM features."
"It is highly recommended to set OPENROUTER_API_KEY, ORCAROUTER_API_KEY or other LLM keys to enable LLM features."
}

return ResponseEntity.ok(
Expand All @@ -174,7 +176,7 @@ class DoctorController(
},
"foundEnvVars" to foundEnvVars,
"foundProperties" to foundProperties,
"keyPrefixes" to listOf("OPENROUTER", "DEEPSEEK", "VOLCENGINE", "OPENAI"),
"keyPrefixes" to listOf("OPENROUTER", "ORCAROUTER", "DEEPSEEK", "VOLCENGINE", "OPENAI"),
"message" to message,
)
)
Expand Down
1 change: 1 addition & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ AI-powered commands such as `extract`, `summarize`, `chat`, `agent run`, and X-S
|---|---|
| DeepSeek | `DEEPSEEK_API_KEY` |
| OpenRouter | `OPENROUTER_API_KEY`, `OPENROUTER_MODEL_NAME`, `OPENROUTER_BASE_URL` |
| OrcaRouter | `ORCAROUTER_API_KEY`, `ORCAROUTER_MODEL_NAME`, `ORCAROUTER_BASE_URL` |
| Volcengine | `VOLCENGINE_API_KEY`, `VOLCENGINE_MODEL_NAME`, `VOLCENGINE_BASE_URL` |
| OpenAI-compatible | `OPENAI_API_KEY`, `OPENAI_MODEL_NAME`, `OPENAI_BASE_URL` |
| Aliyun Qwen | `OPENAI_API_KEY`, `OPENAI_MODEL_NAME`, `OPENAI_BASE_URL` |
Expand Down
18 changes: 18 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ openrouter.base.url=https://openrouter.ai/api/v1/ # optional

OpenRouter gives access to many models through one API. `model.name` defaults to a reasonable choice; override it to use any model available on OpenRouter (e.g. `bytedance-seed/seed-2.0-lite`).

### OrcaRouter

[OrcaRouter](https://www.orcarouter.ai) is an OpenAI-compatible routing gateway: a single key reaches many models (including free models) behind one endpoint. Browser4 registers it as a named provider at startup, so `ORCAROUTER_API_KEY` is recognized like any other LLM key.

```properties
orcarouter.api.key=sk-orca-...
orcarouter.model.name=orcarouter/auto
orcarouter.base.url=https://api.orcarouter.ai/v1 # optional
```

| Env var | Property | Default |
|-------------------------|-------------------------|---|
| `ORCAROUTER_API_KEY` | `orcarouter.api.key` | — |
| `ORCAROUTER_MODEL_NAME` | `orcarouter.model.name` | `orcarouter/auto` |
| `ORCAROUTER_BASE_URL` | `orcarouter.base.url` | `https://api.orcarouter.ai/v1` |

`orcarouter/auto` smart-routes to a suitable model; override `model.name` to pin a specific model (e.g. `orcarouter/fusion`).

### DeepSeek

```properties
Expand Down
1 change: 1 addition & 0 deletions skills/browser4-cli/references/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Agent commands require an LLM API key. Configure one provider via environment va
|---|---|
| DeepSeek | `DEEPSEEK_API_KEY` |
| OpenRouter | `OPENROUTER_API_KEY`, `OPENROUTER_MODEL_NAME`, `OPENROUTER_BASE_URL` |
| OrcaRouter | `ORCAROUTER_API_KEY`, `ORCAROUTER_MODEL_NAME`, `ORCAROUTER_BASE_URL` |
| Volcengine (ByteDance) | `VOLCENGINE_API_KEY`, `VOLCENGINE_MODEL_NAME`, `VOLCENGINE_BASE_URL` |
| OpenAI-compatible | `OPENAI_API_KEY`, `OPENAI_MODEL_NAME`, `OPENAI_BASE_URL` |
| Aliyun Qwen (DashScope) | `OPENAI_API_KEY`, `OPENAI_MODEL_NAME`, `OPENAI_BASE_URL` |
Expand Down