feat(llm): register OrcaRouter as a named OpenAI-compatible provider - #569
feat(llm): register OrcaRouter as a named OpenAI-compatible provider#569XiaoHuo888-hue wants to merge 1 commit into
Conversation
Register OrcaRouter via ChatModelFactory.registerProvider in PulsarContextInitializer so ORCAROUTER_API_KEY is recognized like any other named LLM provider and routes to https://api.orcarouter.ai/v1 (default model orcarouter/auto). Also surface the provider across the config surface: - DoctorController llm-status env keys/property keys/key prefixes - AgentToolExecutor fallback chain + help message - both app help messages - application.properties, Dockerfiles, env-manage scripts - README / docs / AGENTS / CLI reference tables Co-Authored-By: Claude <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe change adds OrcaRouter as an LLM provider. It registers provider defaults, detects its API key, exposes it in containers and environment tools, updates status messages, and documents configuration options. ChangesOrcaRouter provider support
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Application
participant PulsarContextInitializer
participant LLMProviderConfiguration
Application->>PulsarContextInitializer: initialize application context
PulsarContextInitializer->>LLMProviderConfiguration: register OrcaRouter provider
LLMProviderConfiguration-->>PulsarContextInitializer: provider registration result
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
browser4-boot/src/main/kotlin/ai/platon/pulsar/boot/autoconfigure/PulsarContextInitializer.kt (2)
31-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDeclare the public function return type.
Line [31] should declare
registerOrcaRouterProvider(): Unit.As per coding guidelines, Kotlin code should prefer explicit return types.
Suggested change
-fun registerOrcaRouterProvider() { +fun registerOrcaRouterProvider(): Unit {🤖 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` at line 31, Update the public function registerOrcaRouterProvider in PulsarContextInitializer to explicitly declare a Unit return type, preserving its existing implementation.Source: Coding guidelines
31-45: 🩺 Stability & Availability | 🔵 TrivialMake configured-provider registration failure observable.
When
ChatModelFactory.registerProviderfails, Lines [31]-[45] only emit a warning and continue startup. IfORCAROUTER_API_KEYis configured, later requests can fail because the provider was not registered. Fail startup for an explicitly configured provider, or expose an unavailable-provider state to status and request selection.🤖 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, Update registerOrcaRouterProvider so a ChatModelFactory.registerProvider failure is not silently tolerated when ORCAROUTER_API_KEY is configured: fail startup or mark the provider unavailable for status and request selection. Preserve non-fatal behavior only when the provider is not explicitly configured, and ensure the existing warning includes the failure context.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@bin/env-manage.ps1`:
- 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.
In
`@browser4-agentic/src/main/kotlin/ai/platon/pulsar/agentic/tools/builtin/AgentToolExecutor.kt`:
- 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.
In
`@browser4-boot/src/main/kotlin/ai/platon/pulsar/boot/autoconfigure/PulsarContextInitializer.kt`:
- Around line 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.
In
`@browser4-rest/src/main/kotlin/ai/platon/pulsar/rest/api/controller/DoctorController.kt`:
- 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.
In `@Dockerfile`:
- 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.
---
Nitpick comments:
In
`@browser4-boot/src/main/kotlin/ai/platon/pulsar/boot/autoconfigure/PulsarContextInitializer.kt`:
- Line 31: Update the public function registerOrcaRouterProvider in
PulsarContextInitializer to explicitly declare a Unit return type, preserving
its existing implementation.
- Around line 31-45: Update registerOrcaRouterProvider so a
ChatModelFactory.registerProvider failure is not silently tolerated when
ORCAROUTER_API_KEY is configured: fail startup or mark the provider unavailable
for status and request selection. Preserve non-fatal behavior only when the
provider is not explicitly configured, and ensure the existing warning includes
the failure context.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c9d07693-efe4-4013-b60f-4a68fb510ff0
📒 Files selected for processing (17)
AGENTS.mdDockerfileDockerfile.fastDockerfile.nativeREADME.mdREADME.zh.mdapplication.propertiesbin/env-manage.ps1bin/env-manage.shbrowser4-agentic/src/main/kotlin/ai/platon/pulsar/agentic/tools/builtin/AgentToolExecutor.ktbrowser4-apps/browser4-bundle/src/main/kotlin/ai/platon/pulsar/apps/Browser4BundleApplication.ktbrowser4-apps/browser4-standalone/src/main/kotlin/ai/platon/pulsar/apps/Browser4StandaloneApplication.ktbrowser4-boot/src/main/kotlin/ai/platon/pulsar/boot/autoconfigure/PulsarContextInitializer.ktbrowser4-rest/src/main/kotlin/ai/platon/pulsar/rest/api/controller/DoctorController.ktcli/README.mddocs/config.mdskills/browser4-cli/references/agent.md
| @{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} |
There was a problem hiding this comment.
📐 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
' || trueRepository: 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',' '))
PYRepository: 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
| ?: System.getenv("LLM_API_KEY") | ||
| ?: System.getenv("DEEPSEEK_API_KEY") | ||
| ?: System.getenv("OPENROUTER_API_KEY") | ||
| ?: System.getenv("ORCAROUTER_API_KEY") |
There was a problem hiding this comment.
📐 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/**' . \
|| trueRepository: 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 || trueRepository: 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
| 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) | ||
| } |
There was a problem hiding this comment.
📐 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-agenticRepository: 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' || trueRepository: 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 || trueRepository: 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
| fun llmStatus(): ResponseEntity<Map<String, Any?>> { | ||
| val envKeyNames = listOf( | ||
| "OPENROUTER_API_KEY", | ||
| "ORCAROUTER_API_KEY", |
There was a problem hiding this comment.
🎯 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
| # 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} \ |
There was a problem hiding this comment.
🔒 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"
fiRepository: 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))
PYRepository: 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" . || trueRepository: 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))
PYRepository: 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
doneRepository: 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: removeOPENROUTER_API_KEYandORCAROUTER_API_KEY.Dockerfile.fast#L35-L36: removeOPENROUTER_API_KEYandORCAROUTER_API_KEY.Dockerfile.native#L66-L67: removeOPENROUTER_API_KEYandORCAROUTER_API_KEY.
📍 Affects 3 files
Dockerfile#L68-L68(this comment)Dockerfile.fast#L36-L36Dockerfile.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.
What
Registers OrcaRouter as a named, OpenAI-compatible LLM provider. At startup
PulsarContextInitializer(the sharedApplicationContextInitializerused by both the standalone and bundle apps) callsChatModelFactory.registerProvider(...), soORCAROUTER_API_KEYis recognized byChatModelFactoryexactly likeOPENROUTER_API_KEYand LLM requests route tohttps://api.orcarouter.ai/v1, with the smart-routingorcarouter/autodefault model.The provider is also surfaced across the config surface:
DoctorController.llm-statusenv-key / property-key lists and key prefixes, theAgentToolExecutormanual key fallback + help text, both app startup help messages,application.properties, the threeDockerfiles,bin/env-manage.sh/ps1, and the provider tables inREADME.md,README.zh.md,docs/config.md,cli/README.md,skills/browser4-cli/references/agent.md, andAGENTS.md.Why a named provider
Browser4's LLM SDK resolves providers through a data-driven registry keyed by API-key name. Without this registration an
ORCAROUTER_API_KEYwould not be recognized as a supported key and requests would fall back toapi.openai.com/v1. Registeringorcaroutermakes it first-class, so no generic passthrough is involved — the same treatment OpenRouter already gets.OrcaRouter is an OpenAI-compatible model routing gateway that brings 150+ models from OpenAI, Anthropic, Google, DeepSeek, Qwen, MiniMax and xAI behind a single endpoint and API key. Beyond routing, it runs gateway-level, zero-trust security for AI agents on the same endpoint — screening every prompt/response and governing every tool call on a default-deny basis, with no application code changes. This PR registers it as a named provider so users can opt in directly.
Verification
mvn install -DskipTestsforbrowser4-boot,browser4-rest,browser4-standalone,browser4-bundle(with-am -P all-main-modules): BUILD SUCCESS across all 15 reactor modules.pulsar-llmSDK with a real key:registerProvidermakesSUPPORTED_API_KEY_NAMESincludeORCAROUTER_API_KEY;POST /v1/chat/completionswithorcarouter/auto→ HTTP 200 (smart-routed togemini-3.1-flash-lite-preview,usagereturned).I'm an engineer on the OrcaRouter team.
Summary by CodeRabbit
New Features
Documentation