Watch
1
0
Fork
You've already forked souveraine
0

providers: the provider follows the model, not the agent

Subconscious, reflection and archivist each pick a model independently
of the primary, then took the primary's provider to run it on. A
subconscious on deepseek-v4-flash under a claude-subscription primary
was sent to api.anthropic.com asking for a model it has never heard of,
so a split like Opus-5 primary with a DeepSeek subconscious could not
work at all.

ProviderRegistry now carries the [models.<name>] provider map and
resolves by model, falling back to the agent's provider when a model
has no entry — the single-provider case every agent used before.
This commit is contained in:
Fimeg 2026-08-10 14:36:51 -04:00
commit 30e1c34a34
4 changed files with 53 additions and 13 deletions

View file

@ -95,10 +95,17 @@ pub fn build_registry(config: &ConsciousnessConfig) -> anyhow::Result<ProviderRe
.cloned()
.unwrap_or_else(|| map.values().next().unwrap().clone());
let model_providers = config
.models
.iter()
.map(|(model, mcfg)| (model.clone(), mcfg.provider.clone()))
.collect();
Ok(ProviderRegistry {
map,
default_name,
default,
model_providers,
})
}
@ -108,6 +115,10 @@ pub struct ProviderRegistry {
map: HashMap<String, Arc<dyn LlmProvider>>,
default_name: String,
default: Arc<dyn LlmProvider>,
/// `[models.<name>] provider` — which provider serves a given model.
/// Needed wherever a model is chosen independently of the agent that owns
/// the turn, e.g. the subconscious running somewhere the primary does not.
model_providers: HashMap<String, String>,
}
impl std::fmt::Debug for ProviderRegistry {
@ -144,6 +155,16 @@ impl ProviderRegistry {
self.map.get(name).cloned()
}
/// Resolve the provider that serves a model, via `[models.<name>] provider`.
///
/// `None` when the model has no config entry or names a provider that
/// isn't registered — the caller decides the fallback, because "no entry"
/// and "wrong entry" both mean *don't guess a wire for this model*.
pub fn for_model(&self, model: &str) -> Option<Arc<dyn LlmProvider>> {
let name = self.model_providers.get(model)?;
self.map.get(name).cloned()
}
/// The global default provider (for contexts with no agent).
pub fn default_provider(&self) -> Arc<dyn LlmProvider> {
self.default.clone()

View file

@ -208,13 +208,20 @@ impl ArchivistEngine {
let approx_tokens_compressed = raw.len() / 4;
let model = self.resolve_model(agent_id).await;
let llm: Arc<dyn LlmProvider> = self
.agents
.get(agent_id)
.await
.ok()
.map(|a| self.providers.for_agent(&a))
.unwrap_or_else(|| self.providers.default_provider());
// Provider follows the model. The compression model is frequently on
// another provider entirely (a gateway-hosted model while the agent
// runs on a direct wire), so taking the agent's provider sent it to an
// endpoint that has never heard of that model.
let llm: Arc<dyn LlmProvider> = match self.providers.for_model(&model) {
Some(llm) => llm,
None => self
.agents
.get(agent_id)
.await
.ok()
.map(|a| self.providers.for_agent(&a))
.unwrap_or_else(|| self.providers.default_provider()),
};
let synthesis = self
.run_synthesis(&llm, &model, &raw, start_date, end_date)
.await?;

View file

@ -128,9 +128,13 @@ impl ReflectionEngine {
.or_else(|| self.model.clone())
.or_else(|| agent.as_ref().map(|a| a.llm_config.model.clone()))
.unwrap_or_else(|| "openai/glm-5.1".to_string());
let llm: Arc<dyn LlmProvider> = agent
.as_ref()
.map(|a| self.providers.for_agent(a))
// Provider follows the model — the reflection model often belongs to a
// different provider than the agent's own. Agent's provider is the
// fallback for models with no `[models.<name>]` entry.
let llm: Arc<dyn LlmProvider> = self
.providers
.for_model(&model)
.or_else(|| agent.as_ref().map(|a| self.providers.for_agent(a)))
.unwrap_or_else(|| self.providers.default_provider());
// ── Tools ───────────────────────────────────────────────────

View file

@ -593,9 +593,17 @@ impl ConsciousnessEngine {
.or_else(|| self.subconscious_model.clone())
.or_else(|| primary_agent.as_ref().map(|a| a.llm_config.model.clone()))
.unwrap_or_else(|| "openai/kimi-k2.6".to_string());
let llm: Arc<dyn LlmProvider> = primary_agent
.as_ref()
.map(|a| self.providers.for_agent(a))
// The provider follows the *model*, not the primary. Taking it from
// the primary agent meant a subconscious pointed at another provider's
// model was sent to the primary's wire and asked for a model that
// endpoint has never heard of — so a split like Opus-5 primary with a
// DeepSeek subconscious could not work at all. Falls back to the
// primary's provider when the model has no `[models.<name>]` entry,
// which is the single-provider case every agent used before.
let llm: Arc<dyn LlmProvider> = self
.providers
.for_model(&model)
.or_else(|| primary_agent.as_ref().map(|a| self.providers.for_agent(a)))
.unwrap_or_else(|| self.providers.default_provider());
// ── System prompt — load from subconscious agent's own memfs ──