Skip to content

feat(providers): add shared authentication and resilience - #16

Merged
danielkov merged 5 commits into
mainfrom
feat/provider-resiliency
Aug 30, 2026
Merged

feat(providers): add shared authentication and resilience#16
danielkov merged 5 commits into
mainfrom
feat/provider-resiliency

Conversation

@danielkov

@danielkov danielkov commented Aug 29, 2026

Copy link
Copy Markdown
Owner

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:

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:

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:

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:

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:

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.

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:

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:

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.

@danielkov
danielkov changed the base branch from main to feat/acp-v2-session-inject August 29, 2026 20:54
@danielkov
danielkov force-pushed the feat/provider-resiliency branch from eba8025 to 99dcb62 Compare August 30, 2026 00:35
@danielkov
danielkov changed the base branch from feat/acp-v2-session-inject to main August 30, 2026 00:35
@danielkov
danielkov merged commit 68e29cc into main Aug 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant