Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ All paths in the protocol should be absolute
- Run `npm run check`
- Update the example agents and clients in tests and examples in both libraries

## Schema rules

- For any nullable field, explicitly define whether it is required or optional and whether `null` is equivalent to an omitted key before running schema generation.

## Updating existing methods, their params, or output

- Update the mintlify docs and guides in the `docs` directory
Expand Down
4 changes: 2 additions & 2 deletions docs/protocol/draft/schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5908,9 +5908,9 @@ these keys.
See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)

</ResponseField>
<ResponseField name="current" type={<><span><a href="#providercurrentconfig">ProviderCurrentConfig</a></span><span> | null</span></>} required>
<ResponseField name="current" type={<><span><a href="#providercurrentconfig">ProviderCurrentConfig</a></span><span> | null</span></>} >
Current effective non-secret routing config.
Null means provider is disabled.
Null or omitted means provider is disabled.
</ResponseField>
<ResponseField name="id" type={"string"} required>
Provider identifier, for example "main" or "openai".
Expand Down
11 changes: 6 additions & 5 deletions docs/rfds/custom-llm-endpoint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ interface ProviderInfo {

/**
* Current effective non-secret routing config.
* Null means provider is disabled.
* Null or omitted means provider is disabled.
*/
current: ProviderCurrentConfig | null;
current?: ProviderCurrentConfig | null;

/** Extension metadata */
_meta?: Record<string, unknown>;
Expand Down Expand Up @@ -336,9 +336,9 @@ interface ProvidersDisableResponse {
2. **Timing and session impact**: provider methods MUST be called after `initialize`. Clients SHOULD configure providers before creating or loading sessions. Agents MAY choose not to apply changes to already running sessions, but SHOULD apply them to sessions created or loaded after the change.
3. **List semantics**: `providers/list` returns configurable providers, their supported protocol types, current effective routing, and `required` flag. Providers SHOULD remain discoverable in list after `providers/disable`.
4. **Client behavior for required providers**: clients SHOULD NOT call `providers/disable` for providers where `required: true`.
5. **Disabled state encoding**: in `providers/list`, `current: null` means the provider is disabled and MUST NOT be used by the agent for LLM calls.
5. **Disabled state encoding**: in `providers/list`, `current` omitted or `current: null` means the provider is disabled and MUST NOT be used by the agent for LLM calls.
6. **Set semantics and validation**: `providers/set` replaces the full configuration for the target `id` (`apiType`, `baseUrl`, `headers`); an omitted `headers` field is treated as an empty map. If `id` is unknown, `apiType` is unsupported for that provider, or params are malformed, agents SHOULD return `invalid_params`.
7. **Disable semantics**: `providers/disable` disables the target provider at runtime. A disabled provider MUST appear in `providers/list` with `current: null`. If target provider has `required: true`, agents MUST return `invalid_params`. Disabling an unknown `id` SHOULD be treated as success (idempotent behavior).
7. **Disable semantics**: `providers/disable` disables the target provider at runtime. A disabled provider MUST appear in `providers/list` with `current` omitted or `current: null`. If target provider has `required: true`, agents MUST return `invalid_params`. Disabling an unknown `id` SHOULD be treated as success (idempotent behavior).
8. **Scope and persistence**: provider configuration is process-scoped and SHOULD NOT be persisted to disk.

## Frequently asked questions
Expand All @@ -347,7 +347,7 @@ interface ProvidersDisableResponse {

### What does `null` mean in `providers/list`?

`current: null` means the provider is disabled.
`current` omitted or `current: null` means the provider is disabled.

When disabled, the agent MUST NOT route LLM calls through that provider until the client enables it again with `providers/set`.

Expand Down Expand Up @@ -395,6 +395,7 @@ Today, `session-config` values are effectively string-oriented and do not define

## Revision history

- 2026-04-19: Made `ProviderInfo.current` optional in `providers/list`; disabled state may be encoded as omitted `current` or `current: null`
- 2026-03-22: Finalized provider disable semantics - `providers/remove` renamed to `providers/disable`, required providers are non-disableable, and disabled state is represented as `current: null`
- 2026-03-21: Initial draft of provider configuration API (`providers/list`, `providers/set`, `providers/remove`)
- 2026-03-07: Rename "provider" to "protocol" to reflect API compatibility level; make `LlmProtocol` an open string type with well-known values; resolve open questions on identifier standardization and model availability
Expand Down
4 changes: 2 additions & 2 deletions schema/schema.unstable.json
Original file line number Diff line number Diff line change
Expand Up @@ -4879,7 +4879,7 @@
"type": "null"
}
],
"description": "Current effective non-secret routing config.\nNull means provider is disabled."
"description": "Current effective non-secret routing config.\nNull or omitted means provider is disabled."
},
"id": {
"description": "Provider identifier, for example \"main\" or \"openai\".",
Expand All @@ -4897,7 +4897,7 @@
"type": "array"
}
},
"required": ["id", "supported", "required", "current"],
"required": ["id", "supported", "required"],
"type": "object"
},
"ProvidersCapabilities": {
Expand Down
28 changes: 13 additions & 15 deletions src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};

#[cfg(feature = "unstable_llm_providers")]
use crate::RequiredNullable;
use crate::{
ClientCapabilities, ContentBlock, ExtNotification, ExtRequest, ExtResponse, IntoOption, Meta,
ProtocolVersion, SessionId, SkipListener,
Expand Down Expand Up @@ -3537,8 +3535,8 @@ pub struct ProviderInfo {
/// If true, clients must not call `providers/disable` for this id.
pub required: bool,
/// Current effective non-secret routing config.
/// Null means provider is disabled.
pub current: RequiredNullable<ProviderCurrentConfig>,
/// Null or omitted means provider is disabled.
pub current: Option<ProviderCurrentConfig>,
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
/// these keys.
Expand All @@ -3555,13 +3553,13 @@ impl ProviderInfo {
id: impl Into<String>,
supported: Vec<LlmProtocol>,
required: bool,
current: impl Into<RequiredNullable<ProviderCurrentConfig>>,
current: impl IntoOption<ProviderCurrentConfig>,
) -> Self {
Self {
id: id.into(),
supported,
required,
current: current.into(),
current: current.into_option(),
meta: None,
}
}
Expand Down Expand Up @@ -6007,9 +6005,9 @@ mod test_serialization {
assert_eq!(deserialized.id, "main");
assert_eq!(deserialized.supported.len(), 2);
assert!(deserialized.required);
assert!(deserialized.current.is_value());
assert!(deserialized.current.is_some());
assert_eq!(
deserialized.current.value().unwrap().api_type,
deserialized.current.as_ref().unwrap().api_type,
LlmProtocol::Anthropic
);
}
Expand All @@ -6021,7 +6019,7 @@ mod test_serialization {
"secondary",
vec![LlmProtocol::OpenAi],
false,
RequiredNullable::<ProviderCurrentConfig>::null(),
None::<ProviderCurrentConfig>,
);

let json = serde_json::to_value(&info).unwrap();
Expand All @@ -6030,27 +6028,27 @@ mod test_serialization {
json!({
"id": "secondary",
"supported": ["openai"],
"required": false,
"current": null
"required": false
})
);

let deserialized: ProviderInfo = serde_json::from_value(json).unwrap();
assert_eq!(deserialized.id, "secondary");
assert!(!deserialized.required);
assert!(deserialized.current.is_null());
assert!(deserialized.current.is_none());
}

#[cfg(feature = "unstable_llm_providers")]
#[test]
fn test_provider_info_missing_current_fails() {
// current is required-but-nullable — omitting it entirely must fail
fn test_provider_info_missing_current_defaults_to_none() {
// current is optional; omitting it should decode as None
let json = json!({
"id": "main",
"supported": ["anthropic"],
"required": true
});
assert!(serde_json::from_value::<ProviderInfo>(json).is_err());
let deserialized: ProviderInfo = serde_json::from_value(json).unwrap();
assert!(deserialized.current.is_none());
}

#[cfg(feature = "unstable_llm_providers")]
Expand Down