Surfaced by @tobiu while scoping post-v13.2 refactoring: "agent os has embedding and chat models => gemma4:26b. If you check the chat layers, you will probably say 'doh!', similar to embeddings. Maybe even worse."
It is worse, and in a different way. The embedding lane's problem is diffusion — one authority read by many subsystems, which #17411 is consolidating. The chat lane's problem is that the declared authority has no consumers at all, while the leaf its own docblock calls a legacy alias carries the entire lane.
ADR-0019 read gate satisfied before authoring (§critical_gates#10).
Live latest-open sweep: latest 20 open issues read at 2026-08-21T09:00Z; state:all searches for chatProvider modelProvider, provider routing converges, chat provider axis alias, gemma model id namespace. Nearest prior art is a closed cluster (#12740 / #12742 / #12745, local-first provider defaults) and #12059 (Sandman graph provider routing) — all closed, none covering this. A2A claim sweep over the latest 12 messages, all read-states: no in-flight claim on this scope.
The Problem
Defect 1 — the Tier-1 leaf is dead and the alias is load-bearing
ai/configBase.mjs:705-723 declares two leaves bound to one env var:
/** @summaryDeployment-wide chat / generation model provider.
* Tier-1 source of truth for model-consuming Agent OS lanes. Memory Core maps
* this into its historical `modelProvider` key until runtime provider routing
* converges on one canonical key. */
chatProvider : leaf('openAiCompatible', 'NEO_MODEL_PROVIDER', 'string'),
/** @summary Runtime alias for the active chat provider.
* Existing Memory Core consumers read `modelProvider`; keep the Tier-1
* template aligned with `chatProvider` until provider routing converges on
* one canonical key. */modelProvider: leaf('openAiCompatible', 'NEO_MODEL_PROVIDER', 'string'),
So the migration was declared and never performed: the canonical leaf was added and documented as Tier-1, and not one consumer moved to it. Today the prose and the code disagree about which key is authoritative, and the prose is the one a reader trusts.
This is an ADR-0019 Group B violation — indirection AROUND the SSOT (even when reading it) — sitting inside the file that defines the SSOT. The ADR's decision sentence is explicit: "Never re-implement, alias, export, pass-along, mutate, or defend against the SSOT." The instruction "keep the Tier-1 template aligned with chatProvider" is manual synchronisation between two leaves, which is the definition of two sources of truth.
The deferral is stated twice, in both docblocks, in the same words — "until provider routing converges on one canonical key." Nothing schedules that convergence.
Defect 2 — model literals live outside AiConfig, one behind a hidden default
Three live sites hardcode a chat model id rather than reading a resolved leaf:
CORRECTED 2026-08-21 — I described the wrong failure for all three, and each is a different defect. The original text called them three hardcoded literals with one shared fix. Measured at tree 3809616cdc:
site
what actually happens
why the original description was wrong
roadmapPlanner.mjs:90
ReferenceError on a live path.Memory_Config has no import and no declaration in the file; optional chaining does not rescue an undeclared identifier (only typeof does). plannerAgent() is called at :149, so this throws in normal use.
I wrote that it "silently supplies a model the deployment never chose." It supplies nothing — it crashes. And with the import added, the cascade resolves google/gemma-4-26b-a4b, so || 'gemma4' is latent, never reached in this deployment. I described a hidden default that was doing no work and missed a hard crash.
Ollama.mjs:273
A reachable, divergent class default.Neo.create(Ollama, {}) yields modelName: 'gemma4' (measured) while aiConfig.ollama.model resolves gemma4:26b. Neo.ai.Agent:164 builds its provider as Neo.create(providerClass, this.providerConfig || {}), and Agent.providerConfig is declared null at :37 and set nowhere in the tree — so on that path the class default was the only model supplier, and it named a different tag.
Not a "literal to hoist": an adapter that read aiConfig itself would be a fresh Group-B violation. The defect is the divergent default, not the absence of a leaf read.
QA.mjs:26
A dead config that reads as load-bearing.Neo.ai.Agent declares no model config (only modelProvider), and nothing anywhere reads an agent-level .model — verified with a positive control (the same grep shape finds this.modelProvider at Agent.mjs:158). The key pinned nothing.
"Hoist it to a resolved leaf" would have preserved a config with no consumer, making a dead key look deliberate.
Origin of the roadmapPlanner bug, for the next reader: its sibling runSandman.mjs:4 — same directory — carries exactly the missing line, import Memory_Config from '../../mcp/server/memory-core/config.mjs'. The usage was copied; the import was not.
What is deliberately NOT a defect
Recorded because both look like defects and repairing either would be wrong.
Per-provider model ids legitimately differ.gemma4:26b (Ollama), google/gemma-4-26b-a4b (OpenAI-compatible) and mlx-community/gemma-4-26b-a4b-it-bf16 (MLX) are three correct identifiers for the same weights in three runtimes. Collapsing them to one string would break every provider but one. What is missing is only that nothing declares they denote one model.
gemma4:31b is not live. Its single occurrence is a historical comment at ai/provider/OpenAiCompatible.mjs:26 documenting a fixed bug — an Ollama-namespaced id that had been sitting in the OpenAI-compatible slot. That comment is correct practice and must stay.
The Architectural Reality
ai/configBase.mjs:714 / :723 — the two leaves on NEO_MODEL_PROVIDER.
ai/configBase.mjs:734 — graphProvider on NEO_GRAPH_PROVIDER. A legitimately separate axis, not part of this defect: its docblock records that graph extraction supports only native Ollama or OpenAI-compatible, while chat/summarisation may use Gemini. Leave it.
ai/provider/buildChatModel.mjs — the single chat dispatcher, and the alias's primary consumer. Chat dispatch is not diffuse; only its selector is.
ai/Agent.mjs:30-32,158 — reads modelProvider as an instance config defaulting to GeminiProvider, a second declaration of the same choice.
learn/agentos/decisions/0019-aiconfig-reactive-provider-ssot.md §3 Group B — the governing catalog entry.
The Fix
Finish the migration or retract it — do not leave both. Either point buildChatModel.mjs and Agent.mjs at chatProvider and delete modelProvider, or delete chatProvider and stop advertising a Tier-1 key that nothing reads. Deleting the unread leaf is the smaller change; moving the readers is the one that matches the documented intent. Pick one and make the docblocks true.
Hoist the three hardcoded 'gemma4' literals to resolved leaf reads, and remove the ?.-cascade plus || 'gemma4' hidden default at roadmapPlanner.mjs:90. An unresolvable model must fail loudly, matching embeddingProvider's parse hook, which "throws a named diagnostic on an unknown name at config resolution, because an unrecognized provider must never boot quietly."
Declare the model-identity mapping once so the three provider-namespaced ids are visibly one model. A comment at the leaf is sufficient; no new indirection.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
aiConfig.chatProvider
configBase.mjs:714
either the sole selector, or removed
no dual-leaf state survives
ADR-0019, leaf docblock
0 readers measured outside configBase
aiConfig.modelProvider
configBase.mjs:723
removed, or becomes the sole selector
one leaf on NEO_MODEL_PROVIDER
leaf docblock
8 read sites across 2 files
NEO_MODEL_PROVIDER
env
unchanged; one leaf binds it
—
none
bound twice today
aiConfig.graphProvider
configBase.mjs:734
unchanged
—
—
distinct provider set, documented
roadmapPlanner.mjs chat model + host
aiConfig.openAiCompatible
reads the resolved leaves directly and refuses an incomplete block by name
no ?. cascade and no || default — an unnamed endpoint or model must stop the run, not be substituted
ADR-0019 §3
Memory_Config had no import: the path threw ReferenceError, and the documented fallback was unreachable
Neo.ai.provider.Ollama#modelName
caller-injected
defaults no model; a missing id is rejected at request construction with a named diagnostic
never a provider-shaped fallback — 'gemma4' resolved a different tag than the configured gemma4:26b
class docblock + Base.mjs / OpenAiCompatible.mjs precedent
Neo.create(Ollama, {}) measured; the two dispatchers already inject
QA.mjs model key
none — it was never read
removed; model selection is not declared on the profile
no replacement key; Neo.ai.Agent declares no model config
profile docblock
positive control: the same grep shape finds this.modelProvider at Agent.mjs:158
provider-namespaced model identity
openAiCompatible.model leaf comment
the three runtime-specific ids are declared once as one model; the strings stay distinct
unifying the strings is forbidden — it would break two of three runtimes
entry retired; the guard now flags the cascade if it returns
a grandfather whose hit is gone silently re-admits the regression with CI green
checker JSDoc + spec
measured: 0 new violations without the entry; RED control fails the build when the cascade is restored
Ledger split 2026-08-21 (@neo-gpt, PR #17465 RA-2). The single "chat model literal" row said all three sites do a "resolved leaf read". That is true of exactly one of them, and this ticket's own Defect-2 section had already been corrected to say so — the narrative was fixed and the ledger was not. Same failure as #17467 earlier today: a reader consulting a ticket to act reads the operative table, so a correction that stops at the prose leaves the wrong instruction standing where it is used.
Decision Record impact
aligned-with ADR 0019. This ticket does not amend or challenge the ADR — it removes a Group-B violation the ADR already prohibits. Read-gate satisfied before authoring.
Acceptance Criteria
Exactly one leaf binds NEO_MODEL_PROVIDER; a grep for the removed key returns zero live references, with the search tree and command recorded.
Every remaining chat-provider docblock is true of the code: no leaf claims a role its reader count contradicts. A reviewer can verify the claim by counting readers.
roadmapPlanner.mjs declares the Memory_Config import its usage requires, reads openAiCompatible.{host,model} directly, and carries neither an optional-chain config cascade nor a || default — an incomplete block throws a named refusal instead of synthesizing a ROADMAP against an unnamed model.
Neo.ai.provider.Ollama declares no model default, so no caller can silently receive a model the deployment did not configure. Its docblock records the measured divergence ('gemma4' vs gemma4:26b) rather than merely asserting the new value.
QA.mjs's dead model key is removed, and its docblock says the key read nothing and where model selection actually lives — so its removal cannot be re-added as a "fix".
An unresolvable chat model fails with a named diagnostic at the request boundary, and a fixture asserts the throw plus a control asserting a valid model still dispatches — so the guard is not a blanket reject.
Retargeted from "at config resolution". The original wording cannot be satisfied: all three model leaves carry string defaults, so they always resolve, and a parse hook never fires on a value the env did not supply. The condition that actually reaches a provider is a model id of undefined/null arriving at dispatch — measured: Neo.create(Ollama, {modelName: undefined}) yields undefined, because Neo assigns an explicitly-undefined config rather than skipping it, so the two dispatchers that pass modelName: cfg.model from an omitted block hand the daemon no model at all. The named diagnostic belongs where that is observable. This retargets the AC to a detectable layer; it does not weaken the "must never boot quietly" requirement.
Both mutation diagonals are recorded, not just a green: restoring the removed default must redden the detection arms, and making the guard reject unconditionally must redden only the controls.
graphProvider is untouched, asserted explicitly, so the cleanup cannot quietly collapse a legitimately separate axis.
The three provider-namespaced gemma ids are declared once as one model; gemma4:31b's historical comment at OpenAiCompatible.mjs:26 is preserved.
Out of Scope
Agent.providerConfig is never set, and Agent re-implements provider selection. Surfaced while fixing Ollama.mjs: Agent.mjs:158-164 picks a provider class and constructs it with this.providerConfig || {}, duplicating what buildChatModel.mjs already does for every other caller — while providerConfig itself is null everywhere in the tree. Deliberately not fixed here: the repair is a fork (does Agent delegate to buildChatModel, which returns a Gemini-shaped wrapper rather than a provider instance, or does a shared mapper own the model→modelName translation?), and it changes how every agent profile obtains its provider. Guessing it inside this PR would put a cross-cutting design choice in a cleanup diff. Consequence stated plainly: until that lands, the Agent path fails by name instead of silently using an unconfigured model — the intended direction, and the reason the guard is worth shipping first. Its live test is gated behind NEO_RUN_LIVE_AI_TESTS, so CI is unaffected.
graphProvider consolidation — a deliberate separate axis with a documented provider-set difference. Named here so a cleanup pass does not absorb it.
Which provider a deployment should choose — this ticket makes the selector singular, not different.
The embedding lane — #17411 owns that consolidation; this is its chat-side sibling, deliberately not folded because the defects differ in kind (diffusion vs a dead authority).
Avoided Traps
Collapsing the three provider-namespaced model ids into one string. They are three correct ids for the same weights in three runtimes; unifying the strings would break two providers. Only the declaration that they denote one model is missing.
Deleting the gemma4:31b comment as a stale reference. It is the record of a fixed bug — an Ollama-namespaced id in an OpenAI-compatible slot — and is the reason that class is recognisable next time.
Treating this as the same defect as #17411. Embeddings diffuse one authority across many readers; chat declares an authority nobody reads. Opposite shapes, and a single fix serves neither.
Reading the docblocks as ground truth. The prose says chatProvider is Tier-1. The reader count says otherwise. This ticket exists because the prose was believed.
Related
#17411 — OPEN epic; the embedding-lane sibling, same "one authority" goal, different defect shape.
#17344 — OPEN; the same class on the transport leaf — a declared default that live deployments all override.
#17445 — CLOSED as superseded by #17447; it carried the same class on request-context binding.
Retrieval Hint: chatProvider has zero readers while modelProvider alias carries the lane; two leaves one NEO_MODEL_PROVIDER env var; gemma4 hardcoded outside AiConfig with a || hidden default in roadmapPlanner
tobiu referenced in commit c07c8a1 - "fix(ai): the chat lane stops declaring an authority nothing reads (#17448) (#17465) on Aug 21, 2026, 9:24 PM
Context
Surfaced by @tobiu while scoping post-v13.2 refactoring: "agent os has embedding and chat models => gemma4:26b. If you check the chat layers, you will probably say 'doh!', similar to embeddings. Maybe even worse."
It is worse, and in a different way. The embedding lane's problem is diffusion — one authority read by many subsystems, which
#17411is consolidating. The chat lane's problem is that the declared authority has no consumers at all, while the leaf its own docblock calls a legacy alias carries the entire lane.ADR-0019 read gate satisfied before authoring (
§critical_gates#10).Live latest-open sweep: latest 20 open issues read at 2026-08-21T09:00Z;
state:allsearches forchatProvider modelProvider,provider routing converges,chat provider axis alias,gemma model id namespace. Nearest prior art is a closed cluster (#12740/#12742/#12745, local-first provider defaults) and#12059(Sandman graph provider routing) — all closed, none covering this. A2A claim sweep over the latest 12 messages, all read-states: no in-flight claim on this scope.The Problem
Defect 1 — the Tier-1 leaf is dead and the alias is load-bearing
ai/configBase.mjs:705-723declares two leaves bound to one env var:/** @summary Deployment-wide chat / generation model provider. * Tier-1 source of truth for model-consuming Agent OS lanes. Memory Core maps * this into its historical `modelProvider` key until runtime provider routing * converges on one canonical key. */ chatProvider : leaf('openAiCompatible', 'NEO_MODEL_PROVIDER', 'string'), /** @summary Runtime alias for the active chat provider. * Existing Memory Core consumers read `modelProvider`; keep the Tier-1 * template aligned with `chatProvider` until provider routing converges on * one canonical key. */ modelProvider: leaf('openAiCompatible', 'NEO_MODEL_PROVIDER', 'string'),Measured consumers outside
configBase.mjs:chatProvidermodelProviderprovider/buildChatModel.mjs:69,87,107,110,122,Agent.mjs:30,32,158So the migration was declared and never performed: the canonical leaf was added and documented as Tier-1, and not one consumer moved to it. Today the prose and the code disagree about which key is authoritative, and the prose is the one a reader trusts.
This is an ADR-0019 Group B violation — indirection AROUND the SSOT (even when reading it) — sitting inside the file that defines the SSOT. The ADR's decision sentence is explicit: "Never re-implement, alias, export, pass-along, mutate, or defend against the SSOT." The instruction "keep the Tier-1 template aligned with
chatProvider" is manual synchronisation between two leaves, which is the definition of two sources of truth.The deferral is stated twice, in both docblocks, in the same words — "until provider routing converges on one canonical key." Nothing schedules that convergence.
Defect 2 — model literals live outside AiConfig, one behind a hidden default
Three live sites hardcode a chat model id rather than reading a resolved leaf:
ai/provider/Ollama.mjs:273modelName: 'gemma4'ai/agent/profile/QA.mjs:26model: 'gemma4'ai/scripts/runners/roadmapPlanner.mjs:90Memory_Config?.data?.openAiCompatible?.model || 'gemma4'CORRECTED 2026-08-21 — I described the wrong failure for all three, and each is a different defect. The original text called them three hardcoded literals with one shared fix. Measured at tree
3809616cdc:roadmapPlanner.mjs:90ReferenceErroron a live path.Memory_Confighas no import and no declaration in the file; optional chaining does not rescue an undeclared identifier (onlytypeofdoes).plannerAgent()is called at:149, so this throws in normal use.google/gemma-4-26b-a4b, so|| 'gemma4'is latent, never reached in this deployment. I described a hidden default that was doing no work and missed a hard crash.Ollama.mjs:273Neo.create(Ollama, {})yieldsmodelName: 'gemma4'(measured) whileaiConfig.ollama.modelresolvesgemma4:26b.Neo.ai.Agent:164builds its provider asNeo.create(providerClass, this.providerConfig || {}), andAgent.providerConfigis declarednullat:37and set nowhere in the tree — so on that path the class default was the only model supplier, and it named a different tag.aiConfigitself would be a fresh Group-B violation. The defect is the divergent default, not the absence of a leaf read.QA.mjs:26Neo.ai.Agentdeclares nomodelconfig (onlymodelProvider), and nothing anywhere reads an agent-level.model— verified with a positive control (the same grep shape findsthis.modelProvideratAgent.mjs:158). The key pinned nothing.Origin of the
roadmapPlannerbug, for the next reader: its siblingrunSandman.mjs:4— same directory — carries exactly the missing line,import Memory_Config from '../../mcp/server/memory-core/config.mjs'. The usage was copied; the import was not.What is deliberately NOT a defect
Recorded because both look like defects and repairing either would be wrong.
gemma4:26b(Ollama),google/gemma-4-26b-a4b(OpenAI-compatible) andmlx-community/gemma-4-26b-a4b-it-bf16(MLX) are three correct identifiers for the same weights in three runtimes. Collapsing them to one string would break every provider but one. What is missing is only that nothing declares they denote one model.gemma4:31bis not live. Its single occurrence is a historical comment atai/provider/OpenAiCompatible.mjs:26documenting a fixed bug — an Ollama-namespaced id that had been sitting in the OpenAI-compatible slot. That comment is correct practice and must stay.The Architectural Reality
ai/configBase.mjs:714/:723— the two leaves onNEO_MODEL_PROVIDER.ai/configBase.mjs:734—graphProvideronNEO_GRAPH_PROVIDER. A legitimately separate axis, not part of this defect: its docblock records that graph extraction supports only native Ollama or OpenAI-compatible, while chat/summarisation may use Gemini. Leave it.ai/provider/buildChatModel.mjs— the single chat dispatcher, and the alias's primary consumer. Chat dispatch is not diffuse; only its selector is.ai/Agent.mjs:30-32,158— readsmodelProvideras an instance config defaulting toGeminiProvider, a second declaration of the same choice.learn/agentos/decisions/0019-aiconfig-reactive-provider-ssot.md§3 Group B — the governing catalog entry.The Fix
buildChatModel.mjsandAgent.mjsatchatProviderand deletemodelProvider, or deletechatProviderand stop advertising a Tier-1 key that nothing reads. Deleting the unread leaf is the smaller change; moving the readers is the one that matches the documented intent. Pick one and make the docblocks true.'gemma4'literals to resolved leaf reads, and remove the?.-cascade plus|| 'gemma4'hidden default atroadmapPlanner.mjs:90. An unresolvable model must fail loudly, matchingembeddingProvider'sparsehook, which "throws a named diagnostic on an unknown name at config resolution, because an unrecognized provider must never boot quietly."Contract Ledger Matrix
aiConfig.chatProviderconfigBase.mjs:714configBaseaiConfig.modelProviderconfigBase.mjs:723NEO_MODEL_PROVIDERNEO_MODEL_PROVIDERaiConfig.graphProviderconfigBase.mjs:734roadmapPlanner.mjschat model + hostaiConfig.openAiCompatible?.cascade and no||default — an unnamed endpoint or model must stop the run, not be substitutedMemory_Confighad no import: the path threwReferenceError, and the documented fallback was unreachableNeo.ai.provider.Ollama#modelName'gemma4'resolved a different tag than the configuredgemma4:26bBase.mjs/OpenAiCompatible.mjsprecedentNeo.create(Ollama, {})measured; the two dispatchers already injectQA.mjsmodel keyNeo.ai.Agentdeclares nomodelconfigthis.modelProvideratAgent.mjs:158openAiCompatible.modelleaf commentgemma4:26b/google/gemma-4-26b-a4b/mlx-community/gemma-4-26b-a4b-it-bf16roadmapPlanner.mjsB3 grandfathercheck-aiconfig-antipatterns.mjsALLOWLIST.B3Decision Record impact
aligned-with ADR 0019. This ticket does not amend or challenge the ADR — it removes a Group-B violation the ADR already prohibits. Read-gate satisfied before authoring.Acceptance Criteria
Exactly one leaf binds
NEO_MODEL_PROVIDER; a grep for the removed key returns zero live references, with the search tree and command recorded.Every remaining chat-provider docblock is true of the code: no leaf claims a role its reader count contradicts. A reviewer can verify the claim by counting readers.
roadmapPlanner.mjsdeclares theMemory_Configimport its usage requires, readsopenAiCompatible.{host,model}directly, and carries neither an optional-chain config cascade nor a||default — an incomplete block throws a named refusal instead of synthesizing a ROADMAP against an unnamed model.Neo.ai.provider.Ollamadeclares no model default, so no caller can silently receive a model the deployment did not configure. Its docblock records the measured divergence ('gemma4'vsgemma4:26b) rather than merely asserting the new value.QA.mjs's deadmodelkey is removed, and its docblock says the key read nothing and where model selection actually lives — so its removal cannot be re-added as a "fix".An unresolvable chat model fails with a named diagnostic at the request boundary, and a fixture asserts the throw plus a control asserting a valid model still dispatches — so the guard is not a blanket reject.
Retargeted from "at config resolution". The original wording cannot be satisfied: all three model leaves carry string defaults, so they always resolve, and a
parsehook never fires on a value the env did not supply. The condition that actually reaches a provider is a model id ofundefined/nullarriving at dispatch — measured:Neo.create(Ollama, {modelName: undefined})yieldsundefined, because Neo assigns an explicitly-undefined config rather than skipping it, so the two dispatchers that passmodelName: cfg.modelfrom an omitted block hand the daemon no model at all. The named diagnostic belongs where that is observable. This retargets the AC to a detectable layer; it does not weaken the "must never boot quietly" requirement.Both mutation diagonals are recorded, not just a green: restoring the removed default must redden the detection arms, and making the guard reject unconditionally must redden only the controls.
graphProvideris untouched, asserted explicitly, so the cleanup cannot quietly collapse a legitimately separate axis.The three provider-namespaced gemma ids are declared once as one model;
gemma4:31b's historical comment atOpenAiCompatible.mjs:26is preserved.Out of Scope
Agent.providerConfigis never set, andAgentre-implements provider selection. Surfaced while fixingOllama.mjs:Agent.mjs:158-164picks a provider class and constructs it withthis.providerConfig || {}, duplicating whatbuildChatModel.mjsalready does for every other caller — whileproviderConfigitself isnulleverywhere in the tree. Deliberately not fixed here: the repair is a fork (doesAgentdelegate tobuildChatModel, which returns a Gemini-shaped wrapper rather than a provider instance, or does a shared mapper own themodel→modelNametranslation?), and it changes how every agent profile obtains its provider. Guessing it inside this PR would put a cross-cutting design choice in a cleanup diff. Consequence stated plainly: until that lands, the Agent path fails by name instead of silently using an unconfigured model — the intended direction, and the reason the guard is worth shipping first. Its live test is gated behindNEO_RUN_LIVE_AI_TESTS, so CI is unaffected.graphProviderconsolidation — a deliberate separate axis with a documented provider-set difference. Named here so a cleanup pass does not absorb it.Which provider a deployment should choose — this ticket makes the selector singular, not different.
The embedding lane —
#17411owns that consolidation; this is its chat-side sibling, deliberately not folded because the defects differ in kind (diffusion vs a dead authority).Avoided Traps
Collapsing the three provider-namespaced model ids into one string. They are three correct ids for the same weights in three runtimes; unifying the strings would break two providers. Only the declaration that they denote one model is missing.
Deleting the
gemma4:31bcomment as a stale reference. It is the record of a fixed bug — an Ollama-namespaced id in an OpenAI-compatible slot — and is the reason that class is recognisable next time.Treating this as the same defect as
#17411. Embeddings diffuse one authority across many readers; chat declares an authority nobody reads. Opposite shapes, and a single fix serves neither.Reading the docblocks as ground truth. The prose says
chatProvideris Tier-1. The reader count says otherwise. This ticket exists because the prose was believed.Related
#17411— OPEN epic; the embedding-lane sibling, same "one authority" goal, different defect shape.#17344— OPEN; the same class on the transport leaf — a declared default that live deployments all override.#17445— CLOSED as superseded by#17447; it carried the same class on request-context binding.Origin Session ID: 92274805-40ec-4eb9-b778-479c4f96896b
Retrieval Hint:
chatProvider has zero readers while modelProvider alias carries the lane; two leaves one NEO_MODEL_PROVIDER env var; gemma4 hardcoded outside AiConfig with a || hidden default in roadmapPlanner