fprovider 1.0.0

Provider library for the fiddlesticks agent harness framework
Documentation
# Model Provider API


`fprovider` defines the **model provider abstraction** for Fiddlesticks.

Its job is simple:
> Provide a clean, provider-agnostic way to talk to language models.

Everything else in the system (chat, agents, tools) depends on *this* layer instead of directly coupling to OpenAI, Anthropic, or anything else.

---

## What lives here


- Core **provider traits**
- Provider-agnostic request / response types
- Streaming abstractions (tokens, tool calls, events)
- Provider-specific adapters (behind features)

This crate does **not**:
- Define agent logic
- Define conversation state machines
- Execute tools
- Manage memory or persistence

Those concerns live higher up the stack.

---

## Supported Providers


The currently supported providers are:

- **OpenCode Zen**
- **OpenAI**
- **Anthropic**

Each provider implements the same core traits so they can be swapped without changing agent or chat logic.

---

## Design Goals


- **Minimal surface area** – only what every provider must support
- **Async-first** – providers are expected to be network-bound
- **Streaming-friendly** – even if some providers start non-streaming
- **Feature-gated implementations** – avoid pulling heavy deps unless needed
- **No provider leakage** – downstream crates should not need provider-specific types

---

## High-Level Flow


```text
fharness / fchat
        |
        v
    fprovider (traits + adapters)
        |
        v
   External model APIs
```

---

## Using `fprovider` from other crates


### 1) Add dependency


Provider-agnostic usage (recommended default):

```toml
[dependencies]
fprovider = { path = "../fprovider" }
```

If your crate needs OpenAI adapter support, enable the feature:

```toml
[dependencies]
fprovider = { path = "../fprovider", features = ["provider-openai"] }
```

### 2) Build requests with provider-agnostic types


```rust
use fprovider::{Message, ModelRequest, Role};

let request = ModelRequest::builder("gpt-4o-mini")
    .message(Message::new(Role::User, "Summarize this file"))
    .temperature(0.2)
    .max_tokens(512)
    .build()?;
```

### 3) Depend on traits, not SDK types


Higher crates should accept `dyn ModelProvider` so provider choice is runtime-configurable:

```rust
use std::sync::Arc;
use fprovider::{ModelProvider, ModelRequest, ProviderError};

pub async fn run_once(
    provider: Arc<dyn ModelProvider>,
    request: ModelRequest,
) -> Result<(), ProviderError> {
    let response = provider.complete(request).await?;
    let _ = response;
    Ok(())
}
```

### 4) Register and resolve providers


```rust
use fprovider::{ProviderId, ProviderRegistry};

let mut registry = ProviderRegistry::new();
// registry.register(openai_provider);

let provider = registry
    .get(ProviderId::OpenAi)
    .expect("OpenAI provider is not registered");
```

### 5) OpenAI adapter example


```rust
use std::sync::Arc;
use reqwest::Client;
use fprovider::{ProviderRegistry, SecureCredentialManager};
use fprovider::adapters::openai::{OpenAiHttpTransport, OpenAiProvider};

let credentials = Arc::new(SecureCredentialManager::new());
credentials.set_openai_api_key("sk-...")?;

let transport = Arc::new(OpenAiHttpTransport::new(Client::new()));
let openai = OpenAiProvider::new(credentials, transport);

let mut registry = ProviderRegistry::new();
registry.register(openai);
```

### 6) Streaming consumption


`stream(...)` returns a stream implementing `futures_core::Stream<Item = Result<StreamEvent, ProviderError>>`.
This is provider-agnostic and works with standard async ecosystem helpers.

Stream invariants:

- Events are emitted in provider/source order.
- Delta events (`TextDelta`, `ToolCallDelta`) can appear zero or more times.
- Completion milestones (`MessageComplete`, `ResponseComplete`) when present arrive after deltas.
- Once the stream returns `None`, no additional events are emitted.

```rust
use futures_util::StreamExt;
use fprovider::prelude::*;

let mut events = provider.stream(request).await?;
while let Some(event) = events.next().await {
    match event? {
        StreamEvent::TextDelta(delta) => {
            let _ = delta;
        }
        StreamEvent::ToolCallDelta(_) => {}
        StreamEvent::MessageComplete(_) => {}
        StreamEvent::ResponseComplete(_) => {}
    }
}
```

### 7) OpenAI auth policy

When `provider-openai` is enabled, `OpenAiProvider` only uses API key credentials configured via `SecureCredentialManager::set_openai_api_key`.

### 8) Credential lifecycle and auditing

`SecureCredentialManager` now supports lifecycle metadata and access auditing hooks:

- `set_api_key_with_ttl(...)` to expire API keys after a fixed TTL
- `rotate_api_key(...)` and `revoke(...)` for explicit key rotation/revocation
- `credential_metadata(...)` for sanitized metadata (`created_at`, `expires_at`, `last_used_at`, access counters)
- `with_observer(...)` for audit events that include provider/kind/action and never include secret values

### 9) Standard retry/backoff and operational hooks

`fprovider` exposes provider-agnostic resilience primitives:

- `RetryPolicy`: standardized retry attempt limits and exponential backoff settings
- `ProviderOperationHooks`: lifecycle hooks for attempts, retries, success, and failure
- `execute_with_retry(...)`: helper that applies policy + hooks around async operations

Example:

```rust
use std::time::Duration;
use fprovider::prelude::*;

let policy = RetryPolicy {
    max_attempts: 4,
    initial_backoff: Duration::from_millis(100),
    max_backoff: Duration::from_secs(2),
    backoff_multiplier: 2.0,
};

let hooks = NoopOperationHooks;

let value = execute_with_retry(
    ProviderId::OpenAi,
    "complete",
    &policy,
    &hooks,
    |_attempt| async { Ok::<_, ProviderError>("ok") },
    |_delay| async {},
)
.await?;

let _ = value;
```

---

## Feature flags


- `provider-openai`: OpenAI adapter and HTTP transport
- `provider-anthropic`: Anthropic adapter over OpenAI-compatible transport
- `provider-opencode-zen`: OpenCode Zen adapter over OpenAI-compatible transport