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:
parent
eee26fcf57
commit
30e1c34a34
4 changed files with 53 additions and 13 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
|
|
@ -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 ───────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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 ──
|
||||
|
|
|
|||
Loading…
Reference in a new issue