diff --git a/AGENTS.md b/AGENTS.md index 3cd2572cf2..96bbf4cfe1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/Dockerfile b/Dockerfile index 2db3e9241c..460e0dc14f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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} \ PROXY_ROTATION_URL=${PROXY_ROTATION_URL} \ BROWSER_CONTEXT_MODE=DEFAULT \ BROWSER_CONTEXT_NUMBER=2 \ diff --git a/Dockerfile.fast b/Dockerfile.fast index 27cc919f94..8cf1cef945 100644 --- a/Dockerfile.fast +++ b/Dockerfile.fast @@ -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 \ diff --git a/Dockerfile.native b/Dockerfile.native index 7cc909d7f1..7588295e16 100644 --- a/Dockerfile.native +++ b/Dockerfile.native @@ -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 \ diff --git a/README.md b/README.md index e92a9be2a8..dd4370ed38 100644 --- a/README.md +++ b/README.md @@ -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` | diff --git a/README.zh.md b/README.zh.md index 87e28b973e..ec6e2f80b8 100644 --- a/README.zh.md +++ b/README.zh.md @@ -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` | diff --git a/application.properties b/application.properties index 9e0d311951..aa97339681 100644 --- a/application.properties +++ b/application.properties @@ -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) @@ -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-... @@ -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 diff --git a/bin/env-manage.ps1 b/bin/env-manage.ps1 index ff178a496e..ba25e341ac 100644 --- a/bin/env-manage.ps1 +++ b/bin/env-manage.ps1 @@ -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} @{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} diff --git a/bin/env-manage.sh b/bin/env-manage.sh index 3b0ab1cebb..619e7c7b92 100644 --- a/bin/env-manage.sh +++ b/bin/env-manage.sh @@ -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" diff --git a/browser4-agentic/src/main/kotlin/ai/platon/pulsar/agentic/tools/builtin/AgentToolExecutor.kt b/browser4-agentic/src/main/kotlin/ai/platon/pulsar/agentic/tools/builtin/AgentToolExecutor.kt index 945a6b4726..f915313fcd 100644 --- a/browser4-agentic/src/main/kotlin/ai/platon/pulsar/agentic/tools/builtin/AgentToolExecutor.kt +++ b/browser4-agentic/src/main/kotlin/ai/platon/pulsar/agentic/tools/builtin/AgentToolExecutor.kt @@ -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") ?: 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" + diff --git a/browser4-apps/browser4-bundle/src/main/kotlin/ai/platon/pulsar/apps/Browser4BundleApplication.kt b/browser4-apps/browser4-bundle/src/main/kotlin/ai/platon/pulsar/apps/Browser4BundleApplication.kt index 75388e025f..be8164122d 100644 --- a/browser4-apps/browser4-bundle/src/main/kotlin/ai/platon/pulsar/apps/Browser4BundleApplication.kt +++ b/browser4-apps/browser4-bundle/src/main/kotlin/ai/platon/pulsar/apps/Browser4BundleApplication.kt @@ -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) diff --git a/browser4-apps/browser4-standalone/src/main/kotlin/ai/platon/pulsar/apps/Browser4StandaloneApplication.kt b/browser4-apps/browser4-standalone/src/main/kotlin/ai/platon/pulsar/apps/Browser4StandaloneApplication.kt index c593b54a47..6fd86d27d6 100644 --- a/browser4-apps/browser4-standalone/src/main/kotlin/ai/platon/pulsar/apps/Browser4StandaloneApplication.kt +++ b/browser4-apps/browser4-standalone/src/main/kotlin/ai/platon/pulsar/apps/Browser4StandaloneApplication.kt @@ -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) diff --git a/browser4-boot/src/main/kotlin/ai/platon/pulsar/boot/autoconfigure/PulsarContextInitializer.kt b/browser4-boot/src/main/kotlin/ai/platon/pulsar/boot/autoconfigure/PulsarContextInitializer.kt index 47dca82973..9be2b6352a 100644 --- a/browser4-boot/src/main/kotlin/ai/platon/pulsar/boot/autoconfigure/PulsarContextInitializer.kt +++ b/browser4-boot/src/main/kotlin/ai/platon/pulsar/boot/autoconfigure/PulsarContextInitializer.kt @@ -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 { 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) + } + } } } diff --git a/browser4-rest/src/main/kotlin/ai/platon/pulsar/rest/api/controller/DoctorController.kt b/browser4-rest/src/main/kotlin/ai/platon/pulsar/rest/api/controller/DoctorController.kt index 8fc819bd27..3a2f06c89b 100644 --- a/browser4-rest/src/main/kotlin/ai/platon/pulsar/rest/api/controller/DoctorController.kt +++ b/browser4-rest/src/main/kotlin/ai/platon/pulsar/rest/api/controller/DoctorController.kt @@ -124,6 +124,7 @@ class DoctorController( fun llmStatus(): ResponseEntity> { val envKeyNames = listOf( "OPENROUTER_API_KEY", + "ORCAROUTER_API_KEY", "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, ) ) diff --git a/cli/README.md b/cli/README.md index 9ed9395206..50c2c95eb8 100644 --- a/cli/README.md +++ b/cli/README.md @@ -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` | diff --git a/docs/config.md b/docs/config.md index 7b8cdc6532..ef4cb811a9 100644 --- a/docs/config.md +++ b/docs/config.md @@ -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 diff --git a/skills/browser4-cli/references/agent.md b/skills/browser4-cli/references/agent.md index 9628251126..5bdb54d92c 100644 --- a/skills/browser4-cli/references/agent.md +++ b/skills/browser4-cli/references/agent.md @@ -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` |