Skip to content

Commit d2d2b7a

Browse files
committed
Release 2026.3.9
Fix Groq OpenAI-compatible passthrough endpoint selection, persist provider-specific external_llm_url from set-key, and cut the 2026.3.9 release.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1b9a3c6 commit d2d2b7a

6 files changed

Lines changed: 86 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2026.3.9] - 2026-03-27
11+
12+
### Fixed
13+
- **Groq OpenAI passthrough endpoint selection**: OpenAI-compatible tool passthrough now falls back to Groq's own `/openai/v1/chat/completions` endpoint instead of reusing the OpenAI default URL, preventing Groq keys from being sent to OpenAI and failing with misleading `401 invalid_api_key` errors.
14+
- **`isartor set-key -p groq` endpoint wiring**: provider key setup now writes the provider-specific `external_llm_url` alongside the model and key so Groq and other OpenAI-compatible providers start with the correct Layer 3 endpoint.
15+
1016
## [2026.3.8] - 2026-03-27
1117

1218
### Changed
@@ -403,7 +409,8 @@ Isartor's first CalVer release — marking go-live readiness.
403409
### Fixed
404410
- Build musl targets with runner-based zigbuild (no Docker container)
405411

406-
[Unreleased]: https://github.com/isartor-ai/Isartor/compare/v2026.3.8...HEAD
412+
[Unreleased]: https://github.com/isartor-ai/Isartor/compare/v2026.3.9...HEAD
413+
[2026.3.9]: https://github.com/isartor-ai/Isartor/compare/v2026.3.8...v2026.3.9
407414
[2026.3.8]: https://github.com/isartor-ai/Isartor/compare/v2026.3.7...v2026.3.8
408415
[2026.3.7]: https://github.com/isartor-ai/Isartor/compare/v2026.3.6...v2026.3.7
409416
[2026.3.6]: https://github.com/isartor-ai/Isartor/compare/v2026.3.5...v2026.3.6

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "isartor"
3-
version = "2026.3.8"
3+
version = "2026.3.9"
44
edition = "2024"
55
license = "Apache-2.0"
66
description = "Open-source Prompt Firewall — a local LLM gateway with caching, semantic deduplication, and content filtering"

src/cli/set_key.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::path::PathBuf;
44
use anyhow::{Context, Result, bail};
55
use clap::Parser;
66

7-
use crate::config::LlmProvider;
7+
use crate::config::{LlmProvider, default_chat_completions_url};
88

99
/// Set the API key for an LLM provider (writes to isartor.toml or env file).
1010
#[derive(Parser, Debug, Clone)]
@@ -129,8 +129,13 @@ pub async fn handle_set_key(args: SetKeyArgs) -> Result<()> {
129129
// 4. Handle --env-file mode
130130
if args.env_file {
131131
let env_content = format!(
132-
"export ISARTOR__LLM_PROVIDER=\"{}\"\nexport ISARTOR__EXTERNAL_LLM_MODEL=\"{}\"\nexport ISARTOR__EXTERNAL_LLM_API_KEY=\"{}\"\n",
133-
provider_str, model, api_key
132+
"export ISARTOR__LLM_PROVIDER=\"{}\"\n{}export ISARTOR__EXTERNAL_LLM_MODEL=\"{}\"\nexport ISARTOR__EXTERNAL_LLM_API_KEY=\"{}\"\n",
133+
provider_str,
134+
default_chat_completions_url(&provider)
135+
.map(|url| format!("export ISARTOR__EXTERNAL_LLM_URL=\"{}\"\n", url))
136+
.unwrap_or_default(),
137+
model,
138+
api_key
134139
);
135140

136141
if args.dry_run {
@@ -176,6 +181,9 @@ pub async fn handle_set_key(args: SetKeyArgs) -> Result<()> {
176181
.with_context(|| format!("Failed to parse {}", config_path.display()))?;
177182

178183
doc["llm_provider"] = toml_edit::value(provider_str);
184+
if let Some(url) = default_chat_completions_url(&provider) {
185+
doc["external_llm_url"] = toml_edit::value(url);
186+
}
179187
doc["external_llm_model"] = toml_edit::value(model.as_str());
180188
doc["external_llm_api_key"] = toml_edit::value(api_key.as_str());
181189

@@ -288,6 +296,10 @@ mod tests {
288296

289297
let content = std::fs::read_to_string(&tmp).unwrap();
290298
assert!(content.contains("llm_provider = \"groq\""));
299+
assert!(
300+
content
301+
.contains("external_llm_url = \"https://api.groq.com/openai/v1/chat/completions\"")
302+
);
291303
assert!(content.contains("external_llm_model = \"llama-3.1-8b-instant\""));
292304
assert!(content.contains("external_llm_api_key = \"gsk_testkey12345678\""));
293305

src/config.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,26 @@ impl std::fmt::Display for LlmProvider {
9696
}
9797
}
9898

99+
pub const DEFAULT_OPENAI_CHAT_COMPLETIONS_URL: &str = "https://api.openai.com/v1/chat/completions";
100+
101+
pub fn default_chat_completions_url(provider: &LlmProvider) -> Option<&'static str> {
102+
match provider {
103+
LlmProvider::Openai => Some(DEFAULT_OPENAI_CHAT_COMPLETIONS_URL),
104+
LlmProvider::Copilot => Some("https://api.githubcopilot.com/chat/completions"),
105+
LlmProvider::Xai => Some("https://api.x.ai/v1/chat/completions"),
106+
LlmProvider::Mistral => Some("https://api.mistral.ai/v1/chat/completions"),
107+
LlmProvider::Groq => Some("https://api.groq.com/openai/v1/chat/completions"),
108+
LlmProvider::Deepseek => Some("https://api.deepseek.com/chat/completions"),
109+
LlmProvider::Galadriel => Some("https://api.galadriel.com/v1/chat/completions"),
110+
LlmProvider::Hyperbolic => Some("https://api.hyperbolic.xyz/v1/chat/completions"),
111+
LlmProvider::Moonshot => Some("https://api.moonshot.cn/v1/chat/completions"),
112+
LlmProvider::Openrouter => Some("https://openrouter.ai/api/v1/chat/completions"),
113+
LlmProvider::Perplexity => Some("https://api.perplexity.ai/chat/completions"),
114+
LlmProvider::Together => Some("https://api.together.xyz/v1/chat/completions"),
115+
_ => None,
116+
}
117+
}
118+
99119
impl From<&str> for LlmProvider {
100120
fn from(s: &str) -> Self {
101121
match s.to_lowercase().as_str() {
@@ -445,10 +465,7 @@ impl AppConfig {
445465
.set_default("embedding_sidecar.timeout_seconds", 10_i64)?
446466
// Layer 3
447467
.set_default("llm_provider", "openai")?
448-
.set_default(
449-
"external_llm_url",
450-
"https://api.openai.com/v1/chat/completions",
451-
)?
468+
.set_default("external_llm_url", DEFAULT_OPENAI_CHAT_COMPLETIONS_URL)?
452469
.set_default("external_llm_model", "gpt-4o-mini")?
453470
.set_default("external_llm_api_key", "")?
454471
.set_default("l3_timeout_secs", 120_i64)?

src/handler.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ use tokio_stream::wrappers::IntervalStream;
1717
use tracing::{Instrument, info_span};
1818

1919
use crate::anthropic_sse;
20-
use crate::config::LlmProvider;
20+
use crate::config::{
21+
DEFAULT_OPENAI_CHAT_COMPLETIONS_URL, LlmProvider, default_chat_completions_url,
22+
};
2123
use crate::core::cache_scope::{
2224
build_exact_cache_key, extract_session_cache_scope, namespaced_semantic_cache_input,
2325
};
@@ -98,7 +100,17 @@ fn provider_chat_completions_url(state: &AppState) -> Option<String> {
98100
state.config.external_llm_url.clone()
99101
}),
100102
provider if supports_openai_passthrough(provider) => {
101-
Some(state.config.external_llm_url.clone())
103+
let configured_url = state.config.external_llm_url.trim();
104+
let default_url = default_chat_completions_url(provider)?;
105+
106+
if configured_url.is_empty()
107+
|| (*provider != LlmProvider::Openai
108+
&& configured_url == DEFAULT_OPENAI_CHAT_COMPLETIONS_URL)
109+
{
110+
Some(default_url.to_string())
111+
} else {
112+
Some(configured_url.to_string())
113+
}
102114
}
103115
_ => None,
104116
}
@@ -1772,6 +1784,32 @@ mod tests {
17721784
);
17731785
}
17741786

1787+
#[test]
1788+
fn groq_passthrough_uses_groq_default_when_config_still_points_to_openai() {
1789+
let state = test_state(Arc::new(SuccessAgent));
1790+
let mut config = (*state.config).clone();
1791+
config.llm_provider = LlmProvider::Groq;
1792+
config.external_llm_url = DEFAULT_OPENAI_CHAT_COMPLETIONS_URL.into();
1793+
1794+
let state = Arc::new(AppState {
1795+
http_client: reqwest::Client::new(),
1796+
exact_cache: state.exact_cache.clone(),
1797+
vector_cache: state.vector_cache.clone(),
1798+
llm_agent: Arc::new(SuccessAgent),
1799+
slm_client: state.slm_client.clone(),
1800+
text_embedder: state.text_embedder.clone(),
1801+
instruction_cache: Arc::new(InstructionCache::new()),
1802+
config: Arc::new(config),
1803+
#[cfg(feature = "embedded-inference")]
1804+
embedded_classifier: None,
1805+
});
1806+
1807+
assert_eq!(
1808+
provider_chat_completions_url(&state).as_deref(),
1809+
Some("https://api.groq.com/openai/v1/chat/completions")
1810+
);
1811+
}
1812+
17751813
#[tokio::test]
17761814
async fn openai_models_returns_configured_l3_model_list() {
17771815
let state = test_state(Arc::new(SuccessAgent));

0 commit comments

Comments
 (0)