Skip to content

Commit 7941de1

Browse files
committed
Fixes: improve documentation consistency, add API key redaction in Debug output, and enhance robustness of models-dev-update binary
1 parent afac040 commit 7941de1

10 files changed

Lines changed: 517 additions & 48 deletions

File tree

src/llm-coding-tools-agents/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ permission:
4444
Prompt body goes here...
4545
```
4646

47+
**Note**: Provider selection is driven by the `provider:` prefix, not by URL inspection. OpenAI-compatible endpoints should still use `openai:` with a custom base URL provided via provider overrides.
48+
49+
4750
### Mode Options
4851

4952
The `mode` field controls how the agent can be invoked:
@@ -72,7 +75,7 @@ See `examples/serdesai-agents.rs` for the complete example.
7275

7376
```rust,no_run
7477
use llm_coding_tools_agents::{AgentCatalog, AgentLoader, Ruleset, Rule, PermissionAction};
75-
use llm_coding_tools_serdesai::{AgentDefaults, AgentRegistryBuilder, TaskTool, default_tools, TodoState};
78+
use llm_coding_tools_serdesai::{AgentDefaults, AgentRegistryBuilder, ProviderOverrides, TaskTool, default_tools, TodoState};
7679
use std::sync::Arc;
7780
7881
// 1) Load agent configs
@@ -82,6 +85,8 @@ AgentLoader::new().add_directory(&mut catalog, "/home/user/.opencode")?;
8285
// 2) Build framework registry
8386
let defaults = AgentDefaults {
8487
model: "openai:hf:zai-org/GLM-4.7".into(),
88+
model_resolver: None,
89+
provider_overrides: ProviderOverrides::new(),
8590
api_key: Some(std::env::var("OPENAI_API_KEY").unwrap_or_default()),
8691
base_url: Some("https://api.synthetic.new/openai/v1".into()),
8792
temperature: None,

src/llm-coding-tools-agents/benches/fixtures/orchestrator-quality-gate-gpt5.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ These are areas where the implementer was uncertain — validate the approach or
7373
## 7) Review tests
7474
- Tests: basic → ensure basic tests exist for new functionality and run tests
7575
- Tests: no → do not run tests; flag any found tests as overengineering
76-
- Check whole test files, not just diffs
76+
- Check the entire content of changed test files, not just the modified portions
7777
- WARNING IF [MEDIUM]: newly added tests duplicate existing test coverage without adding value (different context, edge case, or scenario)
7878
- WARNING IF [MEDIUM]: tests have significant duplication that would benefit from parameterization without sacrificing readability
7979
- FAIL IF: tests are non-deterministic (real I/O, time, network without mocking/seeding)

src/llm-coding-tools-core/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This crate provides the foundational building blocks for coding tool implementat
1515
Task tools (for agent-to-agent delegation) are implemented as registry-driven tools in the framework-specific crates:
1616
- SerdesAI: See `llm-coding-tools-serdesai::TaskTool` (README for setup example)
1717

18-
The serdesAI framework uses a unified flow: load agent configs into `AgentCatalog`, build a framework-specific registry, then construct a `TaskTool` with the registry and permission rules.
18+
The SerdesAI framework uses a unified flow: load agent configs into `AgentCatalog`, build a framework-specific registry, then construct a `TaskTool` with the registry and permission rules.
1919

2020
## Features
2121

src/llm-coding-tools-models-dev/src/bin/models-dev-update.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,11 @@ struct ProviderSnapshot {
5656
async fn main() -> Result<(), Box<dyn std::error::Error>> {
5757
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
5858
let output = manifest_dir.join("data/models.dev.min.json");
59+
if let Some(parent) = output.parent() {
60+
fs::create_dir_all(parent).await?;
61+
}
5962

60-
let client = Client::builder()
61-
.timeout(Duration::from_secs(30))
62-
.build()?;
63+
let client = Client::builder().timeout(Duration::from_secs(30)).build()?;
6364
let response = client
6465
.get("https://models.dev/api.json")
6566
.send()

src/llm-coding-tools-serdesai/Cargo.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,16 @@ llm-coding-tools-models-dev = { version = "0.1.0", path = "../llm-coding-tools-m
2222

2323
# serdes-ai provides Tool trait, ToolDefinition, RunContext
2424
serdes-ai = "0.1"
25-
serdes-ai-models = { version = "0.1", features = ["openai"] }
25+
serdes-ai-models = { version = "0.1", features = [
26+
"openai",
27+
"anthropic",
28+
"groq",
29+
"mistral",
30+
"google",
31+
"cohere",
32+
"openrouter",
33+
"huggingface",
34+
] }
2635
serdes-ai-streaming = "0.1"
2736
futures = "0.3"
2837

src/llm-coding-tools-serdesai/README.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,53 @@ Setup requires three steps:
9898

9999
The example file shows the complete setup.
100100

101-
**Note**: The `default_tools` function returns cloneable `ToolCatalogEntry` items that can be reused for building multiple agents. The `AgentRegistryBuilder` uses these to construct tool descriptions and filter based on agent permissions. The `deps` parameter is passed to registry agents at invocation time.
101+
**Note**: The `default_tools` function (defined in `examples/serdesai-agents.rs`) returns cloneable `ToolCatalogEntry` items that can be reused for building multiple agents. The `AgentRegistryBuilder` uses these to construct tool descriptions and filter based on agent permissions. The `deps` parameter is passed to registry agents at invocation time.
102102

103103
Other tools: `BashTool`, `WebFetchTool`, `TodoReadTool`, `TodoWriteTool`.
104104
Use `SystemPromptBuilder` to track tools and pass `pb.build()` to `.system_prompt()`. Set `working_directory()` so the environment section is populated.
105105
Use `AgentBuilderExt::tool()` to add tools that implement `Tool<Deps>` to the agent.
106106
Context strings are re-exported in `llm_coding_tools_serdesai::context` (e.g., `BASH`, `READ_ABSOLUTE`).
107107

108+
### models.dev Resolver
109+
110+
Use the models.dev catalog to resolve per-provider API keys/base URLs:
111+
112+
```rust,no_run
113+
# use std::env;
114+
# use llm_coding_tools_models_dev::ModelsDevCatalog;
115+
# use llm_coding_tools_serdesai::{AgentDefaults, ModelsDevResolver, ProviderOverride, ProviderOverrides};
116+
# fn main() -> Result<(), Box<dyn std::error::Error>> {
117+
let catalog = ModelsDevCatalog::load_shared_cache_or_bundled()?.catalog;
118+
let overrides = ProviderOverrides::new().insert_override(
119+
"openai",
120+
ProviderOverride { api_key: Some(env::var("OPENAI_API_KEY")?), base_url: None, endpoint_env: None },
121+
);
122+
let resolver = ModelsDevResolver::new(Some(catalog), overrides.clone());
123+
124+
let defaults = AgentDefaults {
125+
model: "openai:gpt-4o".into(),
126+
model_resolver: Some(resolver),
127+
provider_overrides: overrides,
128+
api_key: None,
129+
base_url: None,
130+
temperature: None,
131+
top_p: None,
132+
options: Default::default(),
133+
};
134+
# Ok(())
135+
# }
136+
```
137+
138+
**OpenAI-compatible providers**: serdesAI does not infer providers from base URLs. Use an `openai:` model spec and set a provider-specific `base_url` via overrides.
139+
140+
**Reasoning models**: If you need `OpenAIResponsesModel` for `o1`, `o3`, or `gpt-5`, construct it directly instead of using `ModelConfig`.
141+
142+
**OpenRouter/HuggingFace**: `build_model_with_config` does not support these providers; use `OpenRouterModel::new` or `HuggingFaceModel::new` directly.
143+
OpenRouter does not support base URL overrides; resolver should not surface `base_url` for this provider.
144+
145+
**Resolver fallback behavior**: When no resolver is provided, the registry attempts to load the models.dev catalog from the shared cache or bundled snapshot. If that fails, it falls back to an empty catalog (meaning only explicit specs are usable and no provider mapping occurs).
146+
147+
108148
### Migration from Legacy Task APIs
109149

110150
The previous task setup using `TaskToolCore` and `SubagentRegistry` has been replaced with the registry-driven flow. Key changes:

src/llm-coding-tools-serdesai/examples/serdesai-agents.rs

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@
1313
1414
use futures::StreamExt;
1515
use llm_coding_tools_agents::{AgentCatalog, AgentLoader, PermissionAction, Rule, Ruleset};
16+
use llm_coding_tools_models_dev::ModelsDevCatalog;
1617
use llm_coding_tools_serdesai::agent_ext::AgentBuilderExt;
1718
use llm_coding_tools_serdesai::{
18-
AgentDefaults, AgentRegistryBuilder, AllowedPathResolver, SystemPromptBuilder, TaskTool,
19-
TodoState, default_tools,
19+
AgentDefaults, AgentRegistryBuilder, AllowedPathResolver, ModelResolver, ModelsDevResolver,
20+
ProviderOverride, ProviderOverrides, SystemPromptBuilder, TaskTool, TodoState, default_tools,
2021
};
2122
use serdes_ai::agent::ModelConfig;
2223
use serdes_ai::prelude::*;
24+
use serdes_ai_models::huggingface::HuggingFaceModel;
25+
use serdes_ai_models::openrouter::OpenRouterModel;
2326
use std::fmt::Write;
2427
use std::sync::Arc;
2528

@@ -48,7 +51,7 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
4851
// Set OPENCODE_USE_ALLOWED environment variable to enable sandboxed (allowed) tools.
4952
// Without the env var, tools use absolute paths with no restrictions.
5053
let use_allowed = std::env::var("OPENCODE_USE_ALLOWED").is_ok();
51-
let resolver = if use_allowed {
54+
let allowed_path_resolver = if use_allowed {
5255
Some(AllowedPathResolver::new([
5356
std::env::current_dir()?,
5457
std::env::temp_dir(),
@@ -62,26 +65,41 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
6265
// Use default_tools to create a catalog of cloneable tools.
6366
// When use_allowed is true, tools are sandboxed to allowed directories.
6467
// When false, tools can access any path.
65-
let tools = default_tools(true, resolver.clone(), TodoState::new());
68+
let tools = default_tools(true, allowed_path_resolver.clone(), TodoState::new());
69+
70+
// === Load models.dev catalog and build model resolver ===
71+
//
72+
let models_dev_catalog = ModelsDevCatalog::load_shared_cache_or_bundled()?.catalog;
73+
let provider_overrides = ProviderOverrides::new().insert_override(
74+
"openai",
75+
ProviderOverride {
76+
api_key: Some(get_openai_api_key()),
77+
base_url: Some(OPENAI_BASE_URL.to_string()),
78+
endpoint_env: None,
79+
},
80+
);
81+
let model_resolver =
82+
ModelsDevResolver::new(Some(models_dev_catalog), provider_overrides.clone());
6683

6784
// === Build registry ===
6885
//
6986
// AgentDefaults specifies the default model and sampling parameters
7087
// for agents that don't override them in their config.
7188
let defaults = AgentDefaults {
7289
model: OPENAI_MODEL.to_string(),
73-
api_key: Some(get_openai_api_key()),
74-
base_url: Some(OPENAI_BASE_URL.to_string()),
90+
model_resolver: Some(model_resolver.clone()),
91+
provider_overrides,
92+
api_key: None,
93+
base_url: None,
7594
temperature: None,
7695
top_p: None,
7796
options: Default::default(),
7897
};
7998

80-
// Build the registry from the catalog and tool catalog.
99+
// Build the registry from the agent catalog and tool catalog.
81100
// The registry prebuilds all agents with their allowed tools from the catalog.
82101
//
83-
// Note: For OpenAI models with "openai:" prefix, AgentBuilder::from_model
84-
// will resolve the model using environment variables like OPENAI_API_KEY.
102+
// Note: The model resolver is used to resolve model specs into per-provider settings.
85103
let registry = AgentRegistryBuilder::<()>::new(defaults, tools).build(&catalog)?;
86104

87105
// === Task tool permissions (allow Task for the single subagent only) ===
@@ -98,21 +116,62 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
98116
// Build a system prompt that includes working directory and optionally allowed paths.
99117
let mut pb = SystemPromptBuilder::new()
100118
.working_directory(std::env::current_dir()?.display().to_string());
101-
if let Some(ref resolver) = resolver {
119+
if let Some(ref resolver) = allowed_path_resolver {
102120
pb = pb.allowed_paths(resolver);
103121
}
104122

105123
// Create the primary agent with ONLY the Task tool (forces delegation to subagent).
106124
//
107-
// Note: For OpenAI models with "openai:" prefix, use ModelConfig to set custom base URL.
108-
let agent = AgentBuilder::<(), String>::from_config(
109-
ModelConfig::new(OPENAI_MODEL)
110-
.with_api_key(get_openai_api_key())
111-
.with_base_url(OPENAI_BASE_URL),
112-
)?
113-
.tool(pb.track(task_tool))
114-
.system_prompt(pb.build())
115-
.build();
125+
// Resolve the primary agent's model spec using the model resolver.
126+
let resolved_primary = model_resolver.resolve(OPENAI_MODEL)?;
127+
let (spec_provider, resolved_model_id) = resolved_primary
128+
.spec
129+
.split_once(':')
130+
.unwrap_or(("", resolved_primary.spec.as_str()));
131+
let resolved_provider = if resolved_primary.provider_id.is_empty() {
132+
spec_provider
133+
} else {
134+
resolved_primary.provider_id.as_str()
135+
};
136+
137+
// Branch on resolved provider to use appropriate constructor (same logic as registry)
138+
let builder = match resolved_provider {
139+
"openrouter" => {
140+
let model = if let Some(api_key) = resolved_primary.api_key.as_deref() {
141+
OpenRouterModel::new(resolved_model_id, api_key)
142+
} else {
143+
OpenRouterModel::from_env(resolved_model_id)?
144+
};
145+
// Note: OpenRouterModel does not support base URL overrides.
146+
AgentBuilder::<(), String>::new(model)
147+
}
148+
"huggingface" => {
149+
let mut model = if let Some(api_key) = resolved_primary.api_key.as_deref() {
150+
HuggingFaceModel::new(resolved_model_id, api_key)
151+
} else {
152+
HuggingFaceModel::from_env(resolved_model_id)?
153+
};
154+
if let Some(endpoint) = resolved_primary.base_url.as_deref() {
155+
model = model.with_endpoint(endpoint);
156+
}
157+
AgentBuilder::<(), String>::new(model)
158+
}
159+
_ => {
160+
let mut model_config = ModelConfig::new(&resolved_primary.spec);
161+
if let Some(api_key) = resolved_primary.api_key.clone() {
162+
model_config = model_config.with_api_key(api_key);
163+
}
164+
if let Some(base_url) = resolved_primary.base_url.clone() {
165+
model_config = model_config.with_base_url(base_url);
166+
}
167+
AgentBuilder::<(), String>::from_config(model_config)?
168+
}
169+
};
170+
171+
let agent = builder
172+
.tool(pb.track(task_tool))
173+
.system_prompt(pb.build())
174+
.build();
116175

117176
// === Print tool info ===
118177
println!("=== Agent Ready ({} tools) ===", agent.tools().len());

0 commit comments

Comments
 (0)