mirror of
https://github.com/block/goose.git
synced 2026-07-09 16:09:22 +00:00
feat(providers): support OpenRouter request parameters (#9276)
Signed-off-by: sunilkumarvalmiki <g.sunilkumarvalmiki@gmail.com> Signed-off-by: Douwe Osinga <douwe@sidewalklabs.com> Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com>
This commit is contained in:
parent
d1cc1439ec
commit
12c3e234f5
3 changed files with 179 additions and 2 deletions
|
|
@ -105,6 +105,11 @@ pub fn apply_reasoning_config(payload: &mut Value, model_config: &ModelConfig) {
|
|||
};
|
||||
|
||||
if let Some(obj) = payload.as_object_mut() {
|
||||
if obj.contains_key("reasoning") {
|
||||
obj.remove("reasoning_effort");
|
||||
return;
|
||||
}
|
||||
|
||||
let clamped_effort = obj
|
||||
.remove("reasoning_effort")
|
||||
.and_then(|value| value.as_str().map(str::to_owned));
|
||||
|
|
@ -201,6 +206,25 @@ mod tests {
|
|||
assert!(payload.get("reasoning_effort").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_reasoning_config_preserves_user_reasoning() {
|
||||
let mut payload = json!({
|
||||
"model": "openai/gpt-5",
|
||||
"messages": [],
|
||||
"reasoning": { "max_tokens": 2000 },
|
||||
"reasoning_effort": "high"
|
||||
});
|
||||
let mut model_config = ModelConfig::new("openai/gpt-5");
|
||||
let mut params = HashMap::new();
|
||||
params.insert("thinking_effort".to_string(), json!("high"));
|
||||
model_config.request_params = Some(params);
|
||||
|
||||
apply_reasoning_config(&mut payload, &model_config);
|
||||
|
||||
assert_eq!(payload["reasoning"], json!({ "max_tokens": 2000 }));
|
||||
assert!(payload.get("reasoning_effort").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_reasoning_config_disables_reasoning_capable_model() {
|
||||
let mut payload = json!({
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::future::BoxFuture;
|
||||
use goose_providers::images::ImageFormat;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::api_client::{ApiClient, AuthMethod};
|
||||
use super::base::{ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata};
|
||||
|
|
@ -17,6 +18,7 @@ use goose_providers::request_log::{start_log, LoggerHandleExt};
|
|||
use rmcp::model::Tool;
|
||||
|
||||
pub const OPENROUTER_PROVIDER_NAME: &str = "openrouter";
|
||||
const OPENROUTER_PARAMETERS_CONFIG_KEY: &str = "OPENROUTER_PARAMETERS";
|
||||
pub const OPENROUTER_DEFAULT_MODEL: &str = "anthropic/claude-sonnet-4";
|
||||
pub const OPENROUTER_DEFAULT_FAST_MODEL: &str = "google/gemini-2.5-flash";
|
||||
pub const OPENROUTER_MODEL_PREFIX_ANTHROPIC: &str = "anthropic";
|
||||
|
|
@ -43,6 +45,8 @@ pub struct OpenRouterProvider {
|
|||
supports_streaming: bool,
|
||||
#[serde(skip)]
|
||||
name: String,
|
||||
#[serde(skip)]
|
||||
configured_parameters: Option<HashMap<String, Value>>,
|
||||
}
|
||||
|
||||
impl OpenRouterProvider {
|
||||
|
|
@ -55,6 +59,8 @@ impl OpenRouterProvider {
|
|||
.get_param("OPENROUTER_HOST")
|
||||
.unwrap_or_else(|_| "https://openrouter.ai".to_string());
|
||||
|
||||
let configured_parameters = configured_openrouter_parameters()?;
|
||||
|
||||
let auth = AuthMethod::BearerToken(api_key);
|
||||
let api_client = ApiClient::new_with_tls(host, auth, tls_config)?
|
||||
.with_request_builder(crate::session_context::session_id_request_builder())
|
||||
|
|
@ -65,6 +71,7 @@ impl OpenRouterProvider {
|
|||
api_client,
|
||||
supports_streaming: true,
|
||||
name: OPENROUTER_PROVIDER_NAME.to_string(),
|
||||
configured_parameters,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -146,6 +153,39 @@ fn is_gemini_model(model_name: &str) -> bool {
|
|||
model_name.starts_with("google/")
|
||||
}
|
||||
|
||||
fn parse_openrouter_parameters(raw: Value) -> Result<HashMap<String, Value>> {
|
||||
match raw {
|
||||
Value::Object(params) => Ok(params.into_iter().collect()),
|
||||
Value::String(raw_json) => match serde_json::from_str::<Value>(&raw_json)? {
|
||||
Value::Object(params) => Ok(params.into_iter().collect()),
|
||||
_ => bail!("{OPENROUTER_PARAMETERS_CONFIG_KEY} must be a JSON object"),
|
||||
},
|
||||
_ => bail!("{OPENROUTER_PARAMETERS_CONFIG_KEY} must be a JSON object"),
|
||||
}
|
||||
}
|
||||
|
||||
fn configured_openrouter_parameters() -> Result<Option<HashMap<String, Value>>> {
|
||||
let config = crate::config::Config::global();
|
||||
match config.get_param::<Value>(OPENROUTER_PARAMETERS_CONFIG_KEY) {
|
||||
Ok(raw) => parse_openrouter_parameters(raw).map(Some),
|
||||
Err(crate::config::ConfigError::NotFound(_)) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_request_params(
|
||||
request_params: &mut Option<HashMap<String, Value>>,
|
||||
params: HashMap<String, Value>,
|
||||
) {
|
||||
request_params
|
||||
.get_or_insert_with(HashMap::new)
|
||||
.extend(params);
|
||||
}
|
||||
|
||||
fn merge_openrouter_parameters(model: &mut ModelConfig, params: HashMap<String, Value>) {
|
||||
merge_request_params(&mut model.request_params, params);
|
||||
}
|
||||
|
||||
impl goose_providers::base::ProviderDescriptor for OpenRouterProvider {
|
||||
fn metadata() -> ProviderMetadata {
|
||||
ProviderMetadata::new(
|
||||
|
|
@ -164,6 +204,7 @@ impl goose_providers::base::ProviderDescriptor for OpenRouterProvider {
|
|||
Some("https://openrouter.ai"),
|
||||
false,
|
||||
),
|
||||
ConfigKey::new(OPENROUTER_PARAMETERS_CONFIG_KEY, false, false, None, false),
|
||||
],
|
||||
)
|
||||
.with_setup_steps(vec![
|
||||
|
|
@ -248,6 +289,16 @@ impl Provider for OpenRouterProvider {
|
|||
tools: &[Tool],
|
||||
) -> Result<MessageStream, ProviderError> {
|
||||
let session_id = crate::session_context::current_session_id().unwrap_or_default();
|
||||
|
||||
let mut merged_model;
|
||||
let model_config = if let Some(params) = &self.configured_parameters {
|
||||
merged_model = model_config.clone();
|
||||
merge_openrouter_parameters(&mut merged_model, params.clone());
|
||||
&merged_model
|
||||
} else {
|
||||
model_config
|
||||
};
|
||||
|
||||
let mut payload = create_request(
|
||||
model_config,
|
||||
system,
|
||||
|
|
@ -301,3 +352,82 @@ fn supports_cache_control(model: &ModelConfig) -> bool {
|
|||
.model_name
|
||||
.starts_with(OPENROUTER_MODEL_PREFIX_ANTHROPIC)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use goose_providers::base::ProviderDescriptor;
|
||||
|
||||
fn model_config(model_name: &str) -> ModelConfig {
|
||||
ModelConfig {
|
||||
model_name: model_name.to_string(),
|
||||
context_limit: None,
|
||||
temperature: None,
|
||||
max_tokens: None,
|
||||
toolshim: false,
|
||||
toolshim_model: None,
|
||||
request_params: None,
|
||||
reasoning: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_includes_openrouter_parameters_config_key() {
|
||||
let metadata = OpenRouterProvider::metadata();
|
||||
|
||||
assert!(metadata
|
||||
.config_keys
|
||||
.iter()
|
||||
.any(|key| key.name == OPENROUTER_PARAMETERS_CONFIG_KEY));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_openrouter_parameters_accepts_object_value() {
|
||||
let params = parse_openrouter_parameters(json!({
|
||||
"verbosity": "xhigh",
|
||||
"reasoning": { "effort": "high" }
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(params["verbosity"], json!("xhigh"));
|
||||
assert_eq!(params["reasoning"], json!({ "effort": "high" }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_openrouter_parameters_accepts_json_string_value() {
|
||||
let params = parse_openrouter_parameters(json!(
|
||||
r#"{"plugins":[{"id":"web"}],"reasoning":{"max_tokens":2000}}"#
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(params["plugins"], json!([{ "id": "web" }]));
|
||||
assert_eq!(params["reasoning"], json!({ "max_tokens": 2000 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_openrouter_parameters_rejects_non_object_json_string() {
|
||||
let err = parse_openrouter_parameters(json!(r#"["web"]"#)).unwrap_err();
|
||||
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("OPENROUTER_PARAMETERS must be a JSON object"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_openrouter_parameters_updates_model_request_params() {
|
||||
let mut model = model_config("anthropic/claude-sonnet-4");
|
||||
model.request_params = Some(HashMap::from([("verbosity".to_string(), json!("low"))]));
|
||||
|
||||
let params = parse_openrouter_parameters(json!({
|
||||
"plugins": [{ "id": "web" }],
|
||||
"verbosity": "xhigh"
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
merge_openrouter_parameters(&mut model, params);
|
||||
|
||||
let request_params = model.request_params.as_ref().unwrap();
|
||||
assert_eq!(request_params["plugins"], json!([{ "id": "web" }]));
|
||||
assert_eq!(request_params["verbosity"], json!("xhigh"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ goose is compatible with a wide range of LLM providers, allowing you to choose a
|
|||
| [Ollama](https://ollama.com/) | Local model runner supporting Qwen, Llama, DeepSeek, and other open-source models. **Because this provider runs locally, you must first [download and run a model](#local-llms).** | `OLLAMA_HOST` |
|
||||
| [Ollama Cloud](https://ollama.com/) | Access hosted models on ollama.com via OpenAI-compatible API. Requires an Ollama account and API key. | `OLLAMA_CLOUD_API_KEY` |
|
||||
| [OpenAI](https://platform.openai.com/api-keys) | Provides gpt-4o, o1, and other advanced language models. Also supports OpenAI-compatible endpoints (e.g., self-hosted LLaMA, vLLM, KServe). **o1-mini and o1-preview are not supported because goose uses tool calling.** | `OPENAI_API_KEY`, `OPENAI_HOST` (optional), `OPENAI_ORGANIZATION` (optional), `OPENAI_PROJECT` (optional), `OPENAI_CUSTOM_HEADERS` (optional) |
|
||||
| [OpenRouter](https://openrouter.ai/) | API gateway for unified access to various models with features like rate-limiting management. | `OPENROUTER_API_KEY` |
|
||||
| [OpenRouter](https://openrouter.ai/) | API gateway for unified access to various models with features like rate-limiting management. | `OPENROUTER_API_KEY`, `OPENROUTER_HOST` (optional), `OPENROUTER_PARAMETERS` (optional) |
|
||||
| [Perplexity](https://www.perplexity.ai/) | Chat models with built-in real-time web search grounding. OpenAI-compatible chat completions API at `https://api.perplexity.ai`. | `PERPLEXITY_API_KEY` |
|
||||
| [OVHcloud AI](https://www.ovhcloud.com/en/public-cloud/ai-endpoints/) | Provides access to open-source models including Qwen, Llama, Mistral, and DeepSeek through AI Endpoints service. | `OVHCLOUD_API_KEY` |
|
||||
| [Ramalama](https://ramalama.ai/) | Local model using native [OCI](https://opencontainers.org/) container runtimes, [CNCF](https://www.cncf.io/) tools, and supporting models as OCI artifacts. Ramalama API is a compatible alternative to Ollama and can be used with the goose Ollama provider. Supports Qwen, Llama, DeepSeek, and other open-source models. **Because this provider runs locally, you must first [download and run a model](#local-llms).** | `OLLAMA_HOST` |
|
||||
|
|
@ -1398,6 +1398,29 @@ Here are some local providers we support:
|
|||
|
||||
|
||||
|
||||
## OpenRouter Advanced Parameters
|
||||
|
||||
OpenRouter accepts provider-specific request parameters such as `verbosity`, `reasoning`, `plugins`, `require_parameters`, and other supported fields. Set `OPENROUTER_PARAMETERS` in your `config.yaml` to add these fields to every OpenRouter chat completion request.
|
||||
|
||||
You can use a YAML object:
|
||||
|
||||
```yaml
|
||||
OPENROUTER_PARAMETERS:
|
||||
verbosity: xhigh
|
||||
reasoning:
|
||||
effort: high
|
||||
plugins:
|
||||
- id: web
|
||||
```
|
||||
|
||||
Or a JSON string:
|
||||
|
||||
```yaml
|
||||
OPENROUTER_PARAMETERS: '{"verbosity":"xhigh","plugins":[{"id":"web"}]}'
|
||||
```
|
||||
|
||||
goose ignores reserved request fields it already manages, such as `model`, `messages`, `stream`, and `stream_options`. Other OpenRouter-specific top-level fields are passed through the shared OpenAI-compatible request parameter handling.
|
||||
|
||||
## GitHub Copilot Authentication
|
||||
|
||||
GitHub Copilot uses a device flow for authentication, so no API keys are required:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue