-
Notifications
You must be signed in to change notification settings - Fork 150
feat(llm): register OrcaRouter as a named OpenAI-compatible provider #569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
' || 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
🧰 Tools🪛 PSScriptAnalyzer (1.25.0)[warning] Missing BOM encoding for non-ASCII encoded file 'env-manage.ps1' (PSUseBOMForUnicodeEncodedFile) 🤖 Prompt for AI AgentsSource: 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} | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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/**' . \
|| 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.
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| ?: System.getenv("VOLCENGINE_API_KEY") | ||
| ?: System.getenv("OPENAI_API_KEY") | ||
|
|
||
|
|
@@ -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" + | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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-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.
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -124,6 +124,7 @@ class DoctorController( | |
| fun llmStatus(): ResponseEntity<Map<String, Any?>> { | ||
| val envKeyNames = listOf( | ||
| "OPENROUTER_API_KEY", | ||
| "ORCAROUTER_API_KEY", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Use 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 AgentsSource: Coding guidelines |
||
| "DEEPSEEK_API_KEY", | ||
| "VOLCENGINE_API_KEY", | ||
| "OPENAI_API_KEY", | ||
|
|
@@ -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", | ||
|
|
@@ -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( | ||
|
|
@@ -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, | ||
| ) | ||
| ) | ||
|
|
||
There was a problem hiding this comment.
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:
Repository: platonai/Browser4
Length of output: 370
🏁 Script executed:
Repository: platonai/Browser4
Length of output: 772
🏁 Script executed:
Repository: platonai/Browser4
Length of output: 50375
🏁 Script executed:
Repository: platonai/Browser4
Length of output: 2023
🏁 Script executed:
Repository: platonai/Browser4
Length of output: 839
Keep provider API keys out of Docker image configuration.
Dockerfile,Dockerfile.fast, andDockerfile.nativeall useOPENROUTER_API_KEY=${OPENROUTER_API_KEY}andORCAROUTER_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 toOPENROUTER_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