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

@danielkovdanielkov 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)]structRotatingTokens;#[async_trait]implAuthenticationProviderforRotatingTokens{asyncfnauthenticate(&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"),};letmut 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:

ProvidersAuthentication behavior
OpenAI, OpenRouter, Groq, Mistral, Baseten, CerebrasBearer by default
Anthropicx-api-key via new, bearer via with_auth_token
Ollama, vLLMOptional; 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-keylet 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();fnretain_latest_attempt(events:implIntoIterator<Item = ModelTurnEvent>,) -> Vec<ModelTurnEvent>{letmut 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-injectAugust 29, 2026 20:54
@danielkov
danielkovforce-pushed the feat/provider-resiliency branch from eba8025 to 99dcb62CompareAugust 30, 2026 00:35
@danielkov
danielkov changed the base branch from feat/acp-v2-session-inject to mainAugust 30, 2026 00:35
@danielkov
danielkov merged commit 68e29cc into mainAug 30, 2026
3 checks passed
Sign up for freeto 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

@danielkov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
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

@danielkovdanielkov 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)]structRotatingTokens;#[async_trait]implAuthenticationProviderforRotatingTokens{asyncfnauthenticate(&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"),};letmut 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:

ProvidersAuthentication behavior
OpenAI, OpenRouter, Groq, Mistral, Baseten, CerebrasBearer by default
Anthropicx-api-key via new, bearer via with_auth_token
Ollama, vLLMOptional; 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-keylet 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();fnretain_latest_attempt(events:implIntoIterator<Item = ModelTurnEvent>,) -> Vec<ModelTurnEvent>{letmut 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-injectAugust 29, 2026 20:54
@danielkov
danielkovforce-pushed the feat/provider-resiliency branch from eba8025 to 99dcb62CompareAugust 30, 2026 00:35
@danielkov
danielkov changed the base branch from feat/acp-v2-session-inject to mainAugust 30, 2026 00:35
@danielkov
danielkov merged commit 68e29cc into mainAug 30, 2026
3 checks passed
Sign up for freeto 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

@danielkov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@danielkovdanielkov 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)]structRotatingTokens;#[async_trait]implAuthenticationProviderforRotatingTokens{asyncfnauthenticate(&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"),};letmut 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:

ProvidersAuthentication behavior
OpenAI, OpenRouter, Groq, Mistral, Baseten, CerebrasBearer by default
Anthropicx-api-key via new, bearer via with_auth_token
Ollama, vLLMOptional; 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-keylet 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();fnretain_latest_attempt(events:implIntoIterator<Item = ModelTurnEvent>,) -> Vec<ModelTurnEvent>{letmut 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-injectAugust 29, 2026 20:54
@danielkov
danielkovforce-pushed the feat/provider-resiliency branch from eba8025 to 99dcb62CompareAugust 30, 2026 00:35
@danielkov
danielkov changed the base branch from feat/acp-v2-session-inject to mainAugust 30, 2026 00:35
@danielkov
danielkov merged commit 68e29cc into mainAug 30, 2026
3 checks passed
Sign up for freeto 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

@danielkov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@danielkovdanielkov 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)]structRotatingTokens;#[async_trait]implAuthenticationProviderforRotatingTokens{asyncfnauthenticate(&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"),};letmut 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:

ProvidersAuthentication behavior
OpenAI, OpenRouter, Groq, Mistral, Baseten, CerebrasBearer by default
Anthropicx-api-key via new, bearer via with_auth_token
Ollama, vLLMOptional; 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-keylet 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();fnretain_latest_attempt(events:implIntoIterator<Item = ModelTurnEvent>,) -> Vec<ModelTurnEvent>{letmut 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-injectAugust 29, 2026 20:54
@danielkov
danielkovforce-pushed the feat/provider-resiliency branch from eba8025 to 99dcb62CompareAugust 30, 2026 00:35
@danielkov
danielkov changed the base branch from feat/acp-v2-session-inject to mainAugust 30, 2026 00:35
@danielkov
danielkov merged commit 68e29cc into mainAug 30, 2026
3 checks passed
Sign up for freeto 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

@danielkov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

@danielkovdanielkov 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)]structRotatingTokens;#[async_trait]implAuthenticationProviderforRotatingTokens{asyncfnauthenticate(&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"),};letmut 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:

ProvidersAuthentication behavior
OpenAI, OpenRouter, Groq, Mistral, Baseten, CerebrasBearer by default
Anthropicx-api-key via new, bearer via with_auth_token
Ollama, vLLMOptional; 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-keylet 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();fnretain_latest_attempt(events:implIntoIterator<Item = ModelTurnEvent>,) -> Vec<ModelTurnEvent>{letmut 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-injectAugust 29, 2026 20:54
@danielkov
danielkovforce-pushed the feat/provider-resiliency branch from eba8025 to 99dcb62CompareAugust 30, 2026 00:35
@danielkov
danielkov changed the base branch from feat/acp-v2-session-inject to mainAugust 30, 2026 00:35
@danielkov
danielkov merged commit 68e29cc into mainAug 30, 2026
3 checks passed
Sign up for freeto 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

@danielkov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@danielkovdanielkov 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)]structRotatingTokens;#[async_trait]implAuthenticationProviderforRotatingTokens{asyncfnauthenticate(&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"),};letmut 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:

ProvidersAuthentication behavior
OpenAI, OpenRouter, Groq, Mistral, Baseten, CerebrasBearer by default
Anthropicx-api-key via new, bearer via with_auth_token
Ollama, vLLMOptional; 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-keylet 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();fnretain_latest_attempt(events:implIntoIterator<Item = ModelTurnEvent>,) -> Vec<ModelTurnEvent>{letmut 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-injectAugust 29, 2026 20:54
@danielkov
danielkovforce-pushed the feat/provider-resiliency branch from eba8025 to 99dcb62CompareAugust 30, 2026 00:35
@danielkov
danielkov changed the base branch from feat/acp-v2-session-inject to mainAugust 30, 2026 00:35
@danielkov
danielkov merged commit 68e29cc into mainAug 30, 2026
3 checks passed
Sign up for freeto 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

@danielkov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@danielkovdanielkov 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)]structRotatingTokens;#[async_trait]implAuthenticationProviderforRotatingTokens{asyncfnauthenticate(&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"),};letmut 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:

ProvidersAuthentication behavior
OpenAI, OpenRouter, Groq, Mistral, Baseten, CerebrasBearer by default
Anthropicx-api-key via new, bearer via with_auth_token
Ollama, vLLMOptional; 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-keylet 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();fnretain_latest_attempt(events:implIntoIterator<Item = ModelTurnEvent>,) -> Vec<ModelTurnEvent>{letmut 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-injectAugust 29, 2026 20:54
@danielkov
danielkovforce-pushed the feat/provider-resiliency branch from eba8025 to 99dcb62CompareAugust 30, 2026 00:35
@danielkov
danielkov changed the base branch from feat/acp-v2-session-inject to mainAugust 30, 2026 00:35
@danielkov
danielkov merged commit 68e29cc into mainAug 30, 2026
3 checks passed
Sign up for freeto 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

@danielkov
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

@danielkovdanielkov 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)]structRotatingTokens;#[async_trait]implAuthenticationProviderforRotatingTokens{asyncfnauthenticate(&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"),};letmut 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:

ProvidersAuthentication behavior
OpenAI, OpenRouter, Groq, Mistral, Baseten, CerebrasBearer by default
Anthropicx-api-key via new, bearer via with_auth_token
Ollama, vLLMOptional; 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-keylet 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();fnretain_latest_attempt(events:implIntoIterator<Item = ModelTurnEvent>,) -> Vec<ModelTurnEvent>{letmut 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-injectAugust 29, 2026 20:54
@danielkov
danielkovforce-pushed the feat/provider-resiliency branch from eba8025 to 99dcb62CompareAugust 30, 2026 00:35
@danielkov
danielkov changed the base branch from feat/acp-v2-session-inject to mainAugust 30, 2026 00:35
@danielkov
danielkov merged commit 68e29cc into mainAug 30, 2026
3 checks passed
Sign up for freeto 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

@danielkov