mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
feat(llm): add Poolside Laguna models
Add Poolside as a built-in OpenAI-compatible provider and expose Laguna S 2.1 and XS 2.1 both directly and through OpenRouter. Include vault/env credential registration, secret redaction, live coverage, catalog tests, and user documentation.
This commit is contained in:
parent
04f55666f9
commit
8f47275d84
18 changed files with 500 additions and 2 deletions
|
|
@ -6,6 +6,8 @@ INCEPTION_API_KEY=
|
|||
KIMI_API_KEY=
|
||||
MINIMAX_API_KEY=
|
||||
OPENAI_API_KEY=
|
||||
OPENROUTER_API_KEY=
|
||||
POOLSIDE_API_KEY=
|
||||
ZAI_API_KEY=
|
||||
|
||||
SESSION_SECRET=
|
||||
|
|
|
|||
|
|
@ -375,6 +375,8 @@ Standalone CLI/library usage can still opt into env-backed credential sources ex
|
|||
| `ZAI_API_KEY` | Zai (GLM) |
|
||||
| `MINIMAX_API_KEY` | Minimax |
|
||||
| `INCEPTION_API_KEY` | Inception (Mercury) |
|
||||
| `POOLSIDE_API_KEY` | Poolside (Laguna) |
|
||||
| `OPENROUTER_API_KEY` | OpenRouter (when enabled) |
|
||||
|
||||
### Sandbox and tools
|
||||
|
||||
|
|
|
|||
|
|
@ -34,11 +34,13 @@ No single model is best at everything. Fabro lets you assign the right model to
|
|||
| `gemini-3-flash-preview` | gemini | `gemini-flash` | 1M | $0.50 / $3.00 | 150 tok/s |
|
||||
| `gemini-3.1-flash-lite` | gemini | `gemini-flash-lite`, `gemini-3.1-flash-lite-preview` | 1M | $0.25 / $1.50 | 200 tok/s |
|
||||
| `kimi-k2.5` | kimi | `kimi` | 262K | $0.60 / $3.00 | 50 tok/s |
|
||||
| `laguna-s-2.1` | poolside | `laguna`, `laguna-s` | 1M | $0.10 / $0.20 | n/a |
|
||||
| `laguna-xs-2.1` | poolside | `laguna-xs` | 262K | $0.10 / $0.20 | n/a |
|
||||
| `glm-4.7` | zai | `glm`, `glm4` | 203K | $0.60 / $2.20 | 100 tok/s |
|
||||
| `minimax-m2.5` | minimax | `minimax` | 197K | $0.30 / $1.20 | 45 tok/s |
|
||||
| `mercury-2` | inception | `mercury` | 131K | $0.25 / $0.75 | 1000 tok/s |
|
||||
|
||||
Each provider requires its own API key. Server-backed workflows read provider credentials from the server vault (for example `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GEMINI_API_KEY` set with `fabro secret set` or `fabro provider login`). Standalone SDK/CLI flows can opt into env-backed credential sources explicitly. See the [Quick Start](/getting-started/quick-start) for setup.
|
||||
Each provider requires its own API key. Server-backed workflows read provider credentials from the server vault (for example `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, or `POOLSIDE_API_KEY` set with `fabro secret set` or `fabro provider login`). Standalone SDK/CLI flows can opt into env-backed credential sources explicitly. See the [Quick Start](/getting-started/quick-start) for setup.
|
||||
|
||||
Claude Fable 5 is available as an explicit model but is not the default Anthropic model. If Fable refuses a request, Fabro reports the refusal as a content-filter LLM error and applies the configured `run.model.fallbacks` chain when one is present.
|
||||
|
||||
|
|
@ -135,6 +137,10 @@ Provider `billing_policy` defaults from `adapter` and controls usage-cost estima
|
|||
Provider fields in configuration, APIs, and model routing are provider ID strings. Built-in names like `anthropic`, `openai`, and `gemini` still work, but custom IDs like `proxy` work anywhere a provider ID is accepted.
|
||||
</Note>
|
||||
|
||||
### Poolside
|
||||
|
||||
Fabro ships a built-in [Poolside](/integrations/poolside) provider for Laguna S 2.1 and Laguna XS 2.1 over Poolside's OpenAI-compatible API. Store a direct API key with `fabro provider login --provider poolside`. The same models are also available through the opt-in OpenRouter provider under vendor-namespaced IDs.
|
||||
|
||||
### OpenRouter
|
||||
|
||||
Fabro ships an [OpenRouter](/integrations/openrouter) provider definition with a curated model catalog, disabled by default. Enable it in settings and store an API key with `fabro provider login --provider openrouter`:
|
||||
|
|
@ -175,6 +181,7 @@ When no model or provider is specified, Fabro checks configured provider credent
|
|||
| `openai` | `gpt-5.6-sol` |
|
||||
| `gemini` | `gemini-3.5-flash` |
|
||||
| `kimi` | `kimi-k2.5` |
|
||||
| `poolside` | `laguna-s-2.1` |
|
||||
| `zai` | `glm-4.7` |
|
||||
| `minimax` | `minimax-m2.5` |
|
||||
| `inception` | `mercury` |
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@
|
|||
"integrations/daytona",
|
||||
"integrations/litellm",
|
||||
"integrations/bedrock",
|
||||
"integrations/poolside",
|
||||
"integrations/openrouter",
|
||||
"integrations/slack",
|
||||
"integrations/brave-search"
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ The built-in catalog curates frontier and open-weights models under vendor-names
|
|||
| `google/gemini-3.1-pro-preview`, `google/gemini-3.5-flash` | |
|
||||
| `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash` | |
|
||||
| `moonshotai/kimi-k2.6`, `qwen/qwen3-coder`, `qwen/qwen3.6-flash` | |
|
||||
| `poolside/laguna-s-2.1`, `poolside/laguna-xs-2.1` | Poolside Laguna coding models with native reasoning and tool use |
|
||||
| `z-ai/glm-4.6`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro` | |
|
||||
| `nvidia/nemotron-3-super-120b-a12b`, `mistralai/devstral-2512` | |
|
||||
|
||||
|
|
|
|||
139
docs/public/integrations/poolside.mdx
Normal file
139
docs/public/integrations/poolside.mdx
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
---
|
||||
title: "Poolside"
|
||||
description: "Run Laguna coding models through Poolside's direct API or OpenRouter"
|
||||
---
|
||||
|
||||
[Poolside](https://poolside.ai/) trains the Laguna family of agentic coding models. Fabro includes Poolside's OpenAI-compatible API as a built-in provider, with Laguna S 2.1 and Laguna XS 2.1 available through both Poolside directly and the opt-in [OpenRouter integration](/integrations/openrouter).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Poolside developer API key from [platform.poolside.ai](https://platform.poolside.ai/)
|
||||
- A running Fabro server
|
||||
|
||||
Poolside Platform currently offers free developer access for a limited time. The built-in catalog records Poolside's published paid endpoint prices so Fabro can produce durable estimated cost telemetry when the preview ends or an account uses paid access.
|
||||
|
||||
## Configure direct access
|
||||
|
||||
The direct `poolside` provider is enabled in the built-in catalog. Store its key in the target Fabro server vault:
|
||||
|
||||
```bash
|
||||
fabro provider login --provider poolside
|
||||
|
||||
# For a non-default remote server:
|
||||
fabro provider login --server https://your-fabro.example --provider poolside
|
||||
|
||||
# Or set the vault token directly:
|
||||
fabro secret set POOLSIDE_API_KEY
|
||||
fabro secret --server https://your-fabro.example set POOLSIDE_API_KEY
|
||||
```
|
||||
|
||||
Standalone SDK usage outside a Fabro server can use an env-backed credential source explicitly:
|
||||
|
||||
```bash
|
||||
export POOLSIDE_API_KEY=<api-key>
|
||||
```
|
||||
|
||||
The provider sends OpenAI-compatible Chat Completions requests to `https://inference.poolside.ai/v1` using bearer authentication.
|
||||
|
||||
## Included models
|
||||
|
||||
| Fabro model ID | Poolside API model ID | Context | Max output | Role |
|
||||
|---|---|---:|---:|---|
|
||||
| `laguna-s-2.1` | `poolside/laguna-s-2.1` | 1,048,576 | 131,072 | Provider default; aliases `laguna`, `laguna-s` |
|
||||
| `laguna-xs-2.1` | `poolside/laguna-xs-2.1` | 262,144 | 32,768 | Small default and connectivity probe; alias `laguna-xs` |
|
||||
|
||||
Both models support text input, tool calling, native reasoning, streaming, and automatic prompt-cache usage reporting. They do not support image input.
|
||||
|
||||
## Use direct Poolside models
|
||||
|
||||
```bash
|
||||
fabro model list --provider poolside
|
||||
fabro model test --model laguna-xs-2.1 --deep
|
||||
fabro run workflow.fabro --model laguna-s-2.1
|
||||
```
|
||||
|
||||
In workflow stylesheets:
|
||||
|
||||
```dot title="workflow.fabro"
|
||||
digraph Example {
|
||||
graph [
|
||||
model_stylesheet="
|
||||
* { model: laguna-s-2.1; }
|
||||
.quick { model: laguna-xs-2.1; }
|
||||
"
|
||||
]
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
work [label="Implement", prompt="Implement and verify the requested change."]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
|
||||
start -> work -> exit
|
||||
}
|
||||
```
|
||||
|
||||
## Reasoning behavior
|
||||
|
||||
Laguna S 2.1 and XS 2.1 support two thinking modes: off and max. Max thinking is enabled by default. These releases do not expose low, medium, or high reasoning-effort levels, so Fabro intentionally does not advertise typed `reasoning_effort` controls for them.
|
||||
|
||||
Fabro preserves Poolside's `reasoning_content` between assistant and tool messages. This is important for interleaved thinking across multi-step tool calls.
|
||||
|
||||
For direct API or SDK requests, disable thinking through `provider_options.poolside`:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "laguna-s-2.1",
|
||||
"provider_options": {
|
||||
"poolside": {
|
||||
"chat_template_kwargs": {
|
||||
"enable_thinking": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Use Laguna through OpenRouter
|
||||
|
||||
Enable OpenRouter and configure its separate API key as described in the [OpenRouter integration](/integrations/openrouter):
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.openrouter]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
The OpenRouter routes use vendor-namespaced model IDs so they can coexist with direct Poolside routes:
|
||||
|
||||
```bash
|
||||
fabro model test --model poolside/laguna-xs-2.1 --deep
|
||||
fabro run workflow.fabro --model poolside/laguna-s-2.1
|
||||
```
|
||||
|
||||
OpenRouter returns authoritative in-band cost telemetry. Its current paid rates per million input, output, and cache-read tokens are:
|
||||
|
||||
| Model | Input | Output | Cache read |
|
||||
|---|---:|---:|---:|
|
||||
| `poolside/laguna-s-2.1` | $0.10 | $0.20 | $0.01 |
|
||||
| `poolside/laguna-xs-2.1` | $0.06 | $0.12 | $0.03 |
|
||||
|
||||
The XS rate reflects OpenRouter's current promotional discount and can change. Fabro prefers OpenRouter's authoritative `usage.cost` over catalog estimates.
|
||||
|
||||
Fabro does not include promotional `:free` OpenRouter variants in the built-in catalog because their availability and limits can change. Add one as a custom model if you explicitly want that route.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"No API key configured"** — Store `POOLSIDE_API_KEY` on the target server with `fabro provider login --provider poolside`. The server runtime resolves the key from its vault, not from process env.
|
||||
|
||||
**Unknown model** — Direct Poolside routes use `laguna-s-2.1` and `laguna-xs-2.1`. OpenRouter routes include the `poolside/` prefix.
|
||||
|
||||
**Reasoning effort rejected** — Laguna's hosted endpoints support thinking off or max, not low/medium/high effort. Omit `reasoning_effort`; use the provider option above only when you need to disable thinking.
|
||||
|
||||
## Further reading
|
||||
|
||||
<Columns cols={2}>
|
||||
<Card title="Poolside API" icon="code" href="https://docs.poolside.ai/api/overview">
|
||||
Official API endpoints, authentication, model listing, and tool-use examples.
|
||||
</Card>
|
||||
<Card title="Laguna models" icon="microchip" href="https://poolside.ai/models">
|
||||
Current model capabilities, weights, context windows, and release information.
|
||||
</Card>
|
||||
</Columns>
|
||||
|
|
@ -373,6 +373,8 @@ For env-backed usage, `EnvCredentialSource` checks for API key environment varia
|
|||
| `ZAI_API_KEY` | ZAI |
|
||||
| `MINIMAX_API_KEY` | Minimax |
|
||||
| `INCEPTION_API_KEY` | Inception |
|
||||
| `POOLSIDE_API_KEY` | Poolside |
|
||||
| `OPENROUTER_API_KEY` | OpenRouter, when enabled in settings |
|
||||
|
||||
The first provider registered becomes the default. Provider base URLs come from the model catalog. For vault-backed usage inside Fabro, use `fabro_auth::VaultCredentialSource` instead.
|
||||
|
||||
|
|
|
|||
|
|
@ -3510,6 +3510,7 @@ root = "{}"
|
|||
assert!(ids.contains(&ProviderId::new("minimax")));
|
||||
assert!(ids.contains(&ProviderId::new("inception")));
|
||||
assert!(ids.contains(&ProviderId::new("venice")));
|
||||
assert!(ids.contains(&ProviderId::new("poolside")));
|
||||
assert!(!ids.contains(&ProviderId::new("ollama")));
|
||||
assert!(!ids.contains(&ProviderId::new("litellm")));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -311,6 +311,8 @@ mod tests {
|
|||
("gpt-5.6-terra", "gpt-5.6-terra", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::OpenAi),
|
||||
("kimi-k2.5", "kimi-k2.5", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi),
|
||||
("kimi-k3", "kimi-k3", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi),
|
||||
("laguna-s-2.1", "poolside/laguna-s-2.1", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi),
|
||||
("laguna-xs-2.1", "poolside/laguna-xs-2.1", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi),
|
||||
("mercury-2", "mercury-2", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi),
|
||||
("minimax-m2.5", "minimax-m2.5", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi),
|
||||
("venice-uncensored-1-2", "venice-uncensored-1-2", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi),
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ fn build_deep_test_params(info: &Model, client: Arc<Client>) -> Option<GenerateP
|
|||
.max_tool_rounds(5)
|
||||
.max_tokens(1024);
|
||||
|
||||
if info.features.reasoning {
|
||||
if info.supports_reasoning_effort() {
|
||||
params = params.reasoning_effort(ReasoningEffort::High);
|
||||
}
|
||||
|
||||
|
|
@ -240,6 +240,40 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_test_omits_effort_for_reasoning_without_effort_controls() {
|
||||
let info = test_model_with(ModelFeatures {
|
||||
tools: true,
|
||||
vision: false,
|
||||
reasoning: true,
|
||||
reasoning_effort: ReasoningEffortFeature::None,
|
||||
prompt_cache: true,
|
||||
sampling_params: true,
|
||||
});
|
||||
|
||||
let params = build_deep_test_params(&info, empty_test_client())
|
||||
.expect("tool-capable model should produce deep-test params");
|
||||
|
||||
assert_eq!(params.reasoning_effort, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_test_uses_high_effort_when_supported() {
|
||||
let info = test_model_with(ModelFeatures {
|
||||
tools: true,
|
||||
vision: false,
|
||||
reasoning: true,
|
||||
reasoning_effort: ReasoningEffortFeature::Levels,
|
||||
prompt_cache: true,
|
||||
sampling_params: true,
|
||||
});
|
||||
|
||||
let params = build_deep_test_params(&info, empty_test_client())
|
||||
.expect("tool-capable model should produce deep-test params");
|
||||
|
||||
assert_eq!(params.reasoning_effort, Some(ReasoningEffort::High));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_deep_result_does_not_fail_only_for_missing_reasoning() {
|
||||
let tool_results = vec![ToolResult::success("call_1", serde_json::json!(42))];
|
||||
|
|
|
|||
|
|
@ -313,6 +313,31 @@ async fn bedrock_openai_frontier_complete() {
|
|||
assert_eq!(response.provider, "bedrock-openai");
|
||||
}
|
||||
|
||||
#[fabro_macros::e2e_test(live("POOLSIDE_API_KEY"))]
|
||||
async fn poolside_laguna_xs_deep_tool_round_trip() {
|
||||
let api_key = std::env::var(EnvVars::POOLSIDE_API_KEY).expect("POOLSIDE_API_KEY must be set");
|
||||
let provider = ProviderId::new("poolside");
|
||||
let catalog = Arc::new(Catalog::from_builtin().expect("built-in catalog should be valid"));
|
||||
let credential = ApiCredential::from_api_key(provider, api_key, &catalog)
|
||||
.expect("Poolside credential should resolve from the catalog");
|
||||
let client = Arc::new(
|
||||
Client::from_credentials(vec![credential], Arc::clone(&catalog))
|
||||
.await
|
||||
.expect("Poolside client should build from the catalog"),
|
||||
);
|
||||
let model = catalog
|
||||
.get("laguna-xs-2.1")
|
||||
.expect("direct Poolside Laguna XS should be present");
|
||||
|
||||
let outcome = run_model_test(model, ModelTestMode::Deep, client).await;
|
||||
assert_eq!(
|
||||
outcome.status,
|
||||
ModelTestStatus::Ok,
|
||||
"direct Poolside Laguna XS deep test failed: {:?}",
|
||||
outcome.error_message
|
||||
);
|
||||
}
|
||||
|
||||
#[fabro_macros::e2e_test(live("OPENROUTER_API_KEY"))]
|
||||
async fn openrouter_complete() {
|
||||
let api_key =
|
||||
|
|
@ -336,6 +361,29 @@ async fn openrouter_complete() {
|
|||
assert_eq!(response.cost_source, Some(CostSource::Authoritative));
|
||||
}
|
||||
|
||||
#[fabro_macros::e2e_test(live("OPENROUTER_API_KEY"))]
|
||||
async fn openrouter_poolside_laguna_complete() {
|
||||
let api_key =
|
||||
std::env::var(EnvVars::OPENROUTER_API_KEY).expect("OPENROUTER_API_KEY must be set");
|
||||
let adapter = OpenAiCompatibleAdapter::new(api_key, "https://openrouter.ai/api/v1")
|
||||
.with_name("openrouter");
|
||||
let request = make_request("poolside/laguna-xs-2.1");
|
||||
let response = adapter.complete(&request).await.unwrap();
|
||||
|
||||
assert!(
|
||||
!response.text().is_empty(),
|
||||
"response text should not be empty"
|
||||
);
|
||||
assert!(response.usage.input_tokens > 0);
|
||||
assert!(response.usage.output_tokens > 0);
|
||||
assert_eq!(response.provider, "openrouter");
|
||||
assert!(
|
||||
response.cost_usd.is_some(),
|
||||
"OpenRouter responses should carry an authoritative usage.cost",
|
||||
);
|
||||
assert_eq!(response.cost_source, Some(CostSource::Authoritative));
|
||||
}
|
||||
|
||||
#[fabro_macros::e2e_test(live("OPENROUTER_API_KEY"))]
|
||||
async fn openrouter_kimi_k3_deep_tool_round_trip() {
|
||||
let api_key =
|
||||
|
|
|
|||
|
|
@ -2005,6 +2005,85 @@ enabled = true
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_poolside_provider_routes_current_laguna_models() {
|
||||
let poolside = ProviderId::new("poolside");
|
||||
let catalog = Catalog::builtin();
|
||||
let provider = catalog
|
||||
.provider(&poolside)
|
||||
.expect("Poolside provider should be active");
|
||||
|
||||
assert_eq!(provider.adapter, AdapterKind::OpenAiCompatible);
|
||||
assert_eq!(provider.codec, CodecKind::OpenAiCompatible);
|
||||
assert_eq!(provider.billing_policy, BillingPolicy::OpenAi);
|
||||
assert_eq!(
|
||||
provider.base_url.as_deref(),
|
||||
Some("https://inference.poolside.ai/v1")
|
||||
);
|
||||
assert_eq!(provider.priority, 65);
|
||||
assert_eq!(provider.auth.as_ref().unwrap().credentials, vec![
|
||||
CredentialRef::Env("POOLSIDE_API_KEY".to_string()),
|
||||
CredentialRef::Vault("POOLSIDE_API_KEY".to_string()),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
catalog
|
||||
.default_for_provider(&poolside)
|
||||
.map(|model| model.id.as_str()),
|
||||
Some("laguna-s-2.1")
|
||||
);
|
||||
assert_eq!(
|
||||
catalog
|
||||
.small_default_for_provider(&poolside)
|
||||
.map(|model| model.id.as_str()),
|
||||
Some("laguna-xs-2.1")
|
||||
);
|
||||
assert_eq!(
|
||||
catalog
|
||||
.probe_for_provider(&poolside)
|
||||
.map(|model| model.id.as_str()),
|
||||
Some("laguna-xs-2.1")
|
||||
);
|
||||
|
||||
let s = catalog.get("laguna").expect("Laguna alias should resolve");
|
||||
assert_eq!(s.id, "laguna-s-2.1");
|
||||
assert_eq!(s.limits.context_window, 1_048_576);
|
||||
assert_eq!(s.limits.max_output, Some(131_072));
|
||||
assert!(s.features.tools);
|
||||
assert!(s.features.reasoning);
|
||||
assert!(s.features.prompt_cache);
|
||||
assert!(s.features.sampling_params);
|
||||
assert!(!s.features.vision);
|
||||
assert!(!s.supports_reasoning_effort());
|
||||
assert_eq!(s.costs.input_cost_per_mtok, Some(0.10));
|
||||
assert_eq!(s.costs.output_cost_per_mtok, Some(0.20));
|
||||
assert_eq!(s.costs.cache_input_cost_per_mtok, Some(0.01));
|
||||
assert_eq!(
|
||||
catalog.model_settings(&s.id).unwrap().api_id,
|
||||
"poolside/laguna-s-2.1"
|
||||
);
|
||||
|
||||
let xs = catalog
|
||||
.get("laguna-xs")
|
||||
.expect("Laguna XS alias should resolve");
|
||||
assert_eq!(xs.id, "laguna-xs-2.1");
|
||||
assert_eq!(xs.limits.context_window, 262_144);
|
||||
assert_eq!(xs.limits.max_output, Some(32_768));
|
||||
assert!(xs.features.tools);
|
||||
assert!(xs.features.reasoning);
|
||||
assert!(xs.features.prompt_cache);
|
||||
assert!(xs.features.sampling_params);
|
||||
assert!(!xs.features.vision);
|
||||
assert!(!xs.supports_reasoning_effort());
|
||||
assert_eq!(xs.costs.input_cost_per_mtok, Some(0.10));
|
||||
assert_eq!(xs.costs.output_cost_per_mtok, Some(0.20));
|
||||
assert_eq!(xs.costs.cache_input_cost_per_mtok, Some(0.05));
|
||||
assert_eq!(
|
||||
catalog.model_settings(&xs.id).unwrap().api_id,
|
||||
"poolside/laguna-xs-2.1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_openrouter_provider_is_opt_in() {
|
||||
let openrouter = ProviderId::new("openrouter");
|
||||
|
|
@ -2121,6 +2200,58 @@ enabled = true
|
|||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_openrouter_includes_poolside_laguna_when_enabled() {
|
||||
let catalog = Catalog::from_builtin_with_overrides(&minimal_settings(
|
||||
r"
|
||||
[providers.openrouter]
|
||||
enabled = true
|
||||
",
|
||||
))
|
||||
.expect("enabled OpenRouter override should build from the built-in provider settings");
|
||||
|
||||
let expected = [
|
||||
(
|
||||
"poolside/laguna-s-2.1",
|
||||
1_048_576,
|
||||
131_072,
|
||||
0.10,
|
||||
0.20,
|
||||
0.01,
|
||||
),
|
||||
("poolside/laguna-xs-2.1", 262_144, 32_768, 0.06, 0.12, 0.03),
|
||||
];
|
||||
|
||||
for (id, context, max_output, input, output, cache_read) in expected {
|
||||
let model = catalog
|
||||
.get(id)
|
||||
.unwrap_or_else(|| panic!("OpenRouter model '{id}' should be present"));
|
||||
assert_eq!(model.provider, ProviderId::new("openrouter"), "{id}");
|
||||
assert_eq!(model.family, "laguna-2", "{id}");
|
||||
assert_eq!(model.limits.context_window, context, "{id}");
|
||||
assert_eq!(model.limits.max_output, Some(max_output), "{id}");
|
||||
assert!(model.features.tools, "{id}");
|
||||
assert!(model.features.reasoning, "{id}");
|
||||
assert!(model.features.prompt_cache, "{id}");
|
||||
assert!(model.features.sampling_params, "{id}");
|
||||
assert!(!model.features.vision, "{id}");
|
||||
assert!(!model.supports_reasoning_effort(), "{id}");
|
||||
assert_eq!(model.costs.input_cost_per_mtok, Some(input), "{id}");
|
||||
assert_eq!(model.costs.output_cost_per_mtok, Some(output), "{id}");
|
||||
assert_eq!(
|
||||
model.costs.cache_input_cost_per_mtok,
|
||||
Some(cache_read),
|
||||
"{id}"
|
||||
);
|
||||
|
||||
let settings = catalog
|
||||
.model_settings(id)
|
||||
.unwrap_or_else(|| panic!("OpenRouter settings for '{id}' should be present"));
|
||||
assert_eq!(settings.api_id, id, "{id}");
|
||||
assert!(settings.controls.reasoning_effort.is_empty(), "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_ollama_provider_is_opt_in() {
|
||||
let ollama = ProviderId::new("ollama");
|
||||
|
|
|
|||
|
|
@ -304,6 +304,52 @@ input_cost_per_mtok = 3.0
|
|||
output_cost_per_mtok = 15.0
|
||||
cache_input_cost_per_mtok = 0.3
|
||||
|
||||
[models."poolside/laguna-s-2.1"]
|
||||
provider = "openrouter"
|
||||
api_id = "poolside/laguna-s-2.1"
|
||||
display_name = "Laguna S 2.1 (via OpenRouter)"
|
||||
family = "laguna-2"
|
||||
|
||||
[models."poolside/laguna-s-2.1".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[models."poolside/laguna-s-2.1".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
sampling_params = true
|
||||
|
||||
[models."poolside/laguna-s-2.1".costs]
|
||||
input_cost_per_mtok = 0.10
|
||||
output_cost_per_mtok = 0.20
|
||||
cache_input_cost_per_mtok = 0.01
|
||||
|
||||
[models."poolside/laguna-xs-2.1"]
|
||||
provider = "openrouter"
|
||||
api_id = "poolside/laguna-xs-2.1"
|
||||
display_name = "Laguna XS 2.1 (via OpenRouter)"
|
||||
family = "laguna-2"
|
||||
|
||||
[models."poolside/laguna-xs-2.1".limits]
|
||||
context_window = 262144
|
||||
max_output = 32768
|
||||
|
||||
[models."poolside/laguna-xs-2.1".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
sampling_params = true
|
||||
|
||||
# Current promotional rate. OpenRouter's authoritative in-band usage.cost
|
||||
# supersedes this estimate on completed responses.
|
||||
[models."poolside/laguna-xs-2.1".costs]
|
||||
input_cost_per_mtok = 0.06
|
||||
output_cost_per_mtok = 0.12
|
||||
cache_input_cost_per_mtok = 0.03
|
||||
|
||||
[models."qwen/qwen3-coder"]
|
||||
provider = "openrouter"
|
||||
api_id = "qwen/qwen3-coder"
|
||||
|
|
|
|||
63
lib/crates/fabro-model/src/catalog/providers/poolside.toml
Normal file
63
lib/crates/fabro-model/src/catalog/providers/poolside.toml
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
[providers.poolside]
|
||||
display_name = "Poolside"
|
||||
adapter = "openai_compatible"
|
||||
api_key_url = "https://platform.poolside.ai"
|
||||
base_url = "https://inference.poolside.ai/v1"
|
||||
priority = 65
|
||||
|
||||
[providers.poolside.auth]
|
||||
credentials = ["env:POOLSIDE_API_KEY", "vault:POOLSIDE_API_KEY"]
|
||||
|
||||
[models."laguna-s-2.1"]
|
||||
provider = "poolside"
|
||||
api_id = "poolside/laguna-s-2.1"
|
||||
display_name = "Laguna S 2.1"
|
||||
family = "laguna-2"
|
||||
default = true
|
||||
aliases = ["laguna", "laguna-s"]
|
||||
|
||||
[models."laguna-s-2.1".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[models."laguna-s-2.1".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
sampling_params = true
|
||||
|
||||
# Poolside Platform is free for a limited preview period. Keep the published
|
||||
# paid hosted rate as Fabro's durable estimate for paid access and post-preview
|
||||
# usage.
|
||||
[models."laguna-s-2.1".costs]
|
||||
input_cost_per_mtok = 0.10
|
||||
output_cost_per_mtok = 0.20
|
||||
cache_input_cost_per_mtok = 0.01
|
||||
|
||||
[models."laguna-xs-2.1"]
|
||||
provider = "poolside"
|
||||
api_id = "poolside/laguna-xs-2.1"
|
||||
display_name = "Laguna XS 2.1"
|
||||
family = "laguna-2"
|
||||
small_default = true
|
||||
probe = true
|
||||
aliases = ["laguna-xs"]
|
||||
|
||||
[models."laguna-xs-2.1".limits]
|
||||
context_window = 262144
|
||||
max_output = 32768
|
||||
|
||||
[models."laguna-xs-2.1".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
sampling_params = true
|
||||
|
||||
# Poolside Platform is free for a limited preview period. These are Poolside's
|
||||
# published paid endpoint rates.
|
||||
[models."laguna-xs-2.1".costs]
|
||||
input_cost_per_mtok = 0.10
|
||||
output_cost_per_mtok = 0.20
|
||||
cache_input_cost_per_mtok = 0.05
|
||||
|
|
@ -2567,6 +2567,13 @@ regex = '''\b(sk-or-v1-[0-9a-f]{64})(?:['|\"|\n|\r|\s|\x60|;]|$)'''
|
|||
entropy = 3
|
||||
keywords = ["sk-or-v1-"]
|
||||
|
||||
[[rules]]
|
||||
id = "poolside-api-key"
|
||||
description = "Found a Poolside API Key, posing a risk of unauthorized model access and billing."
|
||||
regex = '''\b(sky_[A-Za-z0-9]{8}\.[A-Za-z0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)'''
|
||||
entropy = 3
|
||||
keywords = ["sky_"]
|
||||
|
||||
[[rules]]
|
||||
id = "openshift-user-token"
|
||||
description = "Found an OpenShift user token, potentially compromising an OpenShift/Kubernetes cluster."
|
||||
|
|
|
|||
|
|
@ -272,6 +272,14 @@ mod tests {
|
|||
assert_eq!(regions.len(), 1, "expected 1 region, got {regions:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_poolside_api_key() {
|
||||
let input = format!("sky_Ab12Cd34.{}", "Ef56Gh78".repeat(4));
|
||||
let regions = find_gitleaks_regions(&input);
|
||||
assert_eq!(regions.len(), 1, "expected 1 region, got {regions:?}");
|
||||
assert_eq!(&input[regions[0].start..regions[0].end], input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_private_key_block() {
|
||||
let input =
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ impl EnvVars {
|
|||
pub const OPENAI_ORG_ID: &'static str = "OPENAI_ORG_ID";
|
||||
pub const OPENAI_PROJECT_ID: &'static str = "OPENAI_PROJECT_ID";
|
||||
pub const OPENROUTER_API_KEY: &'static str = "OPENROUTER_API_KEY";
|
||||
pub const POOLSIDE_API_KEY: &'static str = "POOLSIDE_API_KEY";
|
||||
pub const ZAI_API_KEY: &'static str = "ZAI_API_KEY";
|
||||
|
||||
// GitHub, OAuth, and Slack
|
||||
|
|
@ -207,6 +208,7 @@ mod tests {
|
|||
EnvVars::OPENAI_ORG_ID,
|
||||
EnvVars::OPENAI_PROJECT_ID,
|
||||
EnvVars::OPENROUTER_API_KEY,
|
||||
EnvVars::POOLSIDE_API_KEY,
|
||||
EnvVars::ZAI_API_KEY,
|
||||
EnvVars::GH_TOKEN,
|
||||
EnvVars::GITHUB_APP_CLIENT_SECRET,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const OPTIONAL_VAULT_SECRETS: &[&str] = &[
|
|||
EnvVars::MINIMAX_API_KEY,
|
||||
EnvVars::OPENAI_API_KEY,
|
||||
EnvVars::OPENROUTER_API_KEY,
|
||||
EnvVars::POOLSIDE_API_KEY,
|
||||
EnvVars::ZAI_API_KEY,
|
||||
EnvVars::DAYTONA_API_KEY,
|
||||
];
|
||||
|
|
@ -96,6 +97,7 @@ mod tests {
|
|||
EnvVars::MINIMAX_API_KEY,
|
||||
EnvVars::OPENAI_API_KEY,
|
||||
EnvVars::OPENROUTER_API_KEY,
|
||||
EnvVars::POOLSIDE_API_KEY,
|
||||
EnvVars::ZAI_API_KEY,
|
||||
] {
|
||||
assert_eq!(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue