Configuration System
The configuration layer of Coding Agent does not end at "filling in the provider and API Key."
In the current implementation, configuration actually controls three things:
- Which provider / protocol / model this session ultimately connects to
- Which layer each of these values comes from: CLI, environment, global assets, or workspace bindings
- After a configuration change, whether it only mutates JSON or rebuilds the current session runtime
If you want to understand this system from the source, the entry points most worth reading directly are:
ai4j-cli/.../provider/CliProviderConfigManagerai4j-cli/.../mcp/CliMcpConfigManagerai4j-cli/.../config/CliWorkspaceConfigai4j-cli/.../factory/DefaultCodingCliAgentFactoryai4j-cli/.../runtime/CodingCliSessionRunnerai4j-cli/.../acp/AcpJsonRpcServer
1. Start with the real configuration chain
On the CLI / TUI path, a configuration resolution roughly goes through this chain:
Ai4jCli / CodeCommand
-> CodeCommandOptionsParser.parse(...)
-> CliProviderConfigManager.resolve(...)
-> CliMcpConfigManager.resolve(...)
-> DefaultCodingCliAgentFactory.resolveProtocol(...)
-> buildWorkspaceContext(...)
-> buildCodingOptions(...)
-> CodingAgentBuilder.build()
The ACP path follows the same base rules; it just enters from AcpCommand / AcpJsonRpcServer instead of the terminal runner.
This chain has an important consequence:
- The "effective value" of provider / protocol / model is not produced by a single file directly
- It is the result computed jointly by the resolver, workspace config, global config, and runtime overrides
2. The current configuration surface is split into three layers
2.1 Global provider assets
Path:
~/.ai4j/providers.json
Determined by CliProviderConfigManager.globalProvidersPath().
It stores:
defaultProfileprofiles
In other words, "which reusable provider profiles exist on this machine," not which one a given repo is currently bound to.
2.2 Global MCP definitions
Path:
~/.ai4j/mcp.json
Determined by CliMcpConfigManager.globalMcpPath().
It answers:
- Which MCP servers are defined on this machine
It does not directly answer:
- Which MCP servers are enabled for the current repo
2.3 Workspace bindings
Path:
<workspace>/.ai4j/workspace.json
Both CliProviderConfigManager and CliMcpConfigManager land in this file.
It currently carries repo-level bindings and toggles, for example:
activeProfilemodelOverrideenabledMcpServersskillDirectoriesagentDirectoriesexperimentalSubagentsEnabledexperimentalAgentTeamsEnabled
The core idea here is:
- Global files store "asset definitions"
- The workspace file stores "per-repo selection and local overrides"
3. workspace.json is not a runtime state directory
Currently <workspace>/.ai4j/ typically contains more than one file:
<workspace>/.ai4j/
workspace.json
sessions/
teams/
state/
mailbox/
Keep them clearly distinct:
workspace.json: human-maintained repo-level configurationsessions/: session snapshots and event ledgersteams/state: team runtime-state snapshotsteams/mailbox: member message streams
That is:
workspace.jsonis the control planesessions/andteams/are runtime artifacts
If you conflate these, it is easy to mistake "persisted state" for "static configuration."
4. Provider resolution is not a whole-object merge, but per-field evaluation
CliProviderConfigManager.resolve(...) does not first merge several JSON blobs into one big object and then read from it.
It walks the precedence chain separately for each field.
4.1 provider
The current order is:
- CLI explicit
--provider activeProfile.providerdefaultProfile.providerAI4J_PROVIDERai4j.provider- Default value
openai
4.2 baseUrl
The current order is:
- CLI explicit
--base-url activeProfile.baseUrldefaultProfile.baseUrlAI4J_BASE_URLai4j.base-url
4.3 protocol
The current order is:
- CLI explicit
--protocol activeProfile.protocoldefaultProfile.protocolAI4J_PROTOCOLai4j.protocol- If empty or
auto, fall back toCliProtocol.defaultProtocol(...)
4.4 model
The current order is:
- CLI explicit
--model workspace.json.modelOverrideactiveProfile.modeldefaultProfile.modelAI4J_MODELai4j.model
The most important point here is:
modelOverridetakes precedence over the model baked into the profile
This lets a repo stay bound to a given provider profile while experimenting with a different model, without polluting the global profile.
4.5 apiKey
The current order is:
- CLI explicit
--api-key activeProfile.apiKeydefaultProfile.apiKeyAI4J_API_KEYai4j.api.key- Provider-specific env, e.g.
OPENAI_API_KEY
This shows the current implementation supports both:
- A generic key entry point
- A provider-specific env-var entry point
5. An explicit --provider affects whether the profile still participates in resolution
This is an easily overlooked but critical detail in the current implementation.
When CliProviderConfigManager.resolve(...) encounters an explicit providerOverride, it first runs:
alignProfileWithProvider(activeProfile, explicitProvider)alignProfileWithProvider(defaultProfile, explicitProvider)
This means:
- If the current
activeProfileiszhipu - but you explicitly pass
--provider openai
then this zhipu profile will no longer serve as a fallback for baseUrl, model, or apiKey.
It is treated as "no longer matching the current provider."
This behavior is sensible, because otherwise you could get:
- provider switched to
openai - while still secretly inheriting another provider profile's URL or key
That would make runtime behavior highly unpredictable.
6. activeProfile, defaultProfile, and effectiveProfile are not the same thing
CliResolvedProviderConfig retains all of:
activeProfiledefaultProfileeffectiveProfile
They respectively mean:
activeProfile: the profile name the workspace wants to bind todefaultProfile: the machine-level global default profile nameeffectiveProfile: the profile name actually used in resolution in the current context
The rule for effectiveProfile is:
- Prefer the workspace
activeProfile, provided it actually exists - Otherwise fall back to the global
defaultProfile, provided it also exists - Otherwise
null
So:
- A non-existent profile name written in the workspace will not take effect by force
- The system falls back instead of silently fabricating a profile
7. Loaded configuration is normalized first
Currently, loadProvidersConfig(), loadWorkspaceConfig(), and loadGlobalConfig() do not "just parse the JSON as-is and finish."
They immediately run a normalize pass.
Common effects include:
- Trimming leading and trailing whitespace from fields
- Normalizing empty strings to
null - Removing blank profile names
- Clearing a non-existent
defaultProfile enabledMcpServers,skillDirectories, andagentDirectoriesare deduplicated (drop empties, keep order)- MCP
httptransport is normalized tostreamable_http
So this system is closer to a "controlled configuration model" than an "arbitrary JSON store."
8. The protocol default has a local inference rule, not dynamic probing
CliProtocol.defaultProtocol(provider, baseUrl) is currently a static rule:
openaiandbaseUrlis empty or containsapi.openai.com->responsesopenaiwith a custom compatible host ->chatdoubao/dashscope->responses- Other providers ->
chat
There is no remote capability probe behind this.
That is:
- The default protocol is a local inference result
- It does not ask the provider at runtime "which protocol do you support?"
9. "Can be inferred by default" does not equal "allowed at runtime"
Currently, DefaultCodingCliAgentFactory.resolveProtocol(...) and the provider/protocol validation in the ACP path add another layer of runtime restriction.
At present, responses is explicitly allowed only for:
openaidoubaodashscope
So there are currently two layers of checks:
- Infer the default protocol
- Validate whether the current provider supports this protocol
These two layers are separate in the implementation, so when troubleshooting, distinguish:
- Whether the default was inferred wrongly
- Or the default was inferred but runtime rejects this combination
10. experimental* is currently default-on
CliWorkspaceConfig has two easily overlooked boolean toggles:
experimentalSubagentsEnabledexperimentalAgentTeamsEnabled
The current logic in DefaultCodingCliAgentFactory is:
- When the field is
null, treat it astrue
So omitting these two fields does not turn the experimental capabilities off; it leaves them on by default.
They do not control provider connection; they control which experimental tools are injected into the current runtime:
- The experimental subagent tool surface
- The experimental team tool surface
11. Which changes trigger a rebind of the current session runtime
This is the biggest difference between the Coding Agent configuration system and ordinary SDK configuration docs.
On the current CLI / TUI path, commands like these do not merely edit files:
/provider use/provider add|edit|default|remove/model/stream on|off/experimental .../mcp enable|disable|pause|resume|retry|remove
These operations usually flow through:
Mutate configuration or in-memory state
-> resolveConfiguredRuntimeOptions(...)
-> switchSessionRuntime(...)
-> rebuild the current session host layer with new options / new MCP runtime
So this configuration system is essentially a live control plane.
It is not "save the config and wait for the next restart."
12. Which configuration is mutable in ACP
ACP mode does not replicate all CLI configuration capabilities.
Currently, the configOptions exposed by AcpJsonRpcServer has only two entries:
modemodel
The corresponding methods are:
session/set_modesession/set_config_option
Where:
modecontrols the approval mode within an ACP sessionmodelcontrols the effective model for subsequent turns
This shows ACP currently leans toward "session-time control" rather than being a "full configuration file editor."
13. Which values are not in workspace.json at all
Many people intuitively assume that, since these are workspace bindings, baseUrl and apiKey should land there too.
But currently they do not.
workspace.json primarily carries:
- Profile bindings
- Model override
- MCP enablement
- Skill / agent roots
- Experimental toggles
Whereas the following values do not exist as persistent workspace fields:
apiKeybaseUrl- Provider profile details
They still belong to:
- CLI override
- env / property
- Global profile assets
This boundary is deliberate, because mixing sensitive credentials with repo bindings carries high risk.
14. The most common failure paths
14.1 activeProfile points to a non-existent name
Result:
- It does not take effect as the profile you intended
- It falls back to
defaultProfileor the env-var chain
14.2 After an explicit --provider, the original profile no longer matches
Result:
- The old profile's
baseUrl/model/apiKeymay no longer participate in evaluation
14.3 Custom OpenAI-compatible host, but forgetting the protocol default changes
Result:
openai + custom baseUrldefaults tochat, notresponsesas on the official OpenAI host
14.4 Assuming that editing a config file requires restarting the entire CLI
Result:
- Many slash command paths actually rebuild the current session runtime directly
- Without understanding this, you may misjudge "why the config took effect immediately"
14.5 Mixing runtime strategy with connection configuration
Result:
- Mixing
provider/profile/modelissues withauto-compact/ outer loop / tool approval issues makes troubleshooting painful
15. What does not belong on this page
This page covers:
- "Who to connect to"
- "Where it is resolved from"
- "Whether the runtime rebinds after a change"
It does not cover another set of equally important runtime strategy parameters:
- auto-compact
- compact window
- reserve tokens
- keep recent tokens
- approval behavior
- outer loop stop / continue behavior
These belong more to:
CodingAgentOptions- session / runtime behavior layer
Do not conflate provider/profile configuration with loop / compact strategy into a single mental model on one page.
16. The takeaways most worth remembering from this page
- The current configuration surface is split into three layers: global provider assets, global MCP definitions, and workspace bindings
- Configuration resolution is per-field evaluation, not a whole-object merge
- An explicit
--providerdrops non-matching profiles out of the resolution chain modelOverridetakes precedence over the profile's model- The protocol default is inferred by local rules, not by remote probing
- Many configuration commands rebuild the current session runtime directly, rather than waiting for the next launch to take effect