-
Notifications
You must be signed in to change notification settings - Fork 6k
add declarative provider support to goose-providers crate #9992
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
Changes from all commits
f6a6e66
f8b05dd
4714739
529240b
542d7f4
a0b9758
9056e1b
da20935
f5b460d
25240f2
f979827
ea8621a
e2e99d5
3186afc
3167101
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| use anyhow::Result; | ||
| use futures::StreamExt; | ||
| use goose_providers::{ | ||
| base::Provider, conversation::message::Message, declarative::EnvKeyResolver, model::ModelConfig, | ||
| }; | ||
|
|
||
| async fn complete(provider: &dyn Provider, model: ModelConfig) -> Result<()> { | ||
| let system = "You are a knowledgable geography expert"; | ||
| let messages = [Message::user().with_text("what is the capital of France?")]; | ||
| let mut stream = provider.stream(&model, system, &messages, &[]).await?; | ||
|
|
||
| while let Some((Some(msg), _)) = stream.next().await.transpose()? { | ||
| print!("{}", msg.as_concat_text()); | ||
| } | ||
| println!(); | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| #[tokio::main] | ||
| async fn main() -> Result<()> { | ||
| let deepseek = include_str!("deepseek.json"); | ||
| let deepseek_model = ModelConfig::new("deepseek-v4-flash"); | ||
| let zai = include_str!("zai.json"); | ||
| let zai_model = ModelConfig::new("glm-4.5-flash"); | ||
|
|
||
| for (json, model) in [(deepseek, deepseek_model), (zai, zai_model)] { | ||
| let provider = goose_providers::declarative::from_json(json, None, EnvKeyResolver {})?; | ||
| println!("{}:", provider.get_name()); | ||
| complete(provider.as_ref(), model).await?; | ||
| } | ||
| Ok(()) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| { | ||
| "name": "deepseek", | ||
| "engine": "openai", | ||
| "display_name": "DeepSeek", | ||
| "description": "Custom DeepSeek provider", | ||
| "api_key_env": "DEEPSEEK_API_KEY", | ||
| "base_url": "https://api.deepseek.com", | ||
| "models": [ | ||
| { | ||
| "name": "deepseek-chat", | ||
| "context_limit": 128000, | ||
| "input_token_cost": null, | ||
| "output_token_cost": null, | ||
| "currency": null, | ||
| "supports_cache_control": null | ||
| }, | ||
| { | ||
| "name": "deepseek-reasoner", | ||
| "context_limit": 128000, | ||
| "input_token_cost": null, | ||
| "output_token_cost": null, | ||
| "currency": null, | ||
| "supports_cache_control": null | ||
| } | ||
| ], | ||
| "headers": null, | ||
| "timeout_seconds": null, | ||
| "preserves_thinking": true, | ||
| "supports_streaming": true | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| { | ||
| "name": "zai", | ||
| "engine": "anthropic", | ||
| "display_name": "Z.AI", | ||
| "description": "Z.AI GLM models via Anthropic-compatible API.", | ||
| "api_key_env": "ZHIPU_API_KEY", | ||
| "base_url": "https://api.z.ai/api/anthropic", | ||
| "catalog_provider_id": "zai", | ||
| "model_doc_link": "https://docs.z.ai/devpack/tool/goose", | ||
| "fast_model": "glm-4.5-air", | ||
| "preserves_thinking": true, | ||
| "models": [ | ||
| { "name": "glm-5.1", "context_limit": 200000 }, | ||
| { "name": "glm-5", "context_limit": 204800 }, | ||
| { "name": "glm-5-turbo", "context_limit": 200000 }, | ||
| { "name": "glm-4.7", "context_limit": 204800 }, | ||
| { "name": "glm-4.7-flash", "context_limit": 200000 }, | ||
| { "name": "glm-4.7-flashx", "context_limit": 200000 }, | ||
| { "name": "glm-4.6", "context_limit": 204800 }, | ||
| { "name": "glm-4.5", "context_limit": 131072 }, | ||
| { "name": "glm-4.5-air", "context_limit": 131072 }, | ||
| { "name": "glm-4.5-flash", "context_limit": 131072 } | ||
| ], | ||
| "supports_streaming": true | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| use crate::api_client::{AuthMethod, TlsConfig}; | ||
| use crate::base::ProviderDescriptor; | ||
| use crate::declarative::{DeclarativeProviderConfig, KeyResolver}; | ||
| use crate::errors::ProviderError; | ||
| use crate::request_log::{start_log, LoggerHandleExt}; | ||
| use anyhow::Result; | ||
|
|
@@ -90,6 +92,24 @@ impl AnthropicProviderBuilder { | |
| } | ||
| } | ||
|
|
||
| pub fn api_client(mut self, api_client: ApiClient) -> Self { | ||
| self.api_client = api_client; | ||
| self | ||
| } | ||
|
|
||
| pub fn map_api_client(mut self, f: impl FnOnce(ApiClient) -> ApiClient) -> Self { | ||
| self.api_client = f(self.api_client); | ||
| self | ||
| } | ||
|
|
||
| pub fn try_map_api_client( | ||
| mut self, | ||
| f: impl FnOnce(ApiClient) -> Result<ApiClient>, | ||
| ) -> Result<Self> { | ||
| self.api_client = f(self.api_client)?; | ||
| Ok(self) | ||
| } | ||
|
|
||
| pub fn supports_streaming(mut self, supports_streaming: bool) -> Self { | ||
| self.supports_streaming = supports_streaming; | ||
| self | ||
|
|
@@ -287,3 +307,96 @@ impl Provider for AnthropicProvider { | |
| })) | ||
| } | ||
| } | ||
|
|
||
| fn format_options_for_provider(preserves_thinking: bool) -> AnthropicFormatOptions { | ||
| AnthropicFormatOptions { | ||
| preserve_unsigned_thinking: preserves_thinking, | ||
| preserve_thinking_context: preserves_thinking, | ||
| thinking_disabled: false, | ||
| } | ||
| } | ||
|
|
||
| pub fn from_declarative_config( | ||
| config: DeclarativeProviderConfig, | ||
| tls_config: Option<TlsConfig>, | ||
| key_resolver: impl KeyResolver, | ||
| ) -> Result<AnthropicProviderBuilder> { | ||
| let custom_models = if !config.models.is_empty() { | ||
| Some( | ||
| config | ||
| .models | ||
| .iter() | ||
| .map(|m| m.name.clone()) | ||
| .collect::<Vec<String>>(), | ||
| ) | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| if config.dynamic_models == Some(false) && custom_models.is_none() { | ||
| return Err(anyhow::anyhow!( | ||
| "Provider '{}' has dynamic_models: false but no static models listed; \ | ||
| at least one entry in `models` is required.", | ||
| config.name | ||
| )); | ||
| } | ||
|
|
||
| let api_key = if config.api_key_env.is_empty() { | ||
| None | ||
| } else { | ||
| match key_resolver.resolve_key(config.api_key_env.as_str()) { | ||
| Ok(key) => Some(key), | ||
| Err(err) => { | ||
| if config.requires_auth { | ||
| anyhow::bail!("missing required key {}: {}", config.api_key_env, err); | ||
| } | ||
| None | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| let auth = match api_key { | ||
| Some(key) if !key.is_empty() => AuthMethod::ApiKey { | ||
| header_name: "x-api-key".to_string(), | ||
| key, | ||
| }, | ||
| _ => AuthMethod::NoAuth, | ||
| }; | ||
|
|
||
| let format_options = format_options_for_provider(config.preserves_thinking); | ||
|
|
||
| let mut api_client = ApiClient::new_with_tls(config.base_url, auth, tls_config)?; | ||
|
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.
When an Anthropic-compatible declarative provider uses a base URL with required query parameters (for example a gateway that routes by Useful? React with 👍 / 👎. |
||
|
|
||
| if let Some(headers) = &config.headers { | ||
| let mut header_map = reqwest::header::HeaderMap::new(); | ||
| header_map.insert( | ||
| reqwest::header::HeaderName::from_static("anthropic-version"), | ||
| reqwest::header::HeaderValue::from_static(ANTHROPIC_API_VERSION), | ||
| ); | ||
| for (key, value) in headers { | ||
| let header_name = reqwest::header::HeaderName::from_bytes(key.as_bytes())?; | ||
| let header_value = reqwest::header::HeaderValue::from_str(value)?; | ||
| header_map.insert(header_name, header_value); | ||
| } | ||
| api_client = api_client.with_headers(header_map)?; | ||
|
jamadeo marked this conversation as resolved.
|
||
| } else { | ||
| api_client = api_client.with_header("anthropic-version", ANTHROPIC_API_VERSION)?; | ||
| } | ||
|
|
||
| let supports_streaming = config.supports_streaming.unwrap_or(true); | ||
|
|
||
| if !supports_streaming { | ||
| return Err(anyhow::anyhow!( | ||
| "Anthropic provider does not support non-streaming mode. All Claude models support streaming. \ | ||
| Please remove 'supports_streaming: false' from your provider configuration." | ||
| )); | ||
| } | ||
|
|
||
| Ok(AnthropicProviderBuilder::new(api_client) | ||
| .supports_streaming(supports_streaming) | ||
| .name(config.name.clone()) | ||
| .custom_models(custom_models) | ||
| .dynamic_models(config.dynamic_models) | ||
| .skip_canonical_filtering(config.skip_canonical_filtering) | ||
| .format_options(format_options)) | ||
| } | ||
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.
these are copies from what we ship with? how do we keep them in sync?
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.
They are, but this is just for example code. Which could drift yes. Once we move all of the declarative definitions into goose-providers, we can switch this to reference those, and they'll have the unit test that checks validity