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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions PR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Custom Providers (YAML-based provider configuration)

## Summary

Add `reloaded-code-provider-config` crate for defining custom LLM providers via YAML files - no Rust code required. Also fixes OpenAI-compatible providers to work without credentials when no env vars are listed (e.g., local Ollama).

## Changes

### New crate: `reloaded-code-provider-config`

- **loader.rs** - `ProviderConfigLoader` collects YAML files and programmatic entries, merges them (later source wins), validates, and produces catalog sources
- **config.rs** - Serde shapes for `ProviderConfig` and `ModelConfig`
- **api_type.rs** - Maps `api_type` strings to `ProviderType` variants
- **error.rs** - Typed errors for validation and I/O failures

Conventional config paths (opt-in via `with_default_paths()`):
- `~/.config/reloaded-code/providers.yaml` (user-global)
- `.reloaded/providers.yaml` (project-local)

### Core changes

- **`Modality::from_label()`** - Parses `"text"`, `"image"`, `"audio"`, `"video"` into `Modality` bitflags
- **Provider bridge fix** - OpenAI-compatible providers without credential env vars now work with empty API key

### Documentation

- New guide: `docs/src/guides/custom-providers.md`
- Updated nav, index, models-catalog, and examples pages

## YAML schema

```yaml
my-llm:
api_url: https://api.myllm.com/v1
api_type: openai-compatible # optional, defaults to "openai-compatible"
env: # optional, credential env var names
- MY_LLM_API_KEY
models:
my-model:
max_input: 128000 # required
max_output: 8192 # required
modalities: [text, image] # optional, defaults to [text]
default_temperature: 0.7 # optional
default_top_p: 0.95 # optional
```

Supported `api_type`: `openai`, `openai-compatible`, `openai-responses`, `anthropic`, `google`, `groq`, `mistral`, `ollama`, `bedrock`, `azure`, `openrouter`, `huggingface`, `cohere`.

## Test coverage

- Config deserialization (full, minimal, multi-provider)
- Loader (single file, empty, override semantics, programmatic entries)
- Validation (missing fields, unrecognized api_type/modality, malformed YAML)
- Catalog conversion (ProviderType mapping, ProviderIdx consistency)
- Provider bridge (keyless endpoints succeed, credential-required still enforced)


4 changes: 3 additions & 1 deletion docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ nav:
- vs. OpenCode:
- Comparison: comparison.md
- Migration: migration.md
- Custom Framework Integration: guides/custom-framework.md
- Guides:
- Custom Framework Integration: guides/custom-framework.md
- Custom Providers: guides/custom-providers.md
- Extra:
- Extra Sandboxing Notes: extra-sandboxing-notes.md
16 changes: 12 additions & 4 deletions docs/src/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Runnable examples live in the repository under each crate's `examples/` director

## SerdesAI Integration

| Example | Description | Run |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| Example | Description | Run |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| [serdesai-basic] | Minimal agent with file tools, shell execution, web fetch, and streaming output. | `cargo run --example serdesai-basic -p reloaded-code-serdesai` |
| [serdesai-agents] | Load markdown agents through `AgentLoader`, build a named agent via `AgentBuildContext` using the models.dev catalog. | `cargo run --example serdesai-agents -p reloaded-code-serdesai` |
| [serdesai-task] | Orchestrator delegates a read-only task to a reader sub-agent, with streamed transcript and tool-call logging. | `cargo run --example serdesai-task -p reloaded-code-serdesai` |
Expand All @@ -20,12 +20,20 @@ Runnable examples live in the repository under each crate's `examples/` director

## Core Library

| Example | Description | Run |
| -------------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Example | Description | Run |
| -------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| [system_prompt_preview] | Full system prompt with all tools enabled, prints static token cost breakdown. | `cargo run --example system_prompt_preview -p reloaded-code-core` |
| [system_prompt_preview_readonly] | Smaller read-only system prompt - minimal tool set, lower token cost. | `cargo run --example system_prompt_preview_readonly -p reloaded-code-core` |
| [system_prompt_preview_compare] | Compares full vs read-only prompt footprints, prints character and token savings. | `cargo run --example system_prompt_preview_compare -p reloaded-code-core` |

[system_prompt_preview]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-core/examples/system_prompt_preview.rs
[system_prompt_preview_readonly]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-core/examples/system_prompt_preview_readonly.rs
[system_prompt_preview_compare]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-core/examples/system_prompt_preview_compare.rs

## Provider Config

| Example | Description | Run |
| --------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| [config-loader] | Load custom provider YAML files and programmatic entries via `ProviderConfigLoader`. | `cargo run --example config-loader -p reloaded-code-provider-config` |

[config-loader]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-provider-config/examples/config-loader.rs
179 changes: 179 additions & 0 deletions docs/src/guides/custom-providers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Custom Providers

Define new LLM providers via YAML configuration files - no Rust code required.

## Conventional config paths

!!! note "Opt-in defaults"
These paths are conventions, not hard-coded lookup locations.
The library never reads config files unless the application
explicitly asks it to.

- [`ProviderConfigLoader::with_default_paths()`][with_default_paths]
resolves and loads both paths in order (user-global, then
project-local).
- [`ProviderConfigLoader::add_path()`][add_path] loads a single
custom path.

Use [`default_config_paths()`][default_config_paths] if you only
need the path list without loading.

When using the defaults, later files override earlier ones per provider key:

| Path | Scope |
| ---------------------------------------- | ------------- |
| `~/.config/reloaded-code/providers.yaml` | User-global |
| `.reloaded/providers.yaml` | Project-local |

When using the defaults, you can have zero, one, or both files present.
If neither exists, only the models.dev catalog is used (no custom providers).

[`default_config_paths()`][default_config_paths] is also available to resolve the conventional
paths without loading them.

## YAML format

```yaml
my-llm:
api_url: https://api.myllm.com/v1
api_type: openai-compatible
env:
- MY_LLM_API_KEY
models:
MiniMax-M2.7:
max_input: 204800
max_output: 131072
modalities: [text]
```

Each provider must include at least one model under `models`.

### Provider fields

| Field | Type | Default | Notes |
| ------------ | ----------- | ------------------- | ------------------------------------- |
| `api_url` | string | required | Base URL for the API endpoint |
| `api_type` | string | `openai-compatible` | Maps to provider behaviour profile |
| `env` | string list | `[]` | Env var names checked for credentials |
| `models` | map | required | Models offered by this provider |

### api_type values

| Value | Provider type |
| ------------------- | -------------------------------------------- |
| `openai` | OpenAI (chat completions) |
| `openai-compatible` | Any OpenAI-API-compatible endpoint (default) |
| `openai-responses` | OpenAI Responses API |
| `anthropic` | Anthropic |
| `google` | Google/Gemini |
| `groq` | Groq |
| `mistral` | Mistral |
| `ollama` | Ollama |
| `bedrock` | AWS Bedrock |
| `azure` | Azure |
| `openrouter` | OpenRouter |
| `huggingface` | Hugging Face |
| `cohere` | Cohere |

Omit `api_type` to default to `openai-compatible`.

`openai` and `openai-compatible` both map to OpenAI chat completions.
`openai` signals actual OpenAI; `openai-compatible` signals any other
OpenAI-API-compatible endpoint.

### Model fields

| Field | Type | Default | Notes |
| --------------------- | ----------- | -------- | ----------------------------------------------- |
| `max_input` | u32 | required | Context window / input limit |
| `max_output` | u32 | required | Output token limit |
| `modalities` | string list | `[text]` | Supported modalities: text, image, audio, video |
| `default_temperature` | f32 | - | Default sampling temperature |
| `default_top_p` | f32 | - | Default nucleus sampling |

## Credentials

Custom providers use the existing `CredentialResolver` - no separate
resolution path needed.

The `env` field lists environment variable names to check, in order.
At runtime, `CredentialResolver` checks its overrides first, then falls
back to those env vars.

```yaml
my-llm:
api_url: https://api.myllm.com/v1
env:
- MY_LLM_API_KEY
- MY_LLM_TOKEN # fallback
```

For providers with no `env` entry (e.g., local endpoints like Ollama
behind a compatibility layer), no API key is required.

## Rust API

```rust
use reloaded_code_provider_config::{ModelConfig, ProviderConfig, ProviderConfigLoader};

fn main() -> Result<(), Box<dyn std::error::Error>> {
// Option A: Use conventional paths (user-global then project-local).
let mut loader = ProviderConfigLoader::with_default_paths()?;

// Option B: Full control - choose paths yourself.
// let mut loader = ProviderConfigLoader::new();
// loader.add_path(".reloaded/providers.yaml")?;

// Add a programmatic entry (loaded last, overrides any file entry with the same key).
loader.add_provider("my-llm", ProviderConfig {
api_url: Some("https://api.myllm.com/v1".into()),
api_type: Some("openai-compatible".into()),
env: Some(vec!["MY_LLM_API_KEY".into()]),
models: Some({
let mut m = indexmap::IndexMap::new();
m.insert("my-model".to_string(), ModelConfig {
max_input: 128000,
max_output: 8192,
modalities: vec!["text".to_string()],
default_temperature: None,
default_top_p: None,
});
m
}),
});

let loaded = loader.load()?;
let (providers, models) = loaded.to_catalog_sources();

// Pass to ModelCatalog::build() alongside models.dev sources.
// let catalog = ModelCatalog::build(&providers, &models)?;
Ok(())
}
```

## Merge Behaviour

When multiple config sources define the same provider key, the **later**
source completely replaces the earlier entry. There is no deep merge of
model maps - the entire provider entry is replaced.

```yaml
# File 1: ~/.config/reloaded-code/providers.yaml
my-llm:
api_url: https://api.myllm.com/v1
models:
v1: { max_input: 128000, max_output: 8192 }

# File 2: .reloaded/providers.yaml (loaded later, wins)
my-llm:
api_url: https://api.myllm.com/v2
models:
v2: { max_input: 256000, max_output: 16384 }
```

Result: only `v2` model exists under `my-llm` - the `v1` model from
file 1 is fully replaced.

[with_default_paths]: https://docs.rs/reloaded-code-provider-config/latest/reloaded_code_provider_config/struct.ProviderConfigLoader.html#method.with_default_paths
[add_path]: https://docs.rs/reloaded-code-provider-config/latest/reloaded_code_provider_config/struct.ProviderConfigLoader.html#method.add_path
[default_config_paths]: https://docs.rs/reloaded-code-provider-config/latest/reloaded_code_provider_config/fn.default_config_paths.html
4 changes: 4 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ dependency setup and an alternate path without agent files.
<h3><a href="https://crates.io/crates/reloaded-code-serdesai">serdesai</a></h3>
<p>Ready-to-use <a href="https://crates.io/crates/serdes-ai">SerdesAI</a> (LLM serialization framework) integration. 15 LLM provider adapters, multi-agent task delegation with recursion depth limits.</p>
</div>
<div class="crate-card">
<h3><a href="https://crates.io/crates/reloaded-code-provider-config">provider-config</a></h3>
<p>YAML-based custom provider definitions. Add providers without writing Rust code, merge multiple config sources, convert to catalog types.</p>
</div>
<div class="crate-card">
<h3><a href="https://crates.io/crates/reloaded-code-bubblewrap">bubblewrap</a></h3>
<p>Sandbox shell execution on Linux. Network-isolated, filesystem-filtered profiles for untrusted input. Two presets included.</p>
Expand Down
6 changes: 6 additions & 0 deletions docs/src/models-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ hash-table format (~30 KiB in memory).

Exactly one must be enabled.

## Custom providers

You can define additional providers via YAML configuration files that extend
the models.dev catalog. See [Custom Providers](guides/custom-providers.md) for
the full schema and API reference.

[models.dev]: https://models.dev
[tokio]: https://tokio.rs
[zstd]: https://facebook.github.io/zstd/
15 changes: 15 additions & 0 deletions src/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"reloaded-code-agents",
"reloaded-code-models-dev",
"reloaded-code-bubblewrap",
"reloaded-code-provider-config",
]

[workspace.dependencies]
Expand Down Expand Up @@ -79,6 +80,7 @@ reloaded-code-core = { version = "0.2.0", path = "reloaded-code-core", default-f
reloaded-code-bubblewrap = { version = "0.1.0", path = "reloaded-code-bubblewrap" }
reloaded-code-agents = { version = "0.1.0", path = "reloaded-code-agents" }
reloaded-code-models-dev = { version = "0.1.0", path = "reloaded-code-models-dev" }
reloaded-code-provider-config = { version = "0.1.0", path = "reloaded-code-provider-config" }

# Dev dependencies
criterion = "0.8"
Expand Down
39 changes: 39 additions & 0 deletions src/reloaded-code-core/src/models/catalog/public/modality.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,48 @@ bitflags! {
}
}

impl Modality {
/// Parse a combined modality label.
///
/// Recognized: `"text"`, `"image"`, `"audio"`, `"video"`.
/// Returns `None` for unrecognized strings.
///
/// When adding a new combined flag to the `bitflags!` block above,
/// add the corresponding label here and to the test below.
pub fn from_label(label: &str) -> Option<Self> {
match label {
"text" => Some(Self::TEXT),
"image" => Some(Self::IMAGE),
"audio" => Some(Self::AUDIO),
"video" => Some(Self::VIDEO),
_ => None,
}
}
}

impl Default for Modality {
#[inline]
fn default() -> Self {
Self::TEXT
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn from_label_covers_all_combined_flags() {
// When adding a new combined flag, add its (label, flag) pair here.
const PAIRS: &[(&str, Modality)] = &[
("text", Modality::TEXT),
("image", Modality::IMAGE),
("audio", Modality::AUDIO),
("video", Modality::VIDEO),
];
for (label, expected) in PAIRS {
assert_eq!(Modality::from_label(label), Some(*expected));
}
assert_eq!(Modality::from_label("smell"), None);
}
}
Loading
Loading