Commit 68e29cc
authored
feat(providers): add shared authentication and resilience (#16)
## Summary
Adds shared authentication and resilience configuration across AgentKit
model providers. Introduces separate OpenAI Chat Completions and
Responses adapters, including configurable support for public OpenAI
Responses and private ChatGPT deployments.
## Motivation
Provider adapters previously stored raw credential strings and
implemented inconsistent authentication, retry, timeout, and
stream-recovery behavior. This change gives providers reusable
HTTP-layer authentication and resilience primitives while keeping each
provider’s authentication scheme and wire protocol explicit.
## Impact
Existing constructors continue to accept string credentials, and omitted
resilience preserves the current single-attempt behavior. The following
public API changes require migration:
- Provider config fields such as `api_key` and `auth_token` become
`authentication: Authentication`; Ollama and vLLM use
`Option<Authentication>` because authentication remains optional.
- Provider config structs gain `resilience: Option<ResilienceConfig>`.
Exhaustive struct literals must add `resilience: None` or move to
constructors and builders.
- `SessionConfig` gains `consumer_capabilities`; direct struct literals
must add the field or use `SessionConfig::new(...)`.
- `ModelTurnEvent` and `AgentEvent` gain `ResponseAttemptSuperseded`;
exhaustive matches must handle the new variant.
- Anthropic still distinguishes `x-api-key` from bearer authentication.
A bare string converted directly to `Authentication` always means bearer
authentication.
## Technical details
### Shared authentication API
`agentkit-http` now exports `Authentication`, `AuthenticationAttempt`,
and the asynchronous `AuthenticationProvider` contract. A provider
receives `None` for initial authentication and the rejected attempt
after a `401`, allowing one controlled refresh while keeping
provider-private state opaque.
Static bearer authentication remains concise:
```rust
use agentkit_http::Authentication;
let from_string: Authentication = "sk-example".into();
let explicit = Authentication::bearer("sk-example");
```
Refreshable authentication can retain rejected-attempt state and attach
a stable, non-secret binding:
```rust
use agentkit_http::{
header, Authentication, AuthenticationAttempt, AuthenticationProvider,
HeaderMap, HeaderValue, HttpError,
};
use async_trait::async_trait;
#[derive(Clone, Copy)]
struct RotatingTokens;
#[async_trait]
impl AuthenticationProvider for RotatingTokens {
async fn authenticate(
&self,
previous: Option<&AuthenticationAttempt>,
) -> Result<AuthenticationAttempt, HttpError> {
let generation = previous
.and_then(|attempt| attempt.state::<u64>())
.copied()
.map_or(0, |generation| generation + 1);
let value = match generation {
0 => HeaderValue::from_static("Bearer initial-token"),
_ => HeaderValue::from_static("Bearer refreshed-token"),
};
let mut headers = HeaderMap::new();
headers.insert(header::AUTHORIZATION, value);
Ok(AuthenticationAttempt::new(headers, generation)
.with_binding(format!("credential-generation-{generation}")))
}
}
let authentication = Authentication::new(RotatingTokens);
```
The rejected-attempt refresh is independent of resilience retries.
OpenAI Responses additionally requires the refreshed attempt to retain
the original non-secret binding before replaying continuation state.
### Optional resilience
All provider configurations can opt into a shared retry and timeout
policy:
```rust
use std::time::Duration;
use agentkit_http::ResilienceConfig;
use agentkit_provider_openai::{
OpenAIChatCompletionsAdapter, OpenAIConfig,
};
let resilience = ResilienceConfig {
max_retries: 3,
retry_budget: Duration::from_secs(60),
attempt_timeout: Some(Duration::from_secs(30)),
stream_idle_timeout: Some(Duration::from_secs(30)),
initial_backoff: Duration::from_millis(200),
max_backoff: Duration::from_secs(10),
};
let adapter = OpenAIChatCompletionsAdapter::new(
OpenAIConfig::new("sk-openai", "gpt-4o")
.with_resilience(resilience),
)?;
```
`resilience: None` preserves the existing no-timeout, single-attempt
behavior. `Some(ResilienceConfig::default())` enables bounded retries
and timeouts; it is not equivalent to omission.
Authentication schemes remain provider-specific:
| Providers | Authentication behavior |
|---|---|
| OpenAI, OpenRouter, Groq, Mistral, Baseten, Cerebras | Bearer by
default |
| Anthropic | `x-api-key` via `new`, bearer via `with_auth_token` |
| Ollama, vLLM | Optional; unauthenticated by default |
For Anthropic, use the constructor matching the intended scheme:
```rust
use agentkit_provider_anthropic::AnthropicConfig;
let api_key = AnthropicConfig::new(
"sk-ant",
"claude-sonnet-4-6",
4096,
)?; // x-api-key
let bearer = AnthropicConfig::with_auth_token(
"oauth-token",
"claude-sonnet-4-6",
4096,
)?; // Authorization: Bearer
```
Protected Ollama and vLLM deployments can now opt in without changing
local defaults:
```rust
use agentkit_provider_ollama::OllamaConfig;
use agentkit_provider_vllm::VllmConfig;
let local = OllamaConfig::new("llama3.2");
let protected = VllmConfig::new("Qwen/Qwen3-8B")
.with_api_key("server-token");
```
### Separate OpenAI schema adapters
Chat Completions and Responses remain separate request codecs and stream
state machines. The explicit Chat Completions name is
`OpenAIChatCompletionsAdapter`; the historical `OpenAIAdapter` alias
remains available.
```rust
use agentkit_provider_openai::{
OpenAIChatCompletionsAdapter, OpenAIConfig,
OpenAIResponsesAdapter, OpenAIResponsesConfig,
};
let chat = OpenAIChatCompletionsAdapter::new(
OpenAIConfig::new("sk-openai", "gpt-4o")
.with_max_completion_tokens(4096),
)?;
let responses = OpenAIResponsesAdapter::new(
OpenAIResponsesConfig::new("sk-openai", "gpt-5")
.with_reasoning_effort("medium")
.with_max_output_tokens(4096),
)?;
```
`OpenAIResponsesConfig::new(authentication, model)` follows the existing
`OpenAIConfig` argument order. Profile-specific constructors are
model-first:
```rust
let public = OpenAIResponsesConfig::public(
"gpt-5",
"sk-openai",
);
let private = OpenAIResponsesConfig::chatgpt_private(
"gpt-5-codex",
Authentication::new(RotatingTokens),
)
.with_originator("my-client")
.with_user_agent("my-client/1.0");
```
The Responses adapter supports configurable endpoints, request policy,
limits, attribution, encrypted reasoning continuation, idempotency,
response-size bounds, and private ChatGPT request/stream behavior
without embedding any consumer-specific metadata or persistence formats.
### Typed response-attempt supersession
OpenAI Responses retries only before visible output by default. A
consumer that can discard an already-rendered attempt may opt into
post-output recovery with a typed capability:
```rust
use agentkit_loop::{ModelTurnEvent, SessionConfig};
let session = SessionConfig::new("session-1")
.with_response_attempt_supersession();
fn retain_latest_attempt(
events: impl IntoIterator<Item = ModelTurnEvent>,
) -> Vec<ModelTurnEvent> {
let mut current_attempt = Vec::new();
for event in events {
match event {
ModelTurnEvent::ResponseAttemptSuperseded => {
current_attempt.clear();
}
other => current_attempt.push(other),
}
}
current_attempt
}
```
Opting in asserts that the consumer can discard all deltas, tool calls,
usage, and reconstruction state from the failed attempt. AgentKit emits
`ResponseAttemptSuperseded` after the failed attempt and before any
replacement output. Without the capability, a failure after visible
output is returned instead of replayed.1 parent db9713c commit 68e29cc
47 files changed
Lines changed: 9910 additions & 542 deletions
File tree
- book/src
- crates
- agentkit-adapter-completions/src
- agentkit-http
- src
- agentkit-integration-tests/src
- agentkit-loop/src
- agentkit-provider-anthropic
- src
- agentkit-provider-baseten
- src
- agentkit-provider-cerebras
- src
- agentkit-provider-groq
- src
- agentkit-provider-mistral
- src
- agentkit-provider-ollama
- src
- agentkit-provider-openai
- src
- agentkit-provider-openrouter
- src
- agentkit-provider-vllm
- src
- docs
- examples/cerebras-chat/src
- projects/how/src
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
122 | 122 | | |
123 | 123 | | |
124 | 124 | | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
125 | 141 | | |
126 | 142 | | |
127 | | - | |
| 143 | + | |
128 | 144 | | |
129 | 145 | | |
130 | 146 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
254 | 254 | | |
255 | 255 | | |
256 | 256 | | |
| 257 | + | |
257 | 258 | | |
258 | 259 | | |
259 | 260 | | |
260 | 261 | | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
261 | 268 | | |
262 | 269 | | |
263 | 270 | | |
| |||
287 | 294 | | |
288 | 295 | | |
289 | 296 | | |
290 | | - | |
| 297 | + | |
| 298 | + | |
291 | 299 | | |
292 | 300 | | |
293 | 301 | | |
294 | 302 | | |
295 | | - | |
| 303 | + | |
| 304 | + | |
296 | 305 | | |
297 | 306 | | |
298 | 307 | | |
| |||
302 | 311 | | |
303 | 312 | | |
304 | 313 | | |
305 | | - | |
| 314 | + | |
| 315 | + | |
306 | 316 | | |
307 | 317 | | |
308 | 318 | | |
| |||
348 | 358 | | |
349 | 359 | | |
350 | 360 | | |
351 | | - | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
352 | 364 | | |
353 | | - | |
354 | | - | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
355 | 370 | | |
356 | 371 | | |
357 | 372 | | |
| |||
619 | 634 | | |
620 | 635 | | |
621 | 636 | | |
622 | | - | |
| 637 | + | |
623 | 638 | | |
624 | | - | |
| 639 | + | |
625 | 640 | | |
626 | 641 | | |
627 | 642 | | |
| |||
633 | 648 | | |
634 | 649 | | |
635 | 650 | | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
636 | 655 | | |
637 | 656 | | |
638 | 657 | | |
639 | 658 | | |
640 | | - | |
| 659 | + | |
641 | 660 | | |
642 | 661 | | |
643 | 662 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
52 | | - | |
53 | | - | |
54 | | - | |
55 | | - | |
56 | | - | |
57 | | - | |
58 | | - | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | | - | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
64 | 69 | | |
65 | 70 | | |
66 | 71 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
| 108 | + | |
108 | 109 | | |
109 | 110 | | |
110 | 111 | | |
| |||
129 | 130 | | |
130 | 131 | | |
131 | 132 | | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
132 | 136 | | |
133 | 137 | | |
134 | 138 | | |
135 | 139 | | |
136 | 140 | | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
137 | 148 | | |
138 | 149 | | |
139 | 150 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
| |||
83 | 84 | | |
84 | 85 | | |
85 | 86 | | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
95 | | - | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
96 | 93 | | |
97 | 94 | | |
98 | 95 | | |
| |||
225 | 222 | | |
226 | 223 | | |
227 | 224 | | |
228 | | - | |
229 | | - | |
230 | | - | |
231 | | - | |
232 | | - | |
233 | | - | |
234 | | - | |
235 | | - | |
236 | | - | |
237 | | - | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
238 | 231 | | |
239 | 232 | | |
240 | 233 | | |
| |||
0 commit comments