migrate openrouter provider to goose-providers (#11757)
This commit is contained in:
@@ -16,6 +16,8 @@ pub mod local_inference;
|
||||
pub mod ollama;
|
||||
pub mod openai;
|
||||
pub mod openai_compatible;
|
||||
pub mod openrouter;
|
||||
pub mod openrouter_format;
|
||||
|
||||
pub use declarative::declarative_providers::*;
|
||||
|
||||
|
||||
+41
-123
@@ -1,22 +1,23 @@
|
||||
use anyhow::{bail, Result};
|
||||
use crate::images::ImageFormat;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use futures::future::BoxFuture;
|
||||
use goose_providers::images::ImageFormat;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ops::Range;
|
||||
|
||||
use super::api_client::{ApiClient, AuthMethod};
|
||||
use super::base::{ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata};
|
||||
use super::openai_compatible::{handle_status, stream_openai_compat};
|
||||
use super::retry::ProviderRetry;
|
||||
use crate::api_client::ApiClient;
|
||||
|
||||
pub type OpenRouterSessionIdProvider = Box<dyn Fn() -> Option<String> + Send + Sync>;
|
||||
use crate::base::{ConfigKey, MessageStream, Provider, ProviderMetadata};
|
||||
use crate::cache_semantics::{apply_chat_payload_breakpoints, CacheSemantics};
|
||||
use crate::conversation::message::Message;
|
||||
use crate::providers::formats::openrouter as openrouter_format;
|
||||
use goose_providers::cache_semantics::{apply_chat_payload_breakpoints, CacheSemantics};
|
||||
use goose_providers::errors::ProviderError;
|
||||
use goose_providers::formats::openai::create_request;
|
||||
use goose_providers::model::ModelConfig;
|
||||
use goose_providers::request_log::{start_log, LoggerHandleExt};
|
||||
use crate::errors::ProviderError;
|
||||
use crate::formats::openai::create_request;
|
||||
use crate::model::ModelConfig;
|
||||
use crate::openai_compatible::{handle_status, stream_openai_compat};
|
||||
use crate::openrouter_format;
|
||||
use crate::request_log::{start_log, LoggerHandleExt};
|
||||
use crate::retry::ProviderRetry;
|
||||
use rmcp::model::Tool;
|
||||
|
||||
pub const OPENROUTER_PROVIDER_NAME: &str = "openrouter";
|
||||
@@ -51,33 +52,23 @@ pub struct OpenRouterProvider {
|
||||
name: String,
|
||||
#[serde(skip)]
|
||||
configured_parameters: Option<HashMap<String, Value>>,
|
||||
#[serde(skip)]
|
||||
session_id_provider: Option<OpenRouterSessionIdProvider>,
|
||||
}
|
||||
|
||||
impl OpenRouterProvider {
|
||||
pub async fn from_env(
|
||||
tls_config: Option<crate::providers::api_client::TlsConfig>,
|
||||
) -> Result<Self> {
|
||||
let config = crate::config::Config::global();
|
||||
let api_key: String = config.get_secret("OPENROUTER_API_KEY")?;
|
||||
let host: String = config
|
||||
.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())
|
||||
.with_header("HTTP-Referer", "https://goose-docs.ai")?
|
||||
.with_header("X-Title", "goose")?
|
||||
.with_header("X-OpenRouter-Categories", "cli-agent,productivity")?;
|
||||
|
||||
Ok(Self {
|
||||
pub fn new(
|
||||
api_client: ApiClient,
|
||||
configured_parameters: Option<HashMap<String, Value>>,
|
||||
session_id_provider: Option<OpenRouterSessionIdProvider>,
|
||||
) -> Self {
|
||||
Self {
|
||||
api_client,
|
||||
supports_streaming: true,
|
||||
name: OPENROUTER_PROVIDER_NAME.to_string(),
|
||||
configured_parameters,
|
||||
})
|
||||
session_id_provider,
|
||||
}
|
||||
}
|
||||
|
||||
async fn post_chat_completions(
|
||||
@@ -368,26 +359,6 @@ fn escape_gemini_schema_ref_keys_in_tool_responses(payload: &mut Value) -> usize
|
||||
escaped
|
||||
}
|
||||
|
||||
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>,
|
||||
@@ -401,7 +372,7 @@ fn merge_openrouter_parameters(model: &mut ModelConfig, params: HashMap<String,
|
||||
merge_request_params(&mut model.request_params, params);
|
||||
}
|
||||
|
||||
impl goose_providers::base::ProviderDescriptor for OpenRouterProvider {
|
||||
impl crate::base::ProviderDescriptor for OpenRouterProvider {
|
||||
fn metadata() -> ProviderMetadata {
|
||||
ProviderMetadata::new(
|
||||
OPENROUTER_PROVIDER_NAME,
|
||||
@@ -422,32 +393,10 @@ impl goose_providers::base::ProviderDescriptor for OpenRouterProvider {
|
||||
ConfigKey::new(OPENROUTER_PARAMETERS_CONFIG_KEY, false, false, None, false),
|
||||
],
|
||||
)
|
||||
.with_setup(
|
||||
crate::providers::catalog::ProviderSetupMetadata::api_key(
|
||||
crate::providers::catalog::ProviderSetupGroup::Default,
|
||||
)
|
||||
.with_docs_url("https://openrouter.ai/keys"),
|
||||
)
|
||||
.with_setup_steps(vec![
|
||||
"Go to https://openrouter.ai/settings/keys",
|
||||
"Click 'Create' or use an existing API key",
|
||||
"Copy the key and paste it above",
|
||||
])
|
||||
.with_fast_model(OPENROUTER_DEFAULT_FAST_MODEL)
|
||||
}
|
||||
}
|
||||
|
||||
impl ProviderDef for OpenRouterProvider {
|
||||
type Provider = Self;
|
||||
|
||||
fn from_env(
|
||||
_extensions: Vec<crate::config::ExtensionConfig>,
|
||||
tls_config: Option<crate::providers::api_client::TlsConfig>,
|
||||
) -> BoxFuture<'static, Result<Self::Provider>> {
|
||||
Box::pin(Self::from_env(tls_config))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Provider for OpenRouterProvider {
|
||||
fn get_name(&self) -> &str {
|
||||
@@ -522,7 +471,11 @@ impl Provider for OpenRouterProvider {
|
||||
messages: &[Message],
|
||||
tools: &[Tool],
|
||||
) -> Result<MessageStream, ProviderError> {
|
||||
let session_id = crate::session_context::current_session_id().unwrap_or_default();
|
||||
let session_id = self
|
||||
.session_id_provider
|
||||
.as_ref()
|
||||
.and_then(|provider| provider())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut merged_model;
|
||||
let model_config = if let Some(params) = &self.configured_parameters {
|
||||
@@ -592,7 +545,7 @@ impl Provider for OpenRouterProvider {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use goose_providers::base::ProviderDescriptor;
|
||||
use crate::base::ProviderDescriptor;
|
||||
|
||||
fn model_config(model_name: &str) -> ModelConfig {
|
||||
ModelConfig {
|
||||
@@ -619,48 +572,14 @@ mod tests {
|
||||
.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();
|
||||
let params = HashMap::from([
|
||||
("plugins".to_string(), json!([{ "id": "web" }])),
|
||||
("verbosity".to_string(), json!("xhigh")),
|
||||
]);
|
||||
|
||||
merge_openrouter_parameters(&mut model, params);
|
||||
|
||||
@@ -700,17 +619,16 @@ mod tests {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let provider = OpenRouterProvider {
|
||||
api_client: ApiClient::new_with_tls(
|
||||
let provider = OpenRouterProvider::new(
|
||||
ApiClient::new_with_tls(
|
||||
server.uri(),
|
||||
AuthMethod::BearerToken("test-key".to_string()),
|
||||
crate::api_client::AuthMethod::BearerToken("test-key".to_string()),
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
supports_streaming: true,
|
||||
name: OPENROUTER_PROVIDER_NAME.to_string(),
|
||||
configured_parameters: None,
|
||||
};
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let mut config = model_config("google/gemini-3.5-flash");
|
||||
config.reasoning = Some(true);
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
use crate::conversation::message::{Message, MessageContent, ProviderMetadata};
|
||||
use goose_providers::formats::openai;
|
||||
use goose_providers::model::ModelConfig;
|
||||
use goose_providers::thinking::ThinkingEffort;
|
||||
use crate::formats::openai;
|
||||
use crate::model::ModelConfig;
|
||||
use crate::thinking::ThinkingEffort;
|
||||
use rmcp::model::Role;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
@@ -167,7 +167,7 @@ pub fn configure_openrouter(config: &Config, api_key: String) -> Result<()> {
|
||||
config.set_secret("OPENROUTER_API_KEY", &api_key)?;
|
||||
crate::config::set_active_provider(
|
||||
config,
|
||||
crate::providers::openrouter::OPENROUTER_PROVIDER_NAME,
|
||||
goose_providers::openrouter::OPENROUTER_PROVIDER_NAME,
|
||||
OPENROUTER_DEFAULT_MODEL,
|
||||
)?;
|
||||
Ok(())
|
||||
|
||||
@@ -29,7 +29,9 @@ pub mod google {
|
||||
create_request_with_thinking_budget(model_config, system, messages, tools, thinking_budget)
|
||||
}
|
||||
}
|
||||
pub mod openrouter;
|
||||
pub mod openrouter {
|
||||
pub use goose_providers::openrouter_format::*;
|
||||
}
|
||||
pub mod snowflake {
|
||||
pub use goose_providers::formats::snowflake::*;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ use super::{
|
||||
kimicode::KimiCodeProvider,
|
||||
litellm::LiteLLMProvider,
|
||||
nanogpt::NanoGptProvider,
|
||||
openrouter::OpenRouterProvider,
|
||||
pi_acp::PiAcpProvider,
|
||||
provider_registry::ProviderRegistry,
|
||||
snowflake_def::SnowflakeProviderDef,
|
||||
@@ -45,6 +44,7 @@ use crate::providers::databricks_v2_def::{self, DatabricksV2ProviderDef};
|
||||
use crate::providers::google_def::GoogleProviderDef;
|
||||
use crate::providers::ollama_def::OllamaProviderDef;
|
||||
use crate::providers::openai_def::OpenAiProviderDef;
|
||||
use crate::providers::openrouter_def::OpenRouterProviderDef;
|
||||
use crate::{
|
||||
config::declarative_providers::register_declarative_providers,
|
||||
providers::provider_registry::ProviderEntry,
|
||||
@@ -157,7 +157,7 @@ async fn init_registry() -> RwLock<ProviderRegistry> {
|
||||
true,
|
||||
Some(registrations::openai_inventory()),
|
||||
);
|
||||
registry.register_with_inventory::<OpenRouterProvider>(
|
||||
registry.register_with_inventory::<OpenRouterProviderDef>(
|
||||
true,
|
||||
Some(registrations::refresh_only().with_configured(|| {
|
||||
let config = crate::config::Config::global();
|
||||
|
||||
@@ -70,7 +70,10 @@ pub mod openai {
|
||||
pub mod openai_compatible {
|
||||
pub use goose_providers::openai_compatible::*;
|
||||
}
|
||||
pub mod openrouter;
|
||||
pub mod openrouter {
|
||||
pub use goose_providers::openrouter::*;
|
||||
}
|
||||
pub mod openrouter_def;
|
||||
pub mod pi_acp;
|
||||
pub(crate) mod private_file;
|
||||
pub mod provider_registry;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
use anyhow::{bail, Result};
|
||||
use futures::future::BoxFuture;
|
||||
use goose_providers::{
|
||||
api_client::{ApiClient, AuthMethod, TlsConfig},
|
||||
base::{ProviderDescriptor, ProviderMetadata},
|
||||
openrouter::OpenRouterProvider,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
config::{Config, ConfigError, ExtensionConfig},
|
||||
providers::base::ProviderDef,
|
||||
};
|
||||
|
||||
const OPENROUTER_PARAMETERS_CONFIG_KEY: &str = "OPENROUTER_PARAMETERS";
|
||||
|
||||
pub struct OpenRouterProviderDef;
|
||||
|
||||
impl ProviderDescriptor for OpenRouterProviderDef {
|
||||
fn metadata() -> ProviderMetadata {
|
||||
OpenRouterProvider::metadata()
|
||||
.with_setup(
|
||||
crate::providers::catalog::ProviderSetupMetadata::api_key(
|
||||
crate::providers::catalog::ProviderSetupGroup::Default,
|
||||
)
|
||||
.with_docs_url("https://openrouter.ai/keys"),
|
||||
)
|
||||
.with_setup_steps(vec![
|
||||
"Go to https://openrouter.ai/settings/keys",
|
||||
"Click 'Create' or use an existing API key",
|
||||
"Copy the key and paste it above",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl ProviderDef for OpenRouterProviderDef {
|
||||
type Provider = OpenRouterProvider;
|
||||
|
||||
fn from_env(
|
||||
_extensions: Vec<ExtensionConfig>,
|
||||
tls_config: Option<TlsConfig>,
|
||||
) -> BoxFuture<'static, Result<Self::Provider>> {
|
||||
Box::pin(from_env(tls_config))
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_env(tls_config: Option<TlsConfig>) -> Result<OpenRouterProvider> {
|
||||
let config = Config::global();
|
||||
let api_key: String = config.get_secret("OPENROUTER_API_KEY")?;
|
||||
let host: String = config
|
||||
.get_param("OPENROUTER_HOST")
|
||||
.unwrap_or_else(|_| "https://openrouter.ai".to_string());
|
||||
let configured_parameters = configured_openrouter_parameters(config)?;
|
||||
|
||||
let api_client = ApiClient::new_with_tls(host, AuthMethod::BearerToken(api_key), tls_config)?
|
||||
.with_request_builder(crate::session_context::session_id_request_builder())
|
||||
.with_header("HTTP-Referer", "https://goose-docs.ai")?
|
||||
.with_header("X-Title", "goose")?
|
||||
.with_header("X-OpenRouter-Categories", "cli-agent,productivity")?;
|
||||
|
||||
Ok(OpenRouterProvider::new(
|
||||
api_client,
|
||||
configured_parameters,
|
||||
Some(Box::new(crate::session_context::current_session_id)),
|
||||
))
|
||||
}
|
||||
|
||||
fn configured_openrouter_parameters(config: &Config) -> Result<Option<HashMap<String, Value>>> {
|
||||
match config.get_param::<Value>(OPENROUTER_PARAMETERS_CONFIG_KEY) {
|
||||
Ok(raw) => parse_openrouter_parameters(raw).map(Some),
|
||||
Err(ConfigError::NotFound(_)) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn metadata_includes_parameters_config_key() {
|
||||
assert!(OpenRouterProviderDef::metadata()
|
||||
.config_keys
|
||||
.iter()
|
||||
.any(|key| key.name == OPENROUTER_PARAMETERS_CONFIG_KEY));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_object_and_json_string_parameters() {
|
||||
assert_eq!(
|
||||
parse_openrouter_parameters(json!({ "verbosity": "high" })).unwrap()["verbosity"],
|
||||
json!("high")
|
||||
);
|
||||
assert_eq!(
|
||||
parse_openrouter_parameters(json!(r#"{"plugins":[{"id":"web"}]}"#)).unwrap()["plugins"],
|
||||
json!([{ "id": "web" }])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_object_parameters() {
|
||||
assert!(parse_openrouter_parameters(json!(r#"["web"]"#))
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("OPENROUTER_PARAMETERS must be a JSON object"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user