Skip to content

Commit 947dfd0

Browse files
committed
Add Venice provider and refactor LLM clients
Add support for the Venice (OpenAI-compatible) provider and refactor how LLM clients/agents are built. Updates include: README and Settings.yaml entries for Venice; new Provider::Venice and venice config in Settings; generic build functions build_anthropic_client and build_openai_client with helpers for base URLs; new Inner/ReviewAgent enums to abstract Anthropic vs OpenAI-style agents; split out build_tools and a generic build_inner<C: CompletionClient> so agents can be constructed for either client type; update GemmyAgent and ChatwootReviewReplyTool to select the appropriate client/agent based on settings. This enables using PROVIDER=venice with VENICE_KEY and keeps existing anthropic/deepseek behavior.
1 parent 3993542 commit 947dfd0

5 files changed

Lines changed: 134 additions & 30 deletions

File tree

core/apps/agent/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ cargo run -p agent --bin repl -- security
3232

3333
Config is layered (later overrides earlier): `Settings.yaml``agents/<name>/agent.yaml`
3434
environment variables (`_`-separated, e.g. `SLACK_BOT_TOKEN``slack.bot.token`). The agent to
35-
run is selected by `AGENT_NAME` (or `argv[1]`).
35+
run is selected by `AGENT_NAME` (or `argv[1]`). Supported LLM providers are `anthropic`,
36+
`deepseek`, and `venice`; set `PROVIDER=venice` with `VENICE_KEY` to use Venice.

core/apps/agent/Settings.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ slack:
1313
bot:
1414
token: "" # SLACK_BOT_TOKEN
1515

16-
provider: anthropic # PROVIDER — anthropic | deepseek
16+
provider: anthropic # PROVIDER — anthropic | deepseek | venice
1717

1818
anthropic:
1919
key: "" # ANTHROPIC_KEY
2020
deepseek:
2121
key: "" # DEEPSEEK_KEY
2222
base: "https://api.deepseek.com/anthropic"
23+
venice:
24+
key: "" # VENICE_KEY
25+
base: "https://api.venice.ai/api/v1"
2326

2427
embedding:
2528
model: "nomic-embed-text-v1.5-q"

core/apps/agent/src/agent.rs

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ use rig::agent::{Agent as RigAgent, PromptResponse};
88
use rig::client::CompletionClient;
99
use rig::completion::Prompt;
1010
use rig::completion::message::{Message, UserContent};
11-
use rig::providers::anthropic;
11+
use rig::providers::{anthropic, openai};
1212
use rig::tool::ToolDyn;
1313

1414
use crate::chatwoot::ChatwootClient;
15-
use crate::config::Settings;
15+
use crate::config::{Provider, ProviderConfig, Settings};
1616
use crate::images::ImageAttachment;
1717
use crate::preamble;
1818
use crate::slack::SlackClient;
@@ -22,14 +22,67 @@ use crate::tools::{
2222
ShellTool, SlackHistoryTool, SlackPostTool, TelegramPostTool, ToolName,
2323
};
2424

25-
type Inner = RigAgent<anthropic::completion::CompletionModel>;
25+
const VENICE_BASE_URL: &str = "https://api.venice.ai/api/v1";
2626

27-
pub(crate) fn build_client(provider: &crate::config::ProviderConfig) -> Result<anthropic::Client> {
28-
let mut builder = anthropic::Client::builder().api_key(&provider.key);
29-
if !provider.base.is_empty() {
30-
builder = builder.base_url(&provider.base);
27+
type AnthropicInner = RigAgent<anthropic::completion::CompletionModel>;
28+
type OpenAiInner = RigAgent<openai::completion::CompletionModel>;
29+
30+
enum Inner {
31+
Anthropic(AnthropicInner),
32+
OpenAi(OpenAiInner),
33+
}
34+
35+
impl Inner {
36+
async fn prompt_response(&self, msg: &str) -> Result<PromptResponse> {
37+
match self {
38+
Inner::Anthropic(inner) => Ok(inner.prompt(msg).extended_details().await?),
39+
Inner::OpenAi(inner) => Ok(inner.prompt(msg).extended_details().await?),
40+
}
41+
}
42+
43+
async fn prompt_message(&self, msg: Message) -> Result<String> {
44+
match self {
45+
Inner::Anthropic(inner) => Ok(inner.prompt(msg).await?),
46+
Inner::OpenAi(inner) => Ok(inner.prompt(msg).await?),
47+
}
48+
}
49+
}
50+
51+
pub(crate) fn build_anthropic_client(provider: Provider, config: &ProviderConfig) -> Result<anthropic::Client> {
52+
let name = provider_name(provider);
53+
let mut builder = anthropic::Client::builder().api_key(&config.key);
54+
if !config.base.is_empty() {
55+
builder = builder.base_url(&config.base);
56+
}
57+
builder.build().map_err(|e| format!("building {name} client: {e}").into())
58+
}
59+
60+
pub(crate) fn build_openai_client(provider: Provider, config: &ProviderConfig) -> Result<openai::CompletionsClient> {
61+
let name = provider_name(provider);
62+
let base_url = openai_base_url(provider, config);
63+
let mut builder = openai::CompletionsClient::builder().api_key(&config.key);
64+
if let Some(base_url) = base_url {
65+
builder = builder.base_url(base_url);
66+
}
67+
builder.build().map_err(|e| format!("building {name} client: {e}").into())
68+
}
69+
70+
fn openai_base_url(provider: Provider, config: &ProviderConfig) -> Option<&str> {
71+
if !config.base.is_empty() {
72+
return Some(&config.base);
73+
}
74+
match provider {
75+
Provider::Venice => Some(VENICE_BASE_URL),
76+
Provider::Anthropic | Provider::Deepseek => None,
77+
}
78+
}
79+
80+
fn provider_name(provider: Provider) -> &'static str {
81+
match provider {
82+
Provider::Anthropic => "anthropic",
83+
Provider::Deepseek => "deepseek",
84+
Provider::Venice => "venice",
3185
}
32-
builder.build().map_err(|e| format!("building Anthropic client: {e}").into())
3386
}
3487

3588
pub struct GemmyAgent {
@@ -45,14 +98,22 @@ impl GemmyAgent {
4598
slack: Arc<SlackClient>,
4699
mcp_tools: Vec<Box<dyn ToolDyn>>,
47100
) -> Result<Self> {
48-
let provider = settings.llm_provider();
49-
if provider.key.is_empty() {
101+
let config = settings.llm_provider();
102+
if config.key.is_empty() {
50103
return Err(format!("no key for the active provider {:?} — set its key in vault/.env", settings.provider).into());
51104
}
52105
let preamble = preamble::render(settings)?;
53-
let client = build_client(provider)?;
54-
55-
let inner = build_inner(&client, settings, &preamble, memory, chatwoot, slack, mcp_tools);
106+
let tools = build_tools(settings, memory, chatwoot, slack, mcp_tools);
107+
let inner = match settings.provider {
108+
Provider::Anthropic | Provider::Deepseek => {
109+
let client = build_anthropic_client(settings.provider, config)?;
110+
Inner::Anthropic(build_inner(&client, settings, &preamble, tools))
111+
}
112+
Provider::Venice => {
113+
let client = build_openai_client(settings.provider, config)?;
114+
Inner::OpenAi(build_inner(&client, settings, &preamble, tools))
115+
}
116+
};
56117

57118
info!(
58119
agent = %settings.agent_name,
@@ -77,7 +138,7 @@ impl GemmyAgent {
77138
images = 0,
78139
"agent.prompt"
79140
);
80-
Ok(self.inner.prompt(msg).extended_details().await?)
141+
self.inner.prompt_response(msg).await
81142
}
82143

83144
pub async fn prompt_with_images(&self, msg: &str, images: Vec<ImageAttachment>) -> Result<String> {
@@ -96,20 +157,17 @@ impl GemmyAgent {
96157
blocks.push(UserContent::image_base64(engine.encode(&img.bytes), Some(img.media_type), None));
97158
}
98159
let content = OneOrMany::many(blocks).expect("text block always present");
99-
Ok(self.inner.prompt(Message::User { content }).await?)
160+
self.inner.prompt_message(Message::User { content }).await
100161
}
101162
}
102163

103-
fn build_inner(
104-
client: &anthropic::Client,
164+
fn build_tools(
105165
settings: &Settings,
106-
preamble: &str,
107166
memory: Option<Arc<MemoryStore>>,
108167
chatwoot: Option<Arc<ChatwootClient>>,
109168
slack: Arc<SlackClient>,
110169
mcp_tools: Vec<Box<dyn ToolDyn>>,
111-
) -> Inner {
112-
let model_id = &settings.agent.model;
170+
) -> Vec<Box<dyn ToolDyn>> {
113171
let mut tools: Vec<Box<dyn ToolDyn>> = mcp_tools;
114172
for entry in &settings.agent.tools {
115173
let policy = entry.policy();
@@ -159,6 +217,14 @@ fn build_inner(
159217
tools.push(Box::new(GatedTool { inner, policy }));
160218
}
161219
}
220+
tools
221+
}
222+
223+
fn build_inner<C>(client: &C, settings: &Settings, preamble: &str, tools: Vec<Box<dyn ToolDyn>>) -> RigAgent<C::CompletionModel>
224+
where
225+
C: CompletionClient,
226+
{
227+
let model_id = &settings.agent.model;
162228
let tool_names: Vec<String> = tools.iter().map(|t| t.name()).collect();
163229
info!(model = %model_id, tools = ?tool_names, "built agent");
164230
client

core/apps/agent/src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ pub enum Provider {
224224
#[default]
225225
Anthropic,
226226
Deepseek,
227+
Venice,
227228
}
228229

229230
#[derive(Debug, Deserialize, Clone, Default)]
@@ -269,6 +270,8 @@ pub struct Settings {
269270
pub anthropic: ProviderConfig,
270271
#[serde(default)]
271272
pub deepseek: ProviderConfig,
273+
#[serde(default)]
274+
pub venice: ProviderConfig,
272275
pub embedding: EmbeddingConfig,
273276
pub agent: AgentProfile,
274277
#[serde(default)]
@@ -288,6 +291,7 @@ impl Settings {
288291
match self.provider {
289292
Provider::Anthropic => &self.anthropic,
290293
Provider::Deepseek => &self.deepseek,
294+
Provider::Venice => &self.venice,
291295
}
292296
}
293297

core/apps/agent/src/tools/chatwoot_review_reply.rs

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,32 @@ use gem_tracing::tracing::{debug, warn};
55
use rig::agent::Agent as RigAgent;
66
use rig::client::CompletionClient;
77
use rig::completion::{Prompt, ToolDefinition};
8-
use rig::providers::anthropic;
8+
use rig::providers::{anthropic, openai};
99
use rig::tool::Tool;
1010
use serde::{Deserialize, Serialize};
1111
use serde_json::json;
1212

13-
use crate::config::Settings;
13+
use crate::config::{Provider, Settings};
1414
use crate::tools::ToolFailure;
1515

1616
#[derive(Clone)]
1717
pub struct ChatwootReviewReplyTool {
18-
inner: Arc<RigAgent<anthropic::completion::CompletionModel>>,
18+
inner: ReviewAgent,
19+
}
20+
21+
#[derive(Clone)]
22+
enum ReviewAgent {
23+
Anthropic(Arc<RigAgent<anthropic::completion::CompletionModel>>),
24+
OpenAi(Arc<RigAgent<openai::completion::CompletionModel>>),
25+
}
26+
27+
impl ReviewAgent {
28+
async fn prompt(&self, prompt: &str) -> Result<String, rig::completion::PromptError> {
29+
match self {
30+
ReviewAgent::Anthropic(inner) => inner.prompt(prompt).await,
31+
ReviewAgent::OpenAi(inner) => inner.prompt(prompt).await,
32+
}
33+
}
1934
}
2035

2136
#[derive(Debug, Deserialize)]
@@ -39,17 +54,32 @@ impl ChatwootReviewReplyTool {
3954
return Ok(None);
4055
}
4156
let preamble = fs::read_to_string(&preamble_path)?;
42-
let provider = settings.llm_provider();
43-
if provider.key.is_empty() {
57+
let config = settings.llm_provider();
58+
if config.key.is_empty() {
4459
return Ok(None);
4560
}
46-
let client = crate::agent::build_client(provider)?;
4761
let model = &settings.agent.model;
48-
let inner = client.agent(model).preamble(&preamble).max_tokens(2048).temperature(0.2).build();
49-
Ok(Some(Self { inner: Arc::new(inner) }))
62+
let inner = match settings.provider {
63+
Provider::Anthropic | Provider::Deepseek => {
64+
let client = crate::agent::build_anthropic_client(settings.provider, config)?;
65+
ReviewAgent::Anthropic(Arc::new(build_inner(&client, model, &preamble)))
66+
}
67+
Provider::Venice => {
68+
let client = crate::agent::build_openai_client(settings.provider, config)?;
69+
ReviewAgent::OpenAi(Arc::new(build_inner(&client, model, &preamble)))
70+
}
71+
};
72+
Ok(Some(Self { inner }))
5073
}
5174
}
5275

76+
fn build_inner<C>(client: &C, model: &str, preamble: &str) -> RigAgent<C::CompletionModel>
77+
where
78+
C: CompletionClient,
79+
{
80+
client.agent(model).preamble(preamble).max_tokens(2048).temperature(0.2).build()
81+
}
82+
5383
impl Tool for ChatwootReviewReplyTool {
5484
const NAME: &'static str = "chatwoot_review_reply";
5585
type Error = ToolFailure;

0 commit comments

Comments
 (0)