mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
fabro(01KY6E8S0YA6KAR5ZF53X7QMWZ): implement (failed)
Fabro-Run: 01KY6E8S0YA6KAR5ZF53X7QMWZ
Fabro-Completed: 5
Fabro-Checkpoint: 5aff0b553a
⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
parent
3f42a97742
commit
85a7690acf
32 changed files with 1657 additions and 564 deletions
|
|
@ -9,6 +9,43 @@ No single model is best at everything. Fabro lets you assign the right model to
|
|||
<img src="/images/ensemble-workflow.svg" alt="Ensemble workflow: fan out to Opus and Gemini Pro, merge, then synthesize" />
|
||||
</Frame>
|
||||
|
||||
## How model selection works
|
||||
|
||||
Fabro separates the name a workflow uses from the value a provider expects on
|
||||
the wire:
|
||||
|
||||
| Term | Meaning |
|
||||
|---|---|
|
||||
| **Provider ID** | Who serves the request, such as `openai` or `openrouter`. |
|
||||
| **Model slug** | The canonical, human-facing model ID, such as `gpt-5.6-sol`. |
|
||||
| **Alias** | An alternate user-facing selector, such as `gpt-56-sol`. |
|
||||
| **Offering** | One provider's route to one model slug. Its identity is `(provider, model slug)`. |
|
||||
| **Family** | Metadata used for display and compatible-model matching, not a routing namespace. |
|
||||
| **API ID** | The opaque string sent to the selected provider API. Workflows do not reference it. |
|
||||
|
||||
A model slug is unique within a provider, not across the whole catalog. Two
|
||||
providers can offer the same slug and reuse the same alias, so a workflow can
|
||||
use one stable selector wherever either provider is available.
|
||||
|
||||
For an unqualified selector, Fabro first finds matching offerings on **ready
|
||||
providers**—providers whose adapters registered successfully with usable
|
||||
credentials and configuration. It then chooses the provider with the highest
|
||||
`priority`; equal priorities use canonical provider ID in ascending order. A
|
||||
canonical model-slug match is considered before alias matches.
|
||||
|
||||
| Ready providers | Selector | Selected offering |
|
||||
|---|---|---|
|
||||
| OpenAI only | `gpt-56-sol` | OpenAI's `gpt-5.6-sol` |
|
||||
| OpenRouter only | `gpt-56-sol` | OpenRouter's `gpt-5.6-sol` offering |
|
||||
| OpenAI and OpenRouter | `gpt-56-sol` | OpenAI, because its provider priority is higher |
|
||||
| Both, with `provider = "openrouter"` | `gpt-56-sol` | OpenRouter, because an explicit provider is a pin |
|
||||
|
||||
<Note>
|
||||
An explicit provider restricts lookup to that provider. If the pinned provider
|
||||
is unavailable or does not offer the selector, Fabro reports the error instead
|
||||
of silently switching providers.
|
||||
</Note>
|
||||
|
||||
## Model catalog
|
||||
|
||||
| Model | Provider | Aliases | Context | Cost (in/out per Mtok) | Speed |
|
||||
|
|
@ -46,13 +83,14 @@ Claude Fable 5 is available as an explicit model but is not the default Anthropi
|
|||
|
||||
## Configuring providers and models
|
||||
|
||||
Fabro's catalog starts with the built-in providers and models, then merges any `[llm]` entries from settings. Provider and model IDs are strings, so a server or project can add an OpenAI-compatible provider without a Fabro release.
|
||||
Fabro's catalog starts with the built-in providers and models, then merges any `[llm]` entries from settings. Provider and model IDs are strings, so a server or project can add an OpenAI-compatible provider without a Fabro release. Declare each model under the provider that serves it:
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.proxy]
|
||||
display_name = "Acme Gateway"
|
||||
adapter = "openai_compatible"
|
||||
base_url = "https://llm-gateway.example.com/v1"
|
||||
priority = 50
|
||||
aliases = ["gateway"]
|
||||
|
||||
[llm.providers.proxy.auth]
|
||||
|
|
@ -62,8 +100,7 @@ credentials = ["env:ACME_GATEWAY_API_KEY", "vault:ACME_GATEWAY_API_KEY"]
|
|||
x-portkey-api-key = "{{ env.PORTKEY_API_KEY }}"
|
||||
x-portkey-config = "@bedrock-prod"
|
||||
|
||||
[llm.models."team-code-large"]
|
||||
provider = "proxy"
|
||||
[llm.providers.proxy.models."team-code-large"]
|
||||
api_id = "provider-wire-model-name"
|
||||
agent_profile = "anthropic"
|
||||
display_name = "Team Code Large"
|
||||
|
|
@ -73,32 +110,33 @@ small_default = true
|
|||
aliases = ["team-code"]
|
||||
estimated_output_tps = 80
|
||||
|
||||
[llm.models."team-code-large".limits]
|
||||
[llm.providers.proxy.models."team-code-large".limits]
|
||||
context_window = 200000
|
||||
max_output = 32000
|
||||
|
||||
[llm.models."team-code-large".features]
|
||||
[llm.providers.proxy.models."team-code-large".features]
|
||||
tools = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
effort = true
|
||||
|
||||
[llm.models."team-code-large".controls]
|
||||
[llm.providers.proxy.models."team-code-large".controls]
|
||||
reasoning_effort = ["low", "medium", "high"]
|
||||
speed = ["fast"]
|
||||
|
||||
[llm.models."team-code-large".costs]
|
||||
[llm.providers.proxy.models."team-code-large".costs]
|
||||
input_cost_per_mtok = 1.50
|
||||
output_cost_per_mtok = 8.00
|
||||
cache_input_cost_per_mtok = 0.30
|
||||
|
||||
[llm.models."team-code-large".costs.speed.fast]
|
||||
[llm.providers.proxy.models."team-code-large".costs.speed.fast]
|
||||
input_cost_per_mtok = 3.00
|
||||
output_cost_per_mtok = 16.00
|
||||
cache_input_cost_per_mtok = 0.60
|
||||
```
|
||||
|
||||
The table key (`team-code-large`) is the model slug that workflows select. `api_id` is an opaque provider-facing wire value. It defaults to the exact model slug when omitted, so configure it only when the provider expects a different string, such as a deployment name, `author/model` slug, or Bedrock inference-profile ID. Fabro does not parse it for provider routing, add prefixes, or otherwise infer meaning from it; an explicitly empty `api_id` is invalid.
|
||||
|
||||
For [LiteLLM](/integrations/litellm), Fabro ships a disabled provider entry. Enable it in settings and declare the models your proxy exposes:
|
||||
|
||||
```toml title="settings.toml"
|
||||
|
|
@ -106,24 +144,27 @@ For [LiteLLM](/integrations/litellm), Fabro ships a disabled provider entry. Ena
|
|||
enabled = true
|
||||
base_url = "http://localhost:4000/v1"
|
||||
|
||||
[llm.models."litellm-gpt-5"]
|
||||
provider = "litellm"
|
||||
[llm.providers.litellm.models."litellm-gpt-5"]
|
||||
api_id = "gpt-5"
|
||||
display_name = "LiteLLM GPT-5"
|
||||
family = "litellm"
|
||||
default = true
|
||||
|
||||
[llm.models."litellm-gpt-5".limits]
|
||||
[llm.providers.litellm.models."litellm-gpt-5".limits]
|
||||
context_window = 128000
|
||||
max_output = 8192
|
||||
|
||||
[llm.models."litellm-gpt-5".features]
|
||||
[llm.providers.litellm.models."litellm-gpt-5".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
```
|
||||
|
||||
`api_id` is the model name sent to the provider API. Omit it when the Fabro model ID and provider model ID are the same.
|
||||
### Reusing aliases across providers
|
||||
|
||||
Aliases are scoped to a provider. An alias or canonical slug must identify exactly one model within that provider, so two models under `proxy` cannot both claim `team-code`. The same slug or alias may be reused by another provider; that reuse is what makes unqualified selectors portable. Across providers, an exact canonical-slug match takes precedence over an alias match. You can still reach a shadowed alias by pinning its provider.
|
||||
|
||||
The older `[llm.models.<id>]` form remains readable as a compatibility input, but new configuration and built-in catalog entries should use `[llm.providers.<provider>.models.<model>]`.
|
||||
|
||||
Model roles are separate: `default = true` controls normal model selection for workflow execution, while `small_default = true` marks the provider's small/cheap utility model for metadata tasks such as generated run titles. If a provider has no small default, Fabro falls back to that provider's normal default.
|
||||
|
||||
|
|
@ -169,7 +210,7 @@ Fabro ships an Ollama provider definition that is disabled by default. Enable it
|
|||
enabled = true
|
||||
```
|
||||
|
||||
Enabling the provider alone does not expose any models — until #267 adds auto-discovery, add explicit `[llm.models.<id>]` blocks for each Ollama model you have pulled locally. Ollama's OpenAI-compatible endpoint accepts any bearer token, so local users can set `OLLAMA_API_KEY=ollama`.
|
||||
Enabling the provider alone does not expose any models — until #267 adds auto-discovery, add an explicit `[llm.providers.ollama.models.<model>]` block for each Ollama model you have pulled locally. Ollama's OpenAI-compatible endpoint accepts any bearer token, so local users can set `OLLAMA_API_KEY=ollama`.
|
||||
|
||||
## Default models
|
||||
|
||||
|
|
@ -217,11 +258,12 @@ Model stylesheets set per-node models inside the workflow graph, but you can als
|
|||
Pass `--model` and optionally `--provider` to `fabro run`:
|
||||
|
||||
```bash
|
||||
fabro run docs/internal/demo/01-hello.fabro --model claude-opus-4-6
|
||||
fabro run docs/internal/demo/01-hello.fabro --model gpt-56-sol
|
||||
fabro run docs/internal/demo/01-hello.fabro --model gpt-56-sol --provider openrouter
|
||||
fabro run docs/internal/demo/04-pipeline.fabro --model gemini-3.1-pro-preview
|
||||
```
|
||||
|
||||
These flags set the default model for all nodes that don't have an explicit model assigned via a stylesheet. The provider is automatically inferred from the model catalog — you only need `--provider` for models not in the catalog or to force a specific provider.
|
||||
These flags set the default model for all nodes that don't have an explicit model assigned via a stylesheet. Without `--provider`, the selector is portable across ready offerings and provider priority decides. `--provider` is an explicit pin, including for models not in the catalog.
|
||||
|
||||
### Run config TOML
|
||||
|
||||
|
|
@ -247,7 +289,13 @@ Then launch with:
|
|||
fabro run run.toml
|
||||
```
|
||||
|
||||
The `fallbacks` array is optional. Each entry may be a bare provider token (like `"gemini"`), a bare model alias (like `"gpt-5.4"`), or a qualified `"provider/model"` reference. Fabro tries them in order when the primary provider is unavailable.
|
||||
The `fallbacks` array is optional. Each entry may be a bare provider token (like `"gemini"`), a bare model alias (like `"gpt-5.4"`), or a qualified `"provider/model"` reference. Fabro resolves each entry to a concrete provider and canonical model, then tries that persisted chain in order after a failover-eligible error. Provider-only entries choose the closest compatible model; qualified model entries stay pinned to their named provider.
|
||||
|
||||
### Resolution is stable for a run
|
||||
|
||||
When Fabro creates a run, it resolves every implicit model selector against the ready-provider snapshot and persists the chosen canonical `(provider, model slug)` in the run. Resuming that run uses the materialized choice—it does not re-rank providers because credentials or priorities changed later.
|
||||
|
||||
This resolve-once behavior makes a run reproducible; the fallback chain is the separate mechanism for handling a provider that fails after creation. A newly created run can choose a different ready offering from the same portable selector.
|
||||
|
||||
<Note>
|
||||
The precedence order is: node-level stylesheet > run config TOML > CLI flags > server defaults. More specific settings always win.
|
||||
|
|
|
|||
|
|
@ -121,7 +121,9 @@ provider = "anthropic"
|
|||
fallbacks = ["gemini", "openai"]
|
||||
```
|
||||
|
||||
When Anthropic is unavailable, Fabro tries Gemini first, then OpenAI. Each fallback entry may be a bare provider token (like `"gemini"`), a bare model alias (like `"gpt-5.4"`), or a qualified `"provider/model"` reference. For each fallback provider, Fabro selects the closest model by matching required capabilities (tool use, vision, reasoning) and minimizing cost difference.
|
||||
When Anthropic is unavailable, Fabro tries Gemini first, then OpenAI. Each fallback entry may be a bare provider token (like `"gemini"`), a bare model alias (like `"gpt-5.4"`), or a qualified `"provider/model"` reference. Provider-only entries select the closest model by matching required capabilities (tool use, vision, reasoning) and minimizing cost difference. Qualified model entries are provider pins; bare models and aliases select among ready providers by priority.
|
||||
|
||||
Fabro resolves the primary and fallback selectors to concrete provider/model offerings when it creates the run and persists the result. Resume reuses that materialized chain rather than re-ranking providers after credentials or priorities change. Runtime fallback is the mechanism for a provider failure that happens after creation.
|
||||
|
||||
### What triggers failover
|
||||
|
||||
|
|
|
|||
|
|
@ -138,11 +138,11 @@ name = "claude-sonnet-4-5"
|
|||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `name` | Model ID or alias (e.g. `claude-sonnet-4-5`, `opus`, `gemini-pro`). See [Models](/core-concepts/models). |
|
||||
| `provider` | Provider name (optional — auto-inferred from the model catalog). Only needed for models not in the catalog or to force a specific provider. |
|
||||
| `fallbacks` | Ordered list of model references to try when the primary is unavailable. Entries can be bare provider tokens (`"openai"`), bare model aliases, or qualified `"provider/model"` references. |
|
||||
| `name` | Canonical model slug or alias (e.g. `claude-sonnet-4-5`, `opus`, `gemini-pro`). See [Models](/core-concepts/models). |
|
||||
| `provider` | Optional provider pin. When omitted, Fabro selects a matching offering from ready providers by provider priority. When set, lookup is restricted to that provider and unavailability is an error. |
|
||||
| `fallbacks` | Ordered list of model references to try after a failover-eligible error. Entries can be bare provider tokens (`"openai"`), bare model aliases, or qualified `"provider/model"` references. |
|
||||
|
||||
Provider values are catalog provider ID strings. Built-in IDs like `anthropic` and `openai` work, and settings-defined IDs like `proxy` work after they are added under `[llm.providers.<id>]`.
|
||||
Provider values are catalog provider ID strings. Built-in IDs like `anthropic` and `openai` work, and settings-defined IDs like `proxy` work after they are added under `[llm.providers.<id>]`. A qualified fallback such as `"openrouter/gpt-56-sol"` is pinned to that provider; a bare alias can select among ready fallback offerings by priority.
|
||||
|
||||
#### `[run.model.controls]`
|
||||
|
||||
|
|
@ -163,6 +163,12 @@ speed = "fast"
|
|||
| `reasoning_effort` | Native reasoning-effort value to request when the selected model allows it, such as `"low"`, `"medium"`, `"high"`, `"xhigh"`, or `"max"`. |
|
||||
| `speed` | Native speed value to request when the selected model declares it, such as `"fast"`. The standard speed is implicit and does not need to be set. |
|
||||
|
||||
#### Resolution and fallback behavior
|
||||
|
||||
At run creation, Fabro resolves unqualified primary and fallback selectors to concrete provider and canonical-model pairs using the ready-provider snapshot, then persists those choices. Resume reuses the materialized routing and does not reconsider provider priority if credentials or configuration changed. Runtime failover walks the persisted fallback chain; create a new run to reselect from current provider availability.
|
||||
|
||||
Provider-only fallbacks choose the closest compatible model on that provider. A provider-qualified model or alias resolves only within that provider, while a bare model or alias uses ready providers and priority.
|
||||
|
||||
#### Fallbacks with splice
|
||||
|
||||
Use the reserved `"..."` marker in `fallbacks` to splice in the inherited list from lower-precedence layers:
|
||||
|
|
|
|||
|
|
@ -24,24 +24,23 @@ _version = 1
|
|||
enabled = true
|
||||
base_url = "http://localhost:4000/v1"
|
||||
|
||||
[llm.models."litellm-gpt-5"]
|
||||
provider = "litellm"
|
||||
[llm.providers.litellm.models."litellm-gpt-5"]
|
||||
api_id = "gpt-5"
|
||||
display_name = "LiteLLM GPT-5"
|
||||
family = "litellm"
|
||||
default = true
|
||||
|
||||
[llm.models."litellm-gpt-5".limits]
|
||||
[llm.providers.litellm.models."litellm-gpt-5".limits]
|
||||
context_window = 128000
|
||||
max_output = 8192
|
||||
|
||||
[llm.models."litellm-gpt-5".features]
|
||||
[llm.providers.litellm.models."litellm-gpt-5".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
```
|
||||
|
||||
`api_id` is the model name Fabro sends to LiteLLM. It should match a model name configured in your LiteLLM proxy.
|
||||
`api_id` is the opaque model name Fabro sends to LiteLLM. It should match a model name configured in your LiteLLM proxy. When the provider-facing name is the same as the Fabro model slug, omit `api_id`; it defaults to the slug.
|
||||
|
||||
## Configure credentials
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ use crate::resolve::{
|
|||
};
|
||||
use crate::user::load_settings_config;
|
||||
use crate::{
|
||||
CliLayer, Combine, CostRates, EnvironmentLayer, Error, LlmLayer, LlmModelFeatures,
|
||||
LlmModelLimits, MergeMap, ModelControls, ModelCostTable, ModelSettings, ProviderSettings,
|
||||
Result, RunLayer, ServerLayer, SettingsLayer, run,
|
||||
CliLayer, Combine, CostRates, EnvironmentLayer, Error, LegacyModelSettings, LlmLayer,
|
||||
LlmModelFeatures, LlmModelLimits, MergeMap, ModelControls, ModelCostTable, ModelSettings,
|
||||
ProviderSettings, Result, RunLayer, ServerLayer, SettingsLayer, run,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -321,7 +321,7 @@ fn llm_layer_to_catalog_settings(llm: LlmLayer) -> model_catalog::LlmCatalogSett
|
|||
.models
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.map(|(id, settings)| (id, model_settings_to_catalog(settings)))
|
||||
.map(|(id, settings)| (id, legacy_model_settings_to_catalog(settings)))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
|
@ -341,6 +341,12 @@ fn provider_settings_to_catalog(
|
|||
.collect()
|
||||
});
|
||||
model_catalog::ProviderCatalogSettings {
|
||||
models: settings
|
||||
.models
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.map(|(id, settings)| (id, model_settings_to_catalog(settings)))
|
||||
.collect(),
|
||||
display_name: settings.display_name,
|
||||
adapter: settings.adapter,
|
||||
codec: settings.codec,
|
||||
|
|
@ -356,9 +362,17 @@ fn provider_settings_to_catalog(
|
|||
}
|
||||
}
|
||||
|
||||
fn legacy_model_settings_to_catalog(
|
||||
settings: LegacyModelSettings,
|
||||
) -> model_catalog::ModelCatalogSettings {
|
||||
let LegacyModelSettings { provider, model } = settings;
|
||||
let mut settings = model_settings_to_catalog(model);
|
||||
settings.provider = provider;
|
||||
settings
|
||||
}
|
||||
|
||||
fn model_settings_to_catalog(settings: ModelSettings) -> model_catalog::ModelCatalogSettings {
|
||||
let ModelSettings {
|
||||
provider,
|
||||
api_id,
|
||||
codec,
|
||||
billing_policy,
|
||||
|
|
@ -379,7 +393,7 @@ fn model_settings_to_catalog(settings: ModelSettings) -> model_catalog::ModelCat
|
|||
costs,
|
||||
} = settings;
|
||||
model_catalog::ModelCatalogSettings {
|
||||
provider,
|
||||
provider: None,
|
||||
api_id,
|
||||
codec,
|
||||
billing_policy,
|
||||
|
|
@ -820,7 +834,7 @@ provider = "docker"
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn server_runtime_settings_preserves_llm_catalog_overrides() {
|
||||
fn server_runtime_settings_preserves_provider_scoped_llm_catalog_overrides() {
|
||||
let settings = server_runtime_settings_from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
|
@ -837,17 +851,16 @@ agent_profile = "anthropic"
|
|||
[llm.providers.acme.auth]
|
||||
credentials = ["env:ACME_API_KEY"]
|
||||
|
||||
[llm.models."acme-large"]
|
||||
provider = "acme"
|
||||
[llm.providers.acme.models."acme-large"]
|
||||
display_name = "Acme Large"
|
||||
family = "acme"
|
||||
default = true
|
||||
agent_profile = "gemini"
|
||||
|
||||
[llm.models."acme-large".limits]
|
||||
[llm.providers.acme.models."acme-large".limits]
|
||||
context_window = 128000
|
||||
|
||||
[llm.models."acme-large".features]
|
||||
[llm.providers.acme.models."acme-large".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
|
@ -857,20 +870,69 @@ reasoning = false
|
|||
)
|
||||
.expect("server runtime settings should resolve");
|
||||
|
||||
let catalog =
|
||||
fabro_model::Catalog::from_builtin_with_overrides(&settings.llm_catalog_settings)
|
||||
.expect("catalog overrides should build");
|
||||
let provider = settings
|
||||
.llm_catalog_settings
|
||||
.providers
|
||||
.get("acme")
|
||||
.expect("provider settings should be present");
|
||||
let model = provider
|
||||
.models
|
||||
.get("acme-large")
|
||||
.expect("provider-scoped model settings should be present");
|
||||
assert_eq!(model.display_name.as_deref(), Some("Acme Large"));
|
||||
assert_eq!(model.agent_profile, Some(fabro_model::AgentProfileKind::Gemini));
|
||||
assert!(settings.llm_catalog_settings.models.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_runtime_settings_converts_legacy_models_to_provider_catalog_shape() {
|
||||
let settings = server_runtime_settings_from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[server.auth]
|
||||
methods = ["dev-token"]
|
||||
|
||||
[llm.models."acme-large"]
|
||||
provider = "acme"
|
||||
display_name = "Acme Large"
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("legacy catalog settings should resolve");
|
||||
|
||||
assert_eq!(
|
||||
catalog
|
||||
.get("acme-large")
|
||||
.map(|model| model.provider.clone()),
|
||||
Some(fabro_model::ProviderId::new("acme"))
|
||||
settings.llm_catalog_settings.providers["acme"].models["acme-large"]
|
||||
.display_name
|
||||
.as_deref(),
|
||||
Some("Acme Large")
|
||||
);
|
||||
assert!(settings.llm_catalog_settings.models.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_runtime_settings_retains_providerless_legacy_models_for_catalog_adoption() {
|
||||
let settings = server_runtime_settings_from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[server.auth]
|
||||
methods = ["dev-token"]
|
||||
|
||||
[llm.models."known-model"]
|
||||
display_name = "Renamed Known Model"
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("provider-less legacy catalog settings should resolve");
|
||||
|
||||
assert_eq!(
|
||||
catalog
|
||||
.effective_agent_profile(&fabro_model::ProviderId::new("acme"), Some("acme-large")),
|
||||
Some(fabro_model::AgentProfileKind::Gemini)
|
||||
settings.llm_catalog_settings.models["known-model"]
|
||||
.display_name
|
||||
.as_deref(),
|
||||
Some("Renamed Known Model")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,11 +12,15 @@
|
|||
//! enabled = true
|
||||
//! aliases = ["moonshot"]
|
||||
//!
|
||||
//! [llm.models."kimi-k2.5"]
|
||||
//! provider = "kimi"
|
||||
//! [llm.providers.kimi.models."kimi-k2.5"]
|
||||
//! ...
|
||||
//! ```
|
||||
//!
|
||||
//! Legacy top-level `[llm.models.<id>]` rows remain accepted by the settings
|
||||
//! parser. Rows with a `provider` are normalized into the canonical provider
|
||||
//! scope before layers combine; provider-less rows are retained for
|
||||
//! catalog-aware compatibility handling.
|
||||
//!
|
||||
//! Per-provider and per-model entries field-merge across layers (default →
|
||||
//! user → server → project → workflow/run). Inner arrays such as
|
||||
//! `auth.credentials`, `aliases`, `controls.reasoning_effort`, and
|
||||
|
|
@ -43,15 +47,22 @@ pub struct LlmLayer {
|
|||
/// Provider definitions keyed by provider ID.
|
||||
#[serde(default, skip_serializing_if = "MergeMap::is_empty")]
|
||||
pub providers: MergeMap<ProviderSettings>,
|
||||
/// Model definitions keyed by canonical model ID.
|
||||
/// Legacy top-level model definitions keyed by canonical model ID.
|
||||
///
|
||||
/// Provider-qualified rows are moved into [`ProviderSettings::models`] by
|
||||
/// the settings parser. Rows without a provider remain here until the
|
||||
/// built-in catalog can adopt them unambiguously.
|
||||
#[serde(default, skip_serializing_if = "MergeMap::is_empty")]
|
||||
pub models: MergeMap<ModelSettings>,
|
||||
pub models: MergeMap<LegacyModelSettings>,
|
||||
}
|
||||
|
||||
/// One entry in `[llm.providers.<id>]`.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ProviderSettings {
|
||||
/// Model definitions owned by this provider, keyed by canonical model ID.
|
||||
#[serde(default, skip_serializing_if = "MergeMap::is_empty")]
|
||||
pub models: MergeMap<ModelSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
/// Adapter registry key (e.g. `"openai_compatible"`).
|
||||
|
|
@ -89,13 +100,10 @@ pub struct ProviderSettings {
|
|||
pub aliases: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// One entry in `[llm.models.<id>]`.
|
||||
/// One entry in `[llm.providers.<provider>.models.<id>]`.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ModelSettings {
|
||||
/// Provider ID this model belongs to.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider: Option<String>,
|
||||
/// Identifier sent to the provider API. Defaults to the catalog model ID
|
||||
/// when omitted.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -155,6 +163,38 @@ pub struct ModelSettings {
|
|||
pub costs: Option<ModelCostTable>,
|
||||
}
|
||||
|
||||
/// Input-only compatibility row for the legacy `[llm.models.<id>]` shape.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LegacyModelSettings {
|
||||
/// Provider ID used to move this row into the canonical provider scope.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub model: ModelSettings,
|
||||
}
|
||||
|
||||
impl LegacyModelSettings {
|
||||
#[must_use]
|
||||
pub(crate) fn into_model(self) -> ModelSettings {
|
||||
self.model
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for LegacyModelSettings {
|
||||
type Target = ModelSettings;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.model
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for LegacyModelSettings {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.model
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ModelLimits {
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ pub use environment::{
|
|||
EnvironmentNetworkLayer, EnvironmentResourcesLayer, RunEnvironmentLayer,
|
||||
};
|
||||
pub use llm::{
|
||||
CostRates, CredentialRef, CredentialRefParseError, LlmLayer, ModelControls, ModelCostTable,
|
||||
ModelFeatures as LlmModelFeatures, ModelLimits as LlmModelLimits, ModelSettings,
|
||||
CostRates, CredentialRef, CredentialRefParseError, LegacyModelSettings, LlmLayer, ModelControls,
|
||||
ModelCostTable, ModelFeatures as LlmModelFeatures, ModelLimits as LlmModelLimits, ModelSettings,
|
||||
ProviderSettings, ReasoningEffortFeature,
|
||||
};
|
||||
pub use log_filter::LogFilter;
|
||||
|
|
|
|||
|
|
@ -46,8 +46,9 @@ pub use layers::{
|
|||
CredentialRefParseError, EnvironmentDockerfileLayer, EnvironmentImageLayer, EnvironmentLayer,
|
||||
EnvironmentLifecycleLayer, EnvironmentNetworkLayer, EnvironmentResourcesLayer, GitAuthorLayer,
|
||||
GithubIntegrationLayer, HookAgentMarker, HookEntry, HookTlsMode, IntegrationWebhooksLayer,
|
||||
InterviewProviderLayer, InterviewsLayer, LlmLayer, LlmModelFeatures, LlmModelLimits, LogFilter,
|
||||
McpEntryLayer, MergeMap, ModelControls, ModelCostTable, ModelRefOrSplice, ModelSettings,
|
||||
InterviewProviderLayer, InterviewsLayer, LegacyModelSettings, LlmLayer, LlmModelFeatures,
|
||||
LlmModelLimits, LogFilter, McpEntryLayer, MergeMap, ModelControls, ModelCostTable,
|
||||
ModelRefOrSplice, ModelSettings,
|
||||
NotificationProviderLayer, NotificationRouteLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer,
|
||||
PrepareStep, ProjectLayer, ProviderSettings, ReasoningEffortFeature, ReplaceMap, RunAgentLayer,
|
||||
RunArtifactsLayer, RunCheckpointLayer, RunCloneLayer, RunEnvironmentLayer, RunExecutionLayer,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,13 @@ pub enum ParseError {
|
|||
path: String,
|
||||
source: SettingsSource,
|
||||
},
|
||||
ConflictingLlmModelDefinitions {
|
||||
provider: String,
|
||||
model: String,
|
||||
},
|
||||
InvalidLegacyLlmModelProvider {
|
||||
model: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
|
|
@ -60,6 +67,14 @@ impl fmt::Display for ParseError {
|
|||
f,
|
||||
"`{path}` is server-managed and cannot be set in {source} settings; configure cwd on a server-managed environment instead."
|
||||
),
|
||||
Self::ConflictingLlmModelDefinitions { provider, model } => write!(
|
||||
f,
|
||||
"model `{model}` on provider `{provider}` is defined in both `llm.models.{model}` and `llm.providers.{provider}.models.{model}` in the same settings source"
|
||||
),
|
||||
Self::InvalidLegacyLlmModelProvider { model } => write!(
|
||||
f,
|
||||
"legacy model `llm.models.{model}` has an empty provider; omit it for catalog-aware adoption or set a non-empty provider ID"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -118,8 +133,36 @@ pub(crate) fn parse_settings(input: &str) -> Result<SettingsLayer, ParseError> {
|
|||
}
|
||||
}
|
||||
|
||||
raw.try_into::<SettingsLayer>()
|
||||
.map_err(|e| ParseError::Toml(e.to_string()))
|
||||
let mut layer = raw
|
||||
.try_into::<SettingsLayer>()
|
||||
.map_err(|e| ParseError::Toml(e.to_string()))?;
|
||||
normalize_legacy_llm_models(&mut layer)?;
|
||||
Ok(layer)
|
||||
}
|
||||
|
||||
fn normalize_legacy_llm_models(layer: &mut SettingsLayer) -> Result<(), ParseError> {
|
||||
let Some(llm) = layer.llm.as_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let legacy_models = std::mem::take(&mut llm.models).into_inner();
|
||||
for (model, legacy) in legacy_models {
|
||||
let Some(provider) = legacy.provider.as_deref() else {
|
||||
llm.models.insert(model, legacy);
|
||||
continue;
|
||||
};
|
||||
if provider.is_empty() {
|
||||
return Err(ParseError::InvalidLegacyLlmModelProvider { model });
|
||||
}
|
||||
|
||||
let provider = provider.to_string();
|
||||
let provider_settings = llm.providers.entry(provider.clone()).or_default();
|
||||
if provider_settings.models.contains_key(&model) {
|
||||
return Err(ParseError::ConflictingLlmModelDefinitions { provider, model });
|
||||
}
|
||||
provider_settings.models.insert(model, legacy.into_model());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -289,11 +332,91 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_new_llm_models_subtree() {
|
||||
let parsed = "[llm.models.\"foo\"]\nprovider = \"kimi\"\n"
|
||||
fn accepts_provider_scoped_models_subtree() {
|
||||
let parsed = "[llm.providers.kimi.models.\"foo\"]\ndisplay_name = \"Foo\"\n"
|
||||
.parse::<SettingsLayer>()
|
||||
.unwrap();
|
||||
assert!(parsed.llm.unwrap().models.contains_key("foo"));
|
||||
.expect("provider-scoped model should parse");
|
||||
let llm = parsed.llm.expect("llm layer should be present");
|
||||
|
||||
assert_eq!(
|
||||
llm.providers["kimi"].models["foo"].display_name.as_deref(),
|
||||
Some("Foo")
|
||||
);
|
||||
assert!(llm.models.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_legacy_llm_model_with_provider_into_provider_scope() {
|
||||
let parsed = r#"
|
||||
[llm.models.foo]
|
||||
provider = "kimi"
|
||||
display_name = "Foo"
|
||||
"#
|
||||
.parse::<SettingsLayer>()
|
||||
.expect("legacy model should parse");
|
||||
let llm = parsed.llm.expect("llm layer should be present");
|
||||
|
||||
assert_eq!(
|
||||
llm.providers["kimi"].models["foo"].display_name.as_deref(),
|
||||
Some("Foo")
|
||||
);
|
||||
assert!(llm.models.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retains_providerless_legacy_llm_model_for_catalog_aware_adoption() {
|
||||
let parsed = r#"
|
||||
[llm.models.foo]
|
||||
display_name = "Renamed Foo"
|
||||
"#
|
||||
.parse::<SettingsLayer>()
|
||||
.expect("provider-less legacy model should remain compatible");
|
||||
let llm = parsed.llm.expect("llm layer should be present");
|
||||
|
||||
assert_eq!(
|
||||
llm.models["foo"].display_name.as_deref(),
|
||||
Some("Renamed Foo")
|
||||
);
|
||||
assert!(llm.providers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_same_source_legacy_and_provider_scoped_model_pair() {
|
||||
let error = r#"
|
||||
[llm.providers.kimi.models.foo]
|
||||
display_name = "Canonical Foo"
|
||||
|
||||
[llm.models.foo]
|
||||
provider = "kimi"
|
||||
display_name = "Legacy Foo"
|
||||
"#
|
||||
.parse::<SettingsLayer>()
|
||||
.expect_err("same pair in both syntaxes should be rejected");
|
||||
|
||||
assert_eq!(
|
||||
error,
|
||||
ParseError::ConflictingLlmModelDefinitions {
|
||||
provider: "kimi".to_string(),
|
||||
model: "foo".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_empty_legacy_llm_model_provider_with_typed_error() {
|
||||
let error = r#"
|
||||
[llm.models.foo]
|
||||
provider = ""
|
||||
"#
|
||||
.parse::<SettingsLayer>()
|
||||
.expect_err("empty legacy provider should be rejected");
|
||||
|
||||
assert_eq!(
|
||||
error,
|
||||
ParseError::InvalidLegacyLlmModelProvider {
|
||||
model: "foo".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -318,3 +318,117 @@ bucket = "higher-bucket"
|
|||
assert_eq!(s3.bucket, Some("higher-bucket".to_string()));
|
||||
assert_eq!(s3.region, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_and_model_rows_field_merge_independently() {
|
||||
let lower = parse(
|
||||
r#"
|
||||
[llm.providers.acme]
|
||||
display_name = "Acme"
|
||||
adapter = "openai_compatible"
|
||||
base_url = "https://lower.example/v1"
|
||||
|
||||
[llm.providers.acme.models.large]
|
||||
display_name = "Acme Large"
|
||||
family = "acme"
|
||||
|
||||
[llm.providers.acme.models.large.limits]
|
||||
context_window = 128000
|
||||
max_output = 32000
|
||||
"#,
|
||||
);
|
||||
let higher = parse(
|
||||
r#"
|
||||
[llm.providers.acme]
|
||||
base_url = "https://higher.example/v1"
|
||||
|
||||
[llm.providers.acme.models.large]
|
||||
display_name = "Acme Large v2"
|
||||
|
||||
[llm.providers.acme.models.large.limits]
|
||||
max_output = 64000
|
||||
"#,
|
||||
);
|
||||
|
||||
let merged = higher.combine(lower);
|
||||
let acme = &merged.llm.expect("llm layer should be present").providers["acme"];
|
||||
assert_eq!(acme.display_name.as_deref(), Some("Acme"));
|
||||
assert_eq!(acme.adapter.as_deref(), Some("openai_compatible"));
|
||||
assert_eq!(acme.base_url.as_deref(), Some("https://higher.example/v1"));
|
||||
|
||||
let model = &acme.models["large"];
|
||||
assert_eq!(model.display_name.as_deref(), Some("Acme Large v2"));
|
||||
assert_eq!(model.family.as_deref(), Some("acme"));
|
||||
assert_eq!(
|
||||
model.limits.as_ref().and_then(|limits| limits.context_window),
|
||||
Some(128_000)
|
||||
);
|
||||
assert_eq!(
|
||||
model.limits.as_ref().and_then(|limits| limits.max_output),
|
||||
Some(64_000)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_model_is_normalized_before_cross_source_combine() {
|
||||
let lower = parse(
|
||||
r#"
|
||||
[llm.models.large]
|
||||
provider = "acme"
|
||||
family = "acme"
|
||||
|
||||
[llm.models.large.limits]
|
||||
context_window = 128000
|
||||
"#,
|
||||
);
|
||||
let higher = parse(
|
||||
r#"
|
||||
[llm.providers.acme.models.large]
|
||||
display_name = "Acme Large"
|
||||
|
||||
[llm.providers.acme.models.large.limits]
|
||||
max_output = 64000
|
||||
"#,
|
||||
);
|
||||
|
||||
let merged = higher.combine(lower);
|
||||
let llm = merged.llm.expect("llm layer should be present");
|
||||
let model = &llm.providers["acme"].models["large"];
|
||||
|
||||
assert_eq!(model.display_name.as_deref(), Some("Acme Large"));
|
||||
assert_eq!(model.family.as_deref(), Some("acme"));
|
||||
assert_eq!(
|
||||
model.limits.as_ref().and_then(|limits| limits.context_window),
|
||||
Some(128_000)
|
||||
);
|
||||
assert_eq!(
|
||||
model.limits.as_ref().and_then(|limits| limits.max_output),
|
||||
Some(64_000)
|
||||
);
|
||||
assert!(llm.models.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_model_id_on_different_providers_stays_independent() {
|
||||
let merged = parse(
|
||||
r#"
|
||||
[llm.providers.openai.models.shared]
|
||||
api_id = "shared"
|
||||
|
||||
[llm.providers.openrouter.models.shared]
|
||||
api_id = "openai/shared"
|
||||
"#,
|
||||
);
|
||||
let llm = merged.llm.expect("llm layer should be present");
|
||||
|
||||
assert_eq!(
|
||||
llm.providers["openai"].models["shared"].api_id.as_deref(),
|
||||
Some("shared")
|
||||
);
|
||||
assert_eq!(
|
||||
llm.providers["openrouter"].models["shared"]
|
||||
.api_id
|
||||
.as_deref(),
|
||||
Some("openai/shared")
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -247,19 +247,20 @@ x-team-secret = "{{ secrets.gateway_team_secret }}"
|
|||
| `auth.credentials` | array<string> | required when `auth` present | Ordered credential refs. Accepted forms are `vault:<NAME>`, `env:<NAME>`, and `aws_sigv4` (sign requests from the AWS default credential chain — Bedrock). Literal secret strings are rejected. |
|
||||
| `auth.header` | `"bearer"` or `{ custom = "Header-Name" }` | `"bearer"` | Primary API-key header policy. Omit when the provider uses a standard bearer token. |
|
||||
| `extra_headers` | table | `{}` | Additional headers attached to provider requests. Values are interpolation strings: literal text, an `{{ env.NAME }}` token, or a `{{ secrets.NAME }}` token. Put credentials in a secret and reference them with a `{{ secrets.NAME }}` token, not a bare literal. |
|
||||
| `priority` | integer | `0` | Higher-priority configured providers win default selection; ties use canonical provider ID. |
|
||||
| `priority` | integer | `0` | Higher-priority ready providers win unqualified model selection; ties use canonical provider ID in ascending order. |
|
||||
| `enabled` | boolean | `true` | Set `false` to disable a provider after lower-precedence layers define it. |
|
||||
| `aliases` | array<string> | `[]` | Additional provider names accepted by model routing and fallback config. |
|
||||
|
||||
## `[llm.models.<id>]`
|
||||
## `[llm.providers.<provider>.models.<model>]`
|
||||
|
||||
Define or override a model in the catalog. The table key is the canonical
|
||||
model ID Fabro users reference; `api_id` is the model string sent to the
|
||||
provider API.
|
||||
Define or override one provider-specific model offering. The containing table
|
||||
supplies the provider ID, and `<model>` is the canonical, human-facing model
|
||||
slug used by workflows. The stable identity of an offering is the pair
|
||||
`(provider, model)`; the same model slug and aliases may be reused by other
|
||||
providers.
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.models."team-code-large"]
|
||||
provider = "proxy"
|
||||
[llm.providers.proxy.models."team-code-large"]
|
||||
api_id = "provider-wire-model-name"
|
||||
agent_profile = "anthropic"
|
||||
display_name = "Team Code Large"
|
||||
|
|
@ -270,56 +271,66 @@ enabled = true
|
|||
aliases = ["team-code"]
|
||||
estimated_output_tps = 80
|
||||
|
||||
[llm.models."team-code-large".limits]
|
||||
[llm.providers.proxy.models."team-code-large".limits]
|
||||
context_window = 200000
|
||||
max_output = 32000
|
||||
|
||||
[llm.models."team-code-large".features]
|
||||
[llm.providers.proxy.models."team-code-large".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[llm.models."team-code-large".controls]
|
||||
[llm.providers.proxy.models."team-code-large".controls]
|
||||
reasoning_effort = ["low", "medium", "high"]
|
||||
speed = ["fast"]
|
||||
|
||||
[llm.models."team-code-large".costs]
|
||||
[llm.providers.proxy.models."team-code-large".costs]
|
||||
input_cost_per_mtok = 1.50
|
||||
output_cost_per_mtok = 8.00
|
||||
cache_input_cost_per_mtok = 0.30
|
||||
|
||||
[llm.models."team-code-large".costs.speed.fast]
|
||||
[llm.providers.proxy.models."team-code-large".costs.speed.fast]
|
||||
input_cost_per_mtok = 3.00
|
||||
output_cost_per_mtok = 16.00
|
||||
cache_input_cost_per_mtok = 0.60
|
||||
```
|
||||
|
||||
`api_id` is an opaque provider wire identifier, not a workflow selector or a
|
||||
routing namespace. When omitted, it defaults to the exact canonical model
|
||||
slug. Set it only when the provider expects a different value; an explicitly
|
||||
empty value is invalid.
|
||||
|
||||
Unqualified model selectors consider ready providers and then choose the
|
||||
highest provider `priority`, with canonical provider ID as the deterministic
|
||||
tie-breaker. Supplying a provider pins lookup to that provider. An alias must
|
||||
identify only one model within a provider, but reusing it on another provider
|
||||
is valid and enables portable workflow selectors.
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `provider` | string | None | Provider ID this model belongs to. |
|
||||
| `api_id` | string | model ID | Identifier sent to the provider API. |
|
||||
| `api_id` | string | canonical model slug | Opaque identifier sent to this offering's provider API. It is not parsed for routing. |
|
||||
| `agent_profile` | `"anthropic"` \| `"openai"` \| `"gemini"` | provider profile | Agent profile override for this model. Model overrides take precedence over provider overrides. |
|
||||
| `billing_policy` | `"openai"` \| `"anthropic"` \| `"gemini"` \| `"none"` | provider policy | Billing algorithm override for this model — for models whose billing family differs from their provider's (e.g. Claude served through OpenRouter bills Anthropic-style cache reads/writes). |
|
||||
| `display_name` | string | model ID | Human-readable model name. |
|
||||
| `family` | string | model ID | Family label used for catalog display and matching. |
|
||||
| `display_name` | string | model slug | Human-readable model name. |
|
||||
| `family` | string | model slug | Family metadata used for catalog display and matching; it is not a routing namespace. |
|
||||
| `training` | string | None | Training data cutoff label. |
|
||||
| `knowledge_cutoff` | string or TOML date | None | Public knowledge cutoff label; TOML dates normalize to `YYYY-MM-DD`. |
|
||||
| `default` | boolean | `false` | Whether this is the provider default model. |
|
||||
| `probe` | boolean | `false` | Whether this model should be preferred for provider connectivity probes. Set `false` in a higher-precedence layer to clear an inherited probe marker. |
|
||||
| `enabled` | boolean | `true` | Set `false` to disable a model after lower-precedence layers define it. |
|
||||
| `aliases` | array<string> | `[]` | Additional model names accepted by routing and fallback config. |
|
||||
| `aliases` | array<string> | `[]` | Additional user-facing selectors. Each selector must be unique within this provider but may be reused by other providers. |
|
||||
| `estimated_output_tps` | number | None | Estimated output tokens per second for catalog display and planning. |
|
||||
|
||||
## `[llm.models.<id>.limits]`
|
||||
## `[llm.providers.<provider>.models.<model>.limits]`
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `context_window` | integer | None | Maximum context window size in tokens. |
|
||||
| `max_output` | integer | None | Maximum output tokens, if known. |
|
||||
|
||||
## `[llm.models.<id>.features]`
|
||||
## `[llm.providers.<provider>.models.<model>.features]`
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
|
|
@ -330,14 +341,14 @@ cache_input_cost_per_mtok = 0.60
|
|||
| `prompt_cache` | boolean | `false` | Whether prompt cache pricing/usage applies. |
|
||||
| `sampling_params` | boolean | `true` | Whether the model accepts classic sampling parameters (`temperature`, `top_p`). |
|
||||
|
||||
## `[llm.models.<id>.controls]`
|
||||
## `[llm.providers.<provider>.models.<model>.controls]`
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `reasoning_effort` | array<string> | all standard levels when feature is `"levels"` or `"always_adaptive"` | User-facing reasoning effort values Fabro may send for this model. Can be set explicitly for reasoning models whose provider adapter maps effort to a non-native API shape. |
|
||||
| `speed` | array<string> | `[]` | Additional speeds beyond implicit `standard`; do not list `standard`. |
|
||||
|
||||
## `[llm.models.<id>.costs]`
|
||||
## `[llm.providers.<provider>.models.<model>.costs]`
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
|
|
@ -345,11 +356,12 @@ cache_input_cost_per_mtok = 0.60
|
|||
| `output_cost_per_mtok` | number | None | Output cost in USD per million tokens. |
|
||||
| `cache_input_cost_per_mtok` | number | None | Cached input/read cost in USD per million tokens. |
|
||||
|
||||
## `[llm.models.<id>.costs.speed.<speed>]`
|
||||
## `[llm.providers.<provider>.models.<model>.costs.speed.<speed>]`
|
||||
|
||||
Per-speed cost overrides use the same keys as `[llm.models.<id>.costs]`.
|
||||
Each `<speed>` key must be declared in `[llm.models.<id>.controls].speed`.
|
||||
The `standard` speed is implicit and always uses the base cost table.
|
||||
Per-speed cost overrides use the same keys as
|
||||
`[llm.providers.<provider>.models.<model>.costs]`. Each `<speed>` key must be
|
||||
declared in `[llm.providers.<provider>.models.<model>.controls].speed`. The
|
||||
`standard` speed is implicit and always uses the base cost table.
|
||||
|
||||
"#,
|
||||
);
|
||||
|
|
@ -389,3 +401,21 @@ See [MCP](/agents/mcp) for transport-specific examples.
|
|||
fn normalize_doc(doc: &str) -> String {
|
||||
doc.trim().trim_end_matches('.').to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn llm_catalog_reference_teaches_provider_scoped_portable_models() {
|
||||
let reference = render_options_reference();
|
||||
|
||||
assert!(reference.contains("## `[llm.providers.<provider>.models.<model>]`"));
|
||||
assert!(reference.contains("[llm.providers.proxy.models.\"team-code-large\"]"));
|
||||
assert!(reference.contains("defaults to the exact canonical model\nslug"));
|
||||
assert!(reference.contains("Supplying a provider pins lookup to that provider"));
|
||||
assert!(reference.contains("reusing it on another provider\nis valid"));
|
||||
assert!(reference.contains("opaque provider wire identifier"));
|
||||
assert!(!reference.contains("## `[llm.models.<id>]`"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -597,6 +597,7 @@ fn format_additional_speeds(values: &[Speed]) -> String {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ pub(crate) fn estimate_cost_usd(
|
|||
let catalog = catalog?;
|
||||
// The billing machinery compares ModelRefs against the catalog's
|
||||
// canonical identity, so resolve model aliases and provider names first.
|
||||
let model = catalog.get(model)?;
|
||||
let provider = catalog.provider(&ProviderId::new(provider))?;
|
||||
let model = catalog.model_on_provider(&provider.id, model)?;
|
||||
let model_ref = ModelRef {
|
||||
provider: provider.id.clone(),
|
||||
model_id: model.id.clone(),
|
||||
model_id: model.id.to_string(),
|
||||
speed,
|
||||
};
|
||||
let micros = catalog.price_tokens(&model_ref, tokens)?;
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ fn build_deep_test_params(info: &Model, client: Arc<Client>) -> Option<GenerateP
|
|||
},
|
||||
);
|
||||
|
||||
let mut params = GenerateParams::new(&info.id, client)
|
||||
let mut params = GenerateParams::new(info.id.as_str(), client)
|
||||
.provider(info.provider.to_string())
|
||||
.prompt(
|
||||
"Use the add tool twice: first add 15 and 27, then add that result to 42. \
|
||||
|
|
|
|||
|
|
@ -523,7 +523,7 @@ impl Model {
|
|||
pub fn billing_model_ref(&self, speed: Option<Speed>) -> ModelRef {
|
||||
ModelRef {
|
||||
provider: self.provider.clone(),
|
||||
model_id: self.id.clone(),
|
||||
model_id: self.id.to_string(),
|
||||
speed,
|
||||
}
|
||||
}
|
||||
|
|
@ -544,7 +544,7 @@ fn pricing_for_model_costs(
|
|||
Some(ModelPricing {
|
||||
model: ModelRef {
|
||||
provider: provider_id,
|
||||
model_id: model.id.clone(),
|
||||
model_id: model.id.to_string(),
|
||||
speed,
|
||||
},
|
||||
policy,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -9,18 +9,16 @@ priority = 100
|
|||
credentials = ["env:ANTHROPIC_API_KEY", "vault:ANTHROPIC_API_KEY"]
|
||||
header = { custom = "x-api-key" }
|
||||
|
||||
[models."claude-fable-5"]
|
||||
provider = "anthropic"
|
||||
api_id = "claude-fable-5"
|
||||
[providers.anthropic.models."claude-fable-5"]
|
||||
display_name = "Claude Fable 5"
|
||||
family = "claude-5"
|
||||
aliases = ["fable", "claude-fable"]
|
||||
|
||||
[models."claude-fable-5".limits]
|
||||
[providers.anthropic.models."claude-fable-5".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[models."claude-fable-5".features]
|
||||
[providers.anthropic.models."claude-fable-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
|
|
@ -28,14 +26,12 @@ reasoning_effort = "always_adaptive"
|
|||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[models."claude-fable-5".costs]
|
||||
[providers.anthropic.models."claude-fable-5".costs]
|
||||
input_cost_per_mtok = 10.0
|
||||
output_cost_per_mtok = 50.0
|
||||
cache_input_cost_per_mtok = 1.0
|
||||
|
||||
[models."claude-opus-4-8"]
|
||||
provider = "anthropic"
|
||||
api_id = "claude-opus-4-8"
|
||||
[providers.anthropic.models."claude-opus-4-8"]
|
||||
display_name = "Claude Opus 4.8"
|
||||
family = "claude-4"
|
||||
training = "2026-01-01"
|
||||
|
|
@ -43,11 +39,11 @@ knowledge_cutoff = "Jan 2026"
|
|||
estimated_output_tps = 25
|
||||
aliases = ["opus", "claude-opus"]
|
||||
|
||||
[models."claude-opus-4-8".limits]
|
||||
[providers.anthropic.models."claude-opus-4-8".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[models."claude-opus-4-8".features]
|
||||
[providers.anthropic.models."claude-opus-4-8".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
|
|
@ -55,33 +51,31 @@ reasoning_effort = "levels"
|
|||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[models."claude-opus-4-8".controls]
|
||||
[providers.anthropic.models."claude-opus-4-8".controls]
|
||||
speed = ["fast"]
|
||||
|
||||
[models."claude-opus-4-8".costs]
|
||||
[providers.anthropic.models."claude-opus-4-8".costs]
|
||||
input_cost_per_mtok = 5.0
|
||||
output_cost_per_mtok = 25.0
|
||||
cache_input_cost_per_mtok = 0.5
|
||||
|
||||
[models."claude-opus-4-8".costs.speed.fast]
|
||||
[providers.anthropic.models."claude-opus-4-8".costs.speed.fast]
|
||||
input_cost_per_mtok = 10.0
|
||||
output_cost_per_mtok = 50.0
|
||||
cache_input_cost_per_mtok = 1.0
|
||||
|
||||
[models."claude-opus-4-7"]
|
||||
provider = "anthropic"
|
||||
api_id = "claude-opus-4-7"
|
||||
[providers.anthropic.models."claude-opus-4-7"]
|
||||
display_name = "Claude Opus 4.7"
|
||||
family = "claude-4"
|
||||
training = "2025-08-01"
|
||||
knowledge_cutoff = "May 2025"
|
||||
estimated_output_tps = 25
|
||||
|
||||
[models."claude-opus-4-7".limits]
|
||||
[providers.anthropic.models."claude-opus-4-7".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[models."claude-opus-4-7".features]
|
||||
[providers.anthropic.models."claude-opus-4-7".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
|
|
@ -89,82 +83,76 @@ reasoning_effort = "levels"
|
|||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[models."claude-opus-4-7".controls]
|
||||
[providers.anthropic.models."claude-opus-4-7".controls]
|
||||
speed = ["fast"]
|
||||
|
||||
[models."claude-opus-4-7".costs]
|
||||
[providers.anthropic.models."claude-opus-4-7".costs]
|
||||
input_cost_per_mtok = 5.0
|
||||
output_cost_per_mtok = 25.0
|
||||
cache_input_cost_per_mtok = 0.5
|
||||
|
||||
[models."claude-opus-4-7".costs.speed.fast]
|
||||
[providers.anthropic.models."claude-opus-4-7".costs.speed.fast]
|
||||
input_cost_per_mtok = 30.0
|
||||
output_cost_per_mtok = 150.0
|
||||
cache_input_cost_per_mtok = 3.0
|
||||
|
||||
[models."claude-opus-4-6"]
|
||||
provider = "anthropic"
|
||||
api_id = "claude-opus-4-6"
|
||||
[providers.anthropic.models."claude-opus-4-6"]
|
||||
display_name = "Claude Opus 4.6"
|
||||
family = "claude-4"
|
||||
training = "2025-08-01"
|
||||
knowledge_cutoff = "May 2025"
|
||||
estimated_output_tps = 25
|
||||
|
||||
[models."claude-opus-4-6".limits]
|
||||
[providers.anthropic.models."claude-opus-4-6".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[models."claude-opus-4-6".features]
|
||||
[providers.anthropic.models."claude-opus-4-6".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[models."claude-opus-4-6".controls]
|
||||
[providers.anthropic.models."claude-opus-4-6".controls]
|
||||
speed = ["fast"]
|
||||
|
||||
[models."claude-opus-4-6".costs]
|
||||
[providers.anthropic.models."claude-opus-4-6".costs]
|
||||
input_cost_per_mtok = 5.0
|
||||
output_cost_per_mtok = 25.0
|
||||
cache_input_cost_per_mtok = 0.5
|
||||
|
||||
[models."claude-opus-4-6".costs.speed.fast]
|
||||
[providers.anthropic.models."claude-opus-4-6".costs.speed.fast]
|
||||
input_cost_per_mtok = 30.0
|
||||
output_cost_per_mtok = 150.0
|
||||
cache_input_cost_per_mtok = 3.0
|
||||
|
||||
[models."claude-sonnet-4-5"]
|
||||
provider = "anthropic"
|
||||
api_id = "claude-sonnet-4-5"
|
||||
[providers.anthropic.models."claude-sonnet-4-5"]
|
||||
display_name = "Claude Sonnet 4.5"
|
||||
family = "claude-4"
|
||||
training = "2025-08-01"
|
||||
knowledge_cutoff = "May 2025"
|
||||
estimated_output_tps = 50
|
||||
|
||||
[models."claude-sonnet-4-5".limits]
|
||||
[providers.anthropic.models."claude-sonnet-4-5".limits]
|
||||
context_window = 200000
|
||||
max_output = 64000
|
||||
|
||||
[models."claude-sonnet-4-5".features]
|
||||
[providers.anthropic.models."claude-sonnet-4-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
|
||||
[models."claude-sonnet-4-5".controls]
|
||||
[providers.anthropic.models."claude-sonnet-4-5".controls]
|
||||
reasoning_effort = ["low", "medium", "high", "xhigh", "max"]
|
||||
|
||||
[models."claude-sonnet-4-5".costs]
|
||||
[providers.anthropic.models."claude-sonnet-4-5".costs]
|
||||
input_cost_per_mtok = 3.0
|
||||
output_cost_per_mtok = 15.0
|
||||
cache_input_cost_per_mtok = 0.3
|
||||
|
||||
[models."claude-sonnet-4-6"]
|
||||
provider = "anthropic"
|
||||
api_id = "claude-sonnet-4-6"
|
||||
[providers.anthropic.models."claude-sonnet-4-6"]
|
||||
display_name = "Claude Sonnet 4.6"
|
||||
family = "claude-4"
|
||||
training = "2025-08-01"
|
||||
|
|
@ -173,25 +161,23 @@ default = true
|
|||
estimated_output_tps = 50
|
||||
aliases = ["sonnet", "claude-sonnet"]
|
||||
|
||||
[models."claude-sonnet-4-6".limits]
|
||||
[providers.anthropic.models."claude-sonnet-4-6".limits]
|
||||
context_window = 200000
|
||||
max_output = 64000
|
||||
|
||||
[models."claude-sonnet-4-6".features]
|
||||
[providers.anthropic.models."claude-sonnet-4-6".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[models."claude-sonnet-4-6".costs]
|
||||
[providers.anthropic.models."claude-sonnet-4-6".costs]
|
||||
input_cost_per_mtok = 3.0
|
||||
output_cost_per_mtok = 15.0
|
||||
cache_input_cost_per_mtok = 0.3
|
||||
|
||||
[models."claude-haiku-4-5"]
|
||||
provider = "anthropic"
|
||||
api_id = "claude-haiku-4-5"
|
||||
[providers.anthropic.models."claude-haiku-4-5"]
|
||||
display_name = "Claude Haiku 4.5"
|
||||
family = "claude-4"
|
||||
training = "2025-08-01"
|
||||
|
|
@ -201,17 +187,17 @@ aliases = ["haiku", "claude-haiku"]
|
|||
probe = true
|
||||
small_default = true
|
||||
|
||||
[models."claude-haiku-4-5".limits]
|
||||
[providers.anthropic.models."claude-haiku-4-5".limits]
|
||||
context_window = 200000
|
||||
max_output = 8192
|
||||
|
||||
[models."claude-haiku-4-5".features]
|
||||
[providers.anthropic.models."claude-haiku-4-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
prompt_cache = true
|
||||
|
||||
[models."claude-haiku-4-5".costs]
|
||||
[providers.anthropic.models."claude-haiku-4-5".costs]
|
||||
input_cost_per_mtok = 0.8
|
||||
output_cost_per_mtok = 4.0
|
||||
cache_input_cost_per_mtok = 0.08
|
||||
|
|
|
|||
|
|
@ -35,41 +35,41 @@ credentials = [
|
|||
# [llm.providers.bedrock-openai]
|
||||
# enabled = true
|
||||
|
||||
[models."openai.gpt-5.5"]
|
||||
provider = "bedrock-openai"
|
||||
[providers.bedrock-openai.models."gpt-5.5"]
|
||||
api_id = "openai.gpt-5.5"
|
||||
display_name = "GPT-5.5 (Bedrock)"
|
||||
family = "gpt-5"
|
||||
default = true
|
||||
|
||||
[models."openai.gpt-5.5".limits]
|
||||
[providers.bedrock-openai.models."gpt-5.5".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[models."openai.gpt-5.5".features]
|
||||
[providers.bedrock-openai.models."gpt-5.5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[models."openai.gpt-5.5".costs]
|
||||
[providers.bedrock-openai.models."gpt-5.5".costs]
|
||||
input_cost_per_mtok = 5.5
|
||||
output_cost_per_mtok = 33.0
|
||||
|
||||
[models."openai.gpt-5.4"]
|
||||
provider = "bedrock-openai"
|
||||
[providers.bedrock-openai.models."gpt-5.4"]
|
||||
api_id = "openai.gpt-5.4"
|
||||
display_name = "GPT-5.4 (Bedrock)"
|
||||
family = "gpt-5"
|
||||
|
||||
[models."openai.gpt-5.4".limits]
|
||||
[providers.bedrock-openai.models."gpt-5.4".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[models."openai.gpt-5.4".features]
|
||||
[providers.bedrock-openai.models."gpt-5.4".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[models."openai.gpt-5.4".costs]
|
||||
[providers.bedrock-openai.models."gpt-5.4".costs]
|
||||
input_cost_per_mtok = 2.75
|
||||
output_cost_per_mtok = 16.5
|
||||
|
|
|
|||
|
|
@ -45,68 +45,67 @@ credentials = [
|
|||
# file because its Bedrock deployment pins sampling parameters and requires an
|
||||
# extra data-sharing opt-in.
|
||||
|
||||
[models."us.anthropic.claude-sonnet-4-6"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."claude-sonnet-4-6"]
|
||||
api_id = "us.anthropic.claude-sonnet-4-6"
|
||||
display_name = "Claude Sonnet 4.6 (Bedrock)"
|
||||
family = "claude-4"
|
||||
billing_policy = "anthropic"
|
||||
default = true
|
||||
|
||||
[models."us.anthropic.claude-sonnet-4-6".limits]
|
||||
[providers.bedrock.models."claude-sonnet-4-6".limits]
|
||||
context_window = 1000000
|
||||
max_output = 64000
|
||||
|
||||
[models."us.anthropic.claude-sonnet-4-6".features]
|
||||
[providers.bedrock.models."claude-sonnet-4-6".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
|
||||
[models."us.anthropic.claude-sonnet-4-6".costs]
|
||||
[providers.bedrock.models."claude-sonnet-4-6".costs]
|
||||
input_cost_per_mtok = 3.0
|
||||
output_cost_per_mtok = 15.0
|
||||
cache_input_cost_per_mtok = 0.3
|
||||
|
||||
[models."us.anthropic.claude-opus-4-8"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."claude-opus-4-8"]
|
||||
api_id = "us.anthropic.claude-opus-4-8"
|
||||
display_name = "Claude Opus 4.8 (Bedrock)"
|
||||
family = "claude-4"
|
||||
billing_policy = "anthropic"
|
||||
|
||||
[models."us.anthropic.claude-opus-4-8".limits]
|
||||
[providers.bedrock.models."claude-opus-4-8".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[models."us.anthropic.claude-opus-4-8".features]
|
||||
[providers.bedrock.models."claude-opus-4-8".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
|
||||
[models."us.anthropic.claude-opus-4-8".costs]
|
||||
[providers.bedrock.models."claude-opus-4-8".costs]
|
||||
input_cost_per_mtok = 5.0
|
||||
output_cost_per_mtok = 25.0
|
||||
cache_input_cost_per_mtok = 0.5
|
||||
|
||||
[models."us.anthropic.claude-haiku-4-5"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."claude-haiku-4-5"]
|
||||
api_id = "us.anthropic.claude-haiku-4-5-20251001-v1:0"
|
||||
display_name = "Claude Haiku 4.5 (Bedrock)"
|
||||
family = "claude-4"
|
||||
billing_policy = "anthropic"
|
||||
small_default = true
|
||||
|
||||
[models."us.anthropic.claude-haiku-4-5".limits]
|
||||
[providers.bedrock.models."claude-haiku-4-5".limits]
|
||||
context_window = 200000
|
||||
max_output = 64000
|
||||
|
||||
[models."us.anthropic.claude-haiku-4-5".features]
|
||||
[providers.bedrock.models."claude-haiku-4-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
prompt_cache = true
|
||||
|
||||
[models."us.anthropic.claude-haiku-4-5".costs]
|
||||
[providers.bedrock.models."claude-haiku-4-5".costs]
|
||||
input_cost_per_mtok = 1.0
|
||||
output_cost_per_mtok = 5.0
|
||||
cache_input_cost_per_mtok = 0.1
|
||||
|
|
@ -116,149 +115,142 @@ cache_input_cost_per_mtok = 0.1
|
|||
# GPT-5.5/5.4 are NOT here: on Bedrock they are Responses-API-only on the
|
||||
# bedrock-mantle endpoint (no Converse), a named follow-up route.
|
||||
|
||||
[models."openai.gpt-oss-120b"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."gpt-oss-120b"]
|
||||
api_id = "openai.gpt-oss-120b-1:0"
|
||||
display_name = "GPT-OSS 120B (Bedrock)"
|
||||
family = "gpt-oss"
|
||||
billing_policy = "openai"
|
||||
agent_profile = "openai"
|
||||
|
||||
[models."openai.gpt-oss-120b".limits]
|
||||
[providers.bedrock.models."gpt-oss-120b".limits]
|
||||
context_window = 128000
|
||||
max_output = 16384
|
||||
|
||||
[models."openai.gpt-oss-120b".features]
|
||||
[providers.bedrock.models."gpt-oss-120b".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
|
||||
[models."openai.gpt-oss-120b".costs]
|
||||
[providers.bedrock.models."gpt-oss-120b".costs]
|
||||
input_cost_per_mtok = 0.15
|
||||
output_cost_per_mtok = 0.60
|
||||
|
||||
[models."openai.gpt-oss-20b"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."gpt-oss-20b"]
|
||||
api_id = "openai.gpt-oss-20b-1:0"
|
||||
display_name = "GPT-OSS 20B (Bedrock)"
|
||||
family = "gpt-oss"
|
||||
billing_policy = "openai"
|
||||
agent_profile = "openai"
|
||||
|
||||
[models."openai.gpt-oss-20b".limits]
|
||||
[providers.bedrock.models."gpt-oss-20b".limits]
|
||||
context_window = 128000
|
||||
max_output = 16384
|
||||
|
||||
[models."openai.gpt-oss-20b".features]
|
||||
[providers.bedrock.models."gpt-oss-20b".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
|
||||
[models."openai.gpt-oss-20b".costs]
|
||||
[providers.bedrock.models."gpt-oss-20b".costs]
|
||||
input_cost_per_mtok = 0.07
|
||||
output_cost_per_mtok = 0.30
|
||||
|
||||
# ---------- Amazon Nova ----------
|
||||
|
||||
[models."amazon.nova-2-lite"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."nova-2-lite"]
|
||||
api_id = "global.amazon.nova-2-lite-v1:0"
|
||||
display_name = "Nova 2 Lite (Bedrock)"
|
||||
family = "nova-2"
|
||||
billing_policy = "openai"
|
||||
agent_profile = "openai"
|
||||
|
||||
[models."amazon.nova-2-lite".limits]
|
||||
[providers.bedrock.models."nova-2-lite".limits]
|
||||
context_window = 1000000
|
||||
# Bedrock caps Nova output at 65535 (2^16 - 1); 65536 trips
|
||||
# "maximum tokens exceeds the model limit of 65535" since the prompt handler
|
||||
# defaults max_tokens to max_output.
|
||||
max_output = 65535
|
||||
|
||||
[models."amazon.nova-2-lite".features]
|
||||
[providers.bedrock.models."nova-2-lite".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
|
||||
[models."amazon.nova-2-lite".costs]
|
||||
[providers.bedrock.models."nova-2-lite".costs]
|
||||
input_cost_per_mtok = 0.30
|
||||
output_cost_per_mtok = 2.50
|
||||
|
||||
# ---------- Open-weights ----------
|
||||
|
||||
[models."meta.llama4-maverick"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."llama-4-maverick"]
|
||||
api_id = "us.meta.llama4-maverick-17b-instruct-v1:0"
|
||||
display_name = "Llama 4 Maverick (Bedrock)"
|
||||
family = "llama-4"
|
||||
billing_policy = "openai"
|
||||
agent_profile = "openai"
|
||||
|
||||
[models."meta.llama4-maverick".limits]
|
||||
[providers.bedrock.models."llama-4-maverick".limits]
|
||||
context_window = 1000000
|
||||
max_output = 8192
|
||||
|
||||
[models."meta.llama4-maverick".features]
|
||||
[providers.bedrock.models."llama-4-maverick".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
|
||||
[models."mistral.mistral-large-3"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."mistral-large-3"]
|
||||
api_id = "mistral.mistral-large-3-675b-instruct"
|
||||
display_name = "Mistral Large 3 (Bedrock)"
|
||||
family = "mistral-large"
|
||||
billing_policy = "openai"
|
||||
agent_profile = "openai"
|
||||
|
||||
[models."mistral.mistral-large-3".limits]
|
||||
[providers.bedrock.models."mistral-large-3".limits]
|
||||
context_window = 256000
|
||||
max_output = 32768
|
||||
|
||||
[models."mistral.mistral-large-3".features]
|
||||
[providers.bedrock.models."mistral-large-3".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
|
||||
[models."mistral.mistral-large-3".costs]
|
||||
[providers.bedrock.models."mistral-large-3".costs]
|
||||
input_cost_per_mtok = 0.50
|
||||
output_cost_per_mtok = 1.50
|
||||
|
||||
[models."mistral.devstral-2"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."devstral-2"]
|
||||
api_id = "mistral.devstral-2-123b"
|
||||
display_name = "Devstral 2 (Bedrock)"
|
||||
family = "devstral"
|
||||
billing_policy = "openai"
|
||||
agent_profile = "openai"
|
||||
|
||||
[models."mistral.devstral-2".limits]
|
||||
[providers.bedrock.models."devstral-2".limits]
|
||||
context_window = 256000
|
||||
max_output = 32768
|
||||
|
||||
[models."mistral.devstral-2".features]
|
||||
[providers.bedrock.models."devstral-2".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."deepseek.v3-2"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."deepseek-v3.2"]
|
||||
api_id = "deepseek.v3.2"
|
||||
display_name = "DeepSeek V3.2 (Bedrock)"
|
||||
family = "deepseek-v3"
|
||||
billing_policy = "openai"
|
||||
agent_profile = "openai"
|
||||
|
||||
[models."deepseek.v3-2".limits]
|
||||
[providers.bedrock.models."deepseek-v3.2".limits]
|
||||
context_window = 164000
|
||||
max_output = 8192
|
||||
|
||||
[models."deepseek.v3-2".features]
|
||||
[providers.bedrock.models."deepseek-v3.2".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
|
||||
[models."deepseek.v3-2".costs]
|
||||
[providers.bedrock.models."deepseek-v3.2".costs]
|
||||
input_cost_per_mtok = 0.62
|
||||
output_cost_per_mtok = 1.85
|
||||
|
||||
|
|
@ -267,92 +259,90 @@ output_cost_per_mtok = 1.85
|
|||
# "The provided model identifier is invalid"), so this row needs an explicit
|
||||
# `api_id` confirmed against `aws bedrock list-inference-profiles` before it
|
||||
# ships. Re-add with:
|
||||
# [models."qwen.qwen3-coder-next"]
|
||||
# provider = "bedrock"
|
||||
# [providers.bedrock.models."qwen3-coder-next"]
|
||||
# api_id = "<verified bedrock id>"
|
||||
# display_name = "Qwen3 Coder Next (Bedrock)"
|
||||
# family = "qwen3"
|
||||
# billing_policy = "openai"
|
||||
# agent_profile = "openai"
|
||||
# [models."qwen.qwen3-coder-next".limits]
|
||||
# [providers.bedrock.models."qwen3-coder-next".limits]
|
||||
# context_window = 256000
|
||||
# max_output = 16384
|
||||
# [models."qwen.qwen3-coder-next".features]
|
||||
# [providers.bedrock.models."qwen3-coder-next".features]
|
||||
# tools = true
|
||||
|
||||
[models."moonshotai.kimi-k2.5"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."kimi-k2.5"]
|
||||
api_id = "moonshotai.kimi-k2.5"
|
||||
display_name = "Kimi K2.5 (Bedrock)"
|
||||
family = "kimi-k2"
|
||||
billing_policy = "openai"
|
||||
agent_profile = "openai"
|
||||
|
||||
[models."moonshotai.kimi-k2.5".limits]
|
||||
[providers.bedrock.models."kimi-k2.5".limits]
|
||||
context_window = 262144
|
||||
max_output = 16384
|
||||
|
||||
[models."moonshotai.kimi-k2.5".features]
|
||||
[providers.bedrock.models."kimi-k2.5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
|
||||
[models."moonshotai.kimi-k2.5".costs]
|
||||
[providers.bedrock.models."kimi-k2.5".costs]
|
||||
input_cost_per_mtok = 0.60
|
||||
output_cost_per_mtok = 3.00
|
||||
|
||||
[models."zai.glm-5"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."glm-5"]
|
||||
api_id = "zai.glm-5"
|
||||
display_name = "GLM 5 (Bedrock)"
|
||||
family = "glm"
|
||||
billing_policy = "openai"
|
||||
agent_profile = "openai"
|
||||
|
||||
[models."zai.glm-5".limits]
|
||||
[providers.bedrock.models."glm-5".limits]
|
||||
context_window = 200000
|
||||
max_output = 128000
|
||||
|
||||
[models."zai.glm-5".features]
|
||||
[providers.bedrock.models."glm-5".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."zai.glm-5".costs]
|
||||
[providers.bedrock.models."glm-5".costs]
|
||||
input_cost_per_mtok = 1.00
|
||||
output_cost_per_mtok = 3.20
|
||||
|
||||
[models."minimax.minimax-m2.5"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."minimax-m2.5"]
|
||||
api_id = "minimax.minimax-m2.5"
|
||||
display_name = "MiniMax M2.5 (Bedrock)"
|
||||
family = "minimax-m2"
|
||||
billing_policy = "openai"
|
||||
agent_profile = "openai"
|
||||
|
||||
[models."minimax.minimax-m2.5".limits]
|
||||
[providers.bedrock.models."minimax-m2.5".limits]
|
||||
context_window = 196000
|
||||
max_output = 8192
|
||||
|
||||
[models."minimax.minimax-m2.5".features]
|
||||
[providers.bedrock.models."minimax-m2.5".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."minimax.minimax-m2.5".costs]
|
||||
[providers.bedrock.models."minimax-m2.5".costs]
|
||||
input_cost_per_mtok = 0.30
|
||||
output_cost_per_mtok = 1.20
|
||||
|
||||
[models."nvidia.nemotron-3-super"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."nemotron-3-super"]
|
||||
api_id = "nvidia.nemotron-super-3-120b"
|
||||
display_name = "Nemotron 3 Super (Bedrock)"
|
||||
family = "nemotron-3"
|
||||
billing_policy = "openai"
|
||||
agent_profile = "openai"
|
||||
|
||||
[models."nvidia.nemotron-3-super".limits]
|
||||
[providers.bedrock.models."nemotron-3-super".limits]
|
||||
context_window = 256000
|
||||
max_output = 32768
|
||||
|
||||
[models."nvidia.nemotron-3-super".features]
|
||||
[providers.bedrock.models."nemotron-3-super".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
|
@ -365,24 +355,24 @@ reasoning = false
|
|||
# reasoning_effort stays undeclared here (requests carrying one are
|
||||
# rejected up front rather than silently dropped).
|
||||
|
||||
[models."us.anthropic.claude-fable-5"]
|
||||
provider = "bedrock"
|
||||
[providers.bedrock.models."claude-fable-5"]
|
||||
api_id = "us.anthropic.claude-fable-5"
|
||||
display_name = "Claude Fable 5 (Bedrock)"
|
||||
family = "claude-5"
|
||||
billing_policy = "anthropic"
|
||||
|
||||
[models."us.anthropic.claude-fable-5".limits]
|
||||
[providers.bedrock.models."claude-fable-5".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[models."us.anthropic.claude-fable-5".features]
|
||||
[providers.bedrock.models."claude-fable-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[models."us.anthropic.claude-fable-5".costs]
|
||||
[providers.bedrock.models."claude-fable-5".costs]
|
||||
input_cost_per_mtok = 10.0
|
||||
output_cost_per_mtok = 50.0
|
||||
cache_input_cost_per_mtok = 1.0
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ priority = 80
|
|||
credentials = ["env:GEMINI_API_KEY", "env:GOOGLE_API_KEY", "vault:GEMINI_API_KEY"]
|
||||
header = { custom = "x-goog-api-key" }
|
||||
|
||||
[models."gemini-3.1-pro-preview"]
|
||||
provider = "gemini"
|
||||
api_id = "gemini-3.1-pro-preview"
|
||||
[providers.gemini.models."gemini-3.1-pro-preview"]
|
||||
display_name = "Gemini 3.1 Pro (Preview)"
|
||||
family = "gemini-3"
|
||||
training = "2025-01-01"
|
||||
|
|
@ -19,24 +17,22 @@ knowledge_cutoff = "January 2025"
|
|||
estimated_output_tps = 85
|
||||
aliases = ["gemini-pro"]
|
||||
|
||||
[models."gemini-3.1-pro-preview".limits]
|
||||
[providers.gemini.models."gemini-3.1-pro-preview".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[models."gemini-3.1-pro-preview".features]
|
||||
[providers.gemini.models."gemini-3.1-pro-preview".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[models."gemini-3.1-pro-preview".costs]
|
||||
[providers.gemini.models."gemini-3.1-pro-preview".costs]
|
||||
input_cost_per_mtok = 2.0
|
||||
output_cost_per_mtok = 12.0
|
||||
cache_input_cost_per_mtok = 0.5
|
||||
|
||||
[models."gemini-3.1-pro-preview-customtools"]
|
||||
provider = "gemini"
|
||||
api_id = "gemini-3.1-pro-preview-customtools"
|
||||
[providers.gemini.models."gemini-3.1-pro-preview-customtools"]
|
||||
display_name = "Gemini 3.1 Pro Custom Tools (Preview)"
|
||||
family = "gemini-3"
|
||||
training = "2025-01-01"
|
||||
|
|
@ -44,24 +40,22 @@ knowledge_cutoff = "January 2025"
|
|||
estimated_output_tps = 85
|
||||
aliases = ["gemini-customtools"]
|
||||
|
||||
[models."gemini-3.1-pro-preview-customtools".limits]
|
||||
[providers.gemini.models."gemini-3.1-pro-preview-customtools".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[models."gemini-3.1-pro-preview-customtools".features]
|
||||
[providers.gemini.models."gemini-3.1-pro-preview-customtools".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[models."gemini-3.1-pro-preview-customtools".costs]
|
||||
[providers.gemini.models."gemini-3.1-pro-preview-customtools".costs]
|
||||
input_cost_per_mtok = 2.0
|
||||
output_cost_per_mtok = 12.0
|
||||
cache_input_cost_per_mtok = 0.5
|
||||
|
||||
[models."gemini-3.5-flash"]
|
||||
provider = "gemini"
|
||||
api_id = "gemini-3.5-flash"
|
||||
[providers.gemini.models."gemini-3.5-flash"]
|
||||
display_name = "Gemini 3.5 Flash"
|
||||
family = "gemini-3"
|
||||
training = "2025-01-01"
|
||||
|
|
@ -70,24 +64,22 @@ default = true
|
|||
estimated_output_tps = 150
|
||||
aliases = ["gemini-35-flash"]
|
||||
|
||||
[models."gemini-3.5-flash".limits]
|
||||
[providers.gemini.models."gemini-3.5-flash".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[models."gemini-3.5-flash".features]
|
||||
[providers.gemini.models."gemini-3.5-flash".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[models."gemini-3.5-flash".costs]
|
||||
[providers.gemini.models."gemini-3.5-flash".costs]
|
||||
input_cost_per_mtok = 1.5
|
||||
output_cost_per_mtok = 9.0
|
||||
cache_input_cost_per_mtok = 0.15
|
||||
|
||||
[models."gemini-3-flash-preview"]
|
||||
provider = "gemini"
|
||||
api_id = "gemini-3-flash-preview"
|
||||
[providers.gemini.models."gemini-3-flash-preview"]
|
||||
display_name = "Gemini 3 Flash (Preview)"
|
||||
family = "gemini-3"
|
||||
training = "2025-01-01"
|
||||
|
|
@ -95,24 +87,22 @@ knowledge_cutoff = "January 2025"
|
|||
estimated_output_tps = 150
|
||||
aliases = ["gemini-flash"]
|
||||
|
||||
[models."gemini-3-flash-preview".limits]
|
||||
[providers.gemini.models."gemini-3-flash-preview".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[models."gemini-3-flash-preview".features]
|
||||
[providers.gemini.models."gemini-3-flash-preview".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[models."gemini-3-flash-preview".costs]
|
||||
[providers.gemini.models."gemini-3-flash-preview".costs]
|
||||
input_cost_per_mtok = 0.5
|
||||
output_cost_per_mtok = 3.0
|
||||
cache_input_cost_per_mtok = 0.125
|
||||
|
||||
[models."gemini-3.1-flash-lite"]
|
||||
provider = "gemini"
|
||||
api_id = "gemini-3.1-flash-lite"
|
||||
[providers.gemini.models."gemini-3.1-flash-lite"]
|
||||
display_name = "Gemini 3.1 Flash Lite"
|
||||
family = "gemini-3"
|
||||
training = "2025-01-01"
|
||||
|
|
@ -121,17 +111,17 @@ estimated_output_tps = 200
|
|||
aliases = ["gemini-flash-lite", "gemini-3.1-flash-lite-preview"]
|
||||
small_default = true
|
||||
|
||||
[models."gemini-3.1-flash-lite".limits]
|
||||
[providers.gemini.models."gemini-3.1-flash-lite".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[models."gemini-3.1-flash-lite".features]
|
||||
[providers.gemini.models."gemini-3.1-flash-lite".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[models."gemini-3.1-flash-lite".costs]
|
||||
[providers.gemini.models."gemini-3.1-flash-lite".costs]
|
||||
input_cost_per_mtok = 0.25
|
||||
output_cost_per_mtok = 1.5
|
||||
cache_input_cost_per_mtok = 0.025
|
||||
|
|
|
|||
|
|
@ -8,25 +8,23 @@ priority = 40
|
|||
[providers.inception.auth]
|
||||
credentials = ["env:INCEPTION_API_KEY", "vault:INCEPTION_API_KEY"]
|
||||
|
||||
[models."mercury-2"]
|
||||
provider = "inception"
|
||||
api_id = "mercury-2"
|
||||
[providers.inception.models."mercury-2"]
|
||||
display_name = "Mercury 2"
|
||||
family = "mercury"
|
||||
default = true
|
||||
estimated_output_tps = 1000
|
||||
aliases = ["mercury"]
|
||||
|
||||
[models."mercury-2".limits]
|
||||
[providers.inception.models."mercury-2".limits]
|
||||
context_window = 131072
|
||||
max_output = 50000
|
||||
|
||||
[models."mercury-2".features]
|
||||
[providers.inception.models."mercury-2".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[models."mercury-2".costs]
|
||||
[providers.inception.models."mercury-2".costs]
|
||||
input_cost_per_mtok = 0.25
|
||||
output_cost_per_mtok = 0.75
|
||||
|
|
|
|||
|
|
@ -8,46 +8,42 @@ priority = 70
|
|||
[providers.kimi.auth]
|
||||
credentials = ["env:KIMI_API_KEY", "vault:KIMI_API_KEY"]
|
||||
|
||||
[models."kimi-k2.5"]
|
||||
provider = "kimi"
|
||||
api_id = "kimi-k2.5"
|
||||
[providers.kimi.models."kimi-k2.5"]
|
||||
display_name = "Kimi K2.5"
|
||||
family = "kimi-k2"
|
||||
training = "2025-10-01"
|
||||
knowledge_cutoff = "October 2025"
|
||||
estimated_output_tps = 50
|
||||
|
||||
[models."kimi-k2.5".limits]
|
||||
[providers.kimi.models."kimi-k2.5".limits]
|
||||
context_window = 262144
|
||||
max_output = 32768
|
||||
|
||||
[models."kimi-k2.5".features]
|
||||
[providers.kimi.models."kimi-k2.5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[models."kimi-k2.5".costs]
|
||||
[providers.kimi.models."kimi-k2.5".costs]
|
||||
input_cost_per_mtok = 0.6
|
||||
output_cost_per_mtok = 3.0
|
||||
cache_input_cost_per_mtok = 0.1
|
||||
|
||||
[models."kimi-k3"]
|
||||
provider = "kimi"
|
||||
api_id = "kimi-k3"
|
||||
[providers.kimi.models."kimi-k3"]
|
||||
display_name = "Kimi K3"
|
||||
family = "kimi-k3"
|
||||
default = true
|
||||
aliases = ["kimi"]
|
||||
|
||||
[models."kimi-k3".limits]
|
||||
[providers.kimi.models."kimi-k3".limits]
|
||||
context_window = 1048576
|
||||
# K3 accepts explicit completion budgets up to 1048576, but Fabro also uses
|
||||
# max_output as the default request budget. Match Kimi's 131072-token default.
|
||||
max_output = 131072
|
||||
|
||||
[models."kimi-k3".features]
|
||||
[providers.kimi.models."kimi-k3".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
|
|
@ -55,10 +51,10 @@ reasoning_effort = "always_adaptive"
|
|||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[models."kimi-k3".controls]
|
||||
[providers.kimi.models."kimi-k3".controls]
|
||||
reasoning_effort = ["low", "high", "max"]
|
||||
|
||||
[models."kimi-k3".costs]
|
||||
[providers.kimi.models."kimi-k3".costs]
|
||||
input_cost_per_mtok = 3.0
|
||||
output_cost_per_mtok = 15.0
|
||||
cache_input_cost_per_mtok = 0.3
|
||||
|
|
|
|||
|
|
@ -14,18 +14,17 @@ credentials = ["env:LITELLM_API_KEY", "vault:LITELLM_API_KEY"]
|
|||
# enabled = true
|
||||
# base_url = "http://localhost:4000/v1"
|
||||
#
|
||||
# [llm.models."litellm-gpt-5"]
|
||||
# provider = "litellm"
|
||||
# [llm.providers.litellm.models."litellm-gpt-5"]
|
||||
# api_id = "gpt-5"
|
||||
# display_name = "LiteLLM GPT-5"
|
||||
# family = "litellm"
|
||||
# default = true
|
||||
#
|
||||
# [llm.models."litellm-gpt-5".limits]
|
||||
# [llm.providers.litellm.models."litellm-gpt-5".limits]
|
||||
# context_window = 128000
|
||||
# max_output = 8192
|
||||
#
|
||||
# [llm.models."litellm-gpt-5".features]
|
||||
# [llm.providers.litellm.models."litellm-gpt-5".features]
|
||||
# tools = true
|
||||
# vision = false
|
||||
# reasoning = false
|
||||
|
|
|
|||
|
|
@ -8,24 +8,22 @@ priority = 50
|
|||
[providers.minimax.auth]
|
||||
credentials = ["env:MINIMAX_API_KEY", "vault:MINIMAX_API_KEY"]
|
||||
|
||||
[models."minimax-m2.5"]
|
||||
provider = "minimax"
|
||||
api_id = "minimax-m2.5"
|
||||
[providers.minimax.models."minimax-m2.5"]
|
||||
display_name = "Minimax M2.5"
|
||||
family = "minimax-m2"
|
||||
default = true
|
||||
estimated_output_tps = 45
|
||||
aliases = ["minimax"]
|
||||
|
||||
[models."minimax-m2.5".limits]
|
||||
[providers.minimax.models."minimax-m2.5".limits]
|
||||
context_window = 196608
|
||||
max_output = 16384
|
||||
|
||||
[models."minimax-m2.5".features]
|
||||
[providers.minimax.models."minimax-m2.5".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."minimax-m2.5".costs]
|
||||
[providers.minimax.models."minimax-m2.5".costs]
|
||||
input_cost_per_mtok = 0.3
|
||||
output_cost_per_mtok = 1.2
|
||||
|
|
|
|||
|
|
@ -9,18 +9,17 @@ enabled = false
|
|||
# Example model. Uncomment after `ollama pull qwen3.5` (and `enabled = true`
|
||||
# above) to expose it through the OpenAI-compatible adapter.
|
||||
#
|
||||
# [models."qwen3.5"]
|
||||
# provider = "ollama"
|
||||
# [providers.ollama.models."qwen3.5"]
|
||||
# api_id = "qwen3.5:latest"
|
||||
# display_name = "Qwen3.5"
|
||||
# family = "qwen3.5"
|
||||
# default = true
|
||||
# aliases = ["ollama-qwen3.5"]
|
||||
#
|
||||
# [models."qwen3.5".limits]
|
||||
# [providers.ollama.models."qwen3.5".limits]
|
||||
# context_window = 32768
|
||||
#
|
||||
# [models."qwen3.5".features]
|
||||
# [providers.ollama.models."qwen3.5".features]
|
||||
# tools = true
|
||||
# vision = false
|
||||
# reasoning = false
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ priority = 90
|
|||
[providers.openai.auth]
|
||||
credentials = ["env:OPENAI_API_KEY", "vault:OPENAI_API_KEY", "vault:OPENAI_CODEX"]
|
||||
|
||||
[models."gpt-5.6-sol"]
|
||||
provider = "openai"
|
||||
api_id = "gpt-5.6-sol"
|
||||
[providers.openai.models."gpt-5.6-sol"]
|
||||
display_name = "GPT-5.6 Sol"
|
||||
family = "gpt-5"
|
||||
training = "2026-02-16"
|
||||
|
|
@ -18,75 +16,69 @@ knowledge_cutoff = "February 16, 2026"
|
|||
default = true
|
||||
aliases = ["gpt56-sol", "gpt-56-sol", "gpt-5.6", "gpt56", "gpt-56"]
|
||||
|
||||
[models."gpt-5.6-sol".limits]
|
||||
[providers.openai.models."gpt-5.6-sol".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[models."gpt-5.6-sol".features]
|
||||
[providers.openai.models."gpt-5.6-sol".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[models."gpt-5.6-sol".costs]
|
||||
[providers.openai.models."gpt-5.6-sol".costs]
|
||||
input_cost_per_mtok = 5.0
|
||||
output_cost_per_mtok = 30.0
|
||||
cache_input_cost_per_mtok = 0.5
|
||||
|
||||
[models."gpt-5.6-terra"]
|
||||
provider = "openai"
|
||||
api_id = "gpt-5.6-terra"
|
||||
[providers.openai.models."gpt-5.6-terra"]
|
||||
display_name = "GPT-5.6 Terra"
|
||||
family = "gpt-5"
|
||||
training = "2026-02-16"
|
||||
knowledge_cutoff = "February 16, 2026"
|
||||
aliases = ["gpt56-terra", "gpt-56-terra"]
|
||||
|
||||
[models."gpt-5.6-terra".limits]
|
||||
[providers.openai.models."gpt-5.6-terra".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[models."gpt-5.6-terra".features]
|
||||
[providers.openai.models."gpt-5.6-terra".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[models."gpt-5.6-terra".costs]
|
||||
[providers.openai.models."gpt-5.6-terra".costs]
|
||||
input_cost_per_mtok = 2.5
|
||||
output_cost_per_mtok = 15.0
|
||||
cache_input_cost_per_mtok = 0.25
|
||||
|
||||
[models."gpt-5.6-luna"]
|
||||
provider = "openai"
|
||||
api_id = "gpt-5.6-luna"
|
||||
[providers.openai.models."gpt-5.6-luna"]
|
||||
display_name = "GPT-5.6 Luna"
|
||||
family = "gpt-5"
|
||||
training = "2026-02-16"
|
||||
knowledge_cutoff = "February 16, 2026"
|
||||
aliases = ["gpt56-luna", "gpt-56-luna"]
|
||||
|
||||
[models."gpt-5.6-luna".limits]
|
||||
[providers.openai.models."gpt-5.6-luna".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[models."gpt-5.6-luna".features]
|
||||
[providers.openai.models."gpt-5.6-luna".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[models."gpt-5.6-luna".costs]
|
||||
[providers.openai.models."gpt-5.6-luna".costs]
|
||||
input_cost_per_mtok = 1.0
|
||||
output_cost_per_mtok = 6.0
|
||||
cache_input_cost_per_mtok = 0.1
|
||||
|
||||
[models."gpt-5.4"]
|
||||
provider = "openai"
|
||||
api_id = "gpt-5.4"
|
||||
[providers.openai.models."gpt-5.4"]
|
||||
display_name = "GPT-5.4"
|
||||
family = "gpt-5"
|
||||
training = "2025-08-31"
|
||||
|
|
@ -94,24 +86,22 @@ knowledge_cutoff = "April 2025"
|
|||
estimated_output_tps = 70
|
||||
aliases = ["gpt54", "gpt-54", "gpt-5.2", "gpt5", "gpt-5.3-codex", "codex"]
|
||||
|
||||
[models."gpt-5.4".limits]
|
||||
[providers.openai.models."gpt-5.4".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[models."gpt-5.4".features]
|
||||
[providers.openai.models."gpt-5.4".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[models."gpt-5.4".costs]
|
||||
[providers.openai.models."gpt-5.4".costs]
|
||||
input_cost_per_mtok = 2.5
|
||||
output_cost_per_mtok = 15.0
|
||||
cache_input_cost_per_mtok = 0.25
|
||||
|
||||
[models."gpt-5.5"]
|
||||
provider = "openai"
|
||||
api_id = "gpt-5.5"
|
||||
[providers.openai.models."gpt-5.5"]
|
||||
display_name = "GPT-5.5"
|
||||
family = "gpt-5"
|
||||
training = "2025-12-01"
|
||||
|
|
@ -119,24 +109,22 @@ knowledge_cutoff = "December 2025"
|
|||
estimated_output_tps = 70
|
||||
aliases = ["gpt55", "gpt-55"]
|
||||
|
||||
[models."gpt-5.5".limits]
|
||||
[providers.openai.models."gpt-5.5".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[models."gpt-5.5".features]
|
||||
[providers.openai.models."gpt-5.5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[models."gpt-5.5".costs]
|
||||
[providers.openai.models."gpt-5.5".costs]
|
||||
input_cost_per_mtok = 5.0
|
||||
output_cost_per_mtok = 30.0
|
||||
cache_input_cost_per_mtok = 0.5
|
||||
|
||||
[models."gpt-5.5-pro"]
|
||||
provider = "openai"
|
||||
api_id = "gpt-5.5-pro"
|
||||
[providers.openai.models."gpt-5.5-pro"]
|
||||
display_name = "GPT-5.5 Pro"
|
||||
family = "gpt-5"
|
||||
training = "2025-12-01"
|
||||
|
|
@ -144,24 +132,22 @@ knowledge_cutoff = "December 2025"
|
|||
estimated_output_tps = 20
|
||||
aliases = ["gpt55-pro", "gpt-55-pro"]
|
||||
|
||||
[models."gpt-5.5-pro".limits]
|
||||
[providers.openai.models."gpt-5.5-pro".limits]
|
||||
context_window = 1050000
|
||||
max_output = 128000
|
||||
|
||||
[models."gpt-5.5-pro".features]
|
||||
[providers.openai.models."gpt-5.5-pro".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[models."gpt-5.5-pro".costs]
|
||||
[providers.openai.models."gpt-5.5-pro".costs]
|
||||
input_cost_per_mtok = 30.0
|
||||
output_cost_per_mtok = 180.0
|
||||
cache_input_cost_per_mtok = 3.0
|
||||
|
||||
[models."gpt-5.4-pro"]
|
||||
provider = "openai"
|
||||
api_id = "gpt-5.4-pro"
|
||||
[providers.openai.models."gpt-5.4-pro"]
|
||||
display_name = "GPT-5.4 Pro"
|
||||
family = "gpt-5"
|
||||
training = "2025-08-31"
|
||||
|
|
@ -169,24 +155,22 @@ knowledge_cutoff = "April 2025"
|
|||
estimated_output_tps = 20
|
||||
aliases = ["gpt54-pro", "gpt-54-pro"]
|
||||
|
||||
[models."gpt-5.4-pro".limits]
|
||||
[providers.openai.models."gpt-5.4-pro".limits]
|
||||
context_window = 1047576
|
||||
max_output = 128000
|
||||
|
||||
[models."gpt-5.4-pro".features]
|
||||
[providers.openai.models."gpt-5.4-pro".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[models."gpt-5.4-pro".costs]
|
||||
[providers.openai.models."gpt-5.4-pro".costs]
|
||||
input_cost_per_mtok = 30.0
|
||||
output_cost_per_mtok = 180.0
|
||||
cache_input_cost_per_mtok = 3.0
|
||||
|
||||
[models."gpt-5.4-mini"]
|
||||
provider = "openai"
|
||||
api_id = "gpt-5.4-mini"
|
||||
[providers.openai.models."gpt-5.4-mini"]
|
||||
display_name = "GPT-5.4 Mini"
|
||||
family = "gpt-5"
|
||||
training = "2025-08-31"
|
||||
|
|
@ -196,17 +180,17 @@ aliases = ["gpt54-mini", "gpt-54-mini", "gpt-5.3-codex-spark", "codex-spark"]
|
|||
probe = true
|
||||
small_default = true
|
||||
|
||||
[models."gpt-5.4-mini".limits]
|
||||
[providers.openai.models."gpt-5.4-mini".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[models."gpt-5.4-mini".features]
|
||||
[providers.openai.models."gpt-5.4-mini".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[models."gpt-5.4-mini".costs]
|
||||
[providers.openai.models."gpt-5.4-mini".costs]
|
||||
input_cost_per_mtok = 0.75
|
||||
output_cost_per_mtok = 4.5
|
||||
cache_input_cost_per_mtok = 0.075
|
||||
|
|
|
|||
|
|
@ -33,262 +33,249 @@ credentials = ["env:OPENROUTER_API_KEY", "vault:OPENROUTER_API_KEY"]
|
|||
# best-effort estimates; OpenRouter returns the authoritative usage.cost
|
||||
# in-band on every response.
|
||||
|
||||
[models."anthropic/claude-opus-4-7"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."claude-opus-4-7"]
|
||||
api_id = "anthropic/claude-opus-4.7"
|
||||
display_name = "Claude Opus 4.7 (via OpenRouter)"
|
||||
family = "claude-4"
|
||||
billing_policy = "anthropic"
|
||||
|
||||
[models."anthropic/claude-opus-4-7".limits]
|
||||
[providers.openrouter.models."claude-opus-4-7".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[models."anthropic/claude-opus-4-7".features]
|
||||
[providers.openrouter.models."claude-opus-4-7".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
|
||||
[models."anthropic/claude-opus-4-7".costs]
|
||||
[providers.openrouter.models."claude-opus-4-7".costs]
|
||||
input_cost_per_mtok = 5.0
|
||||
output_cost_per_mtok = 25.0
|
||||
cache_input_cost_per_mtok = 0.5
|
||||
|
||||
[models."anthropic/claude-sonnet-4-6"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."claude-sonnet-4-6"]
|
||||
api_id = "anthropic/claude-sonnet-4.6"
|
||||
display_name = "Claude Sonnet 4.6 (via OpenRouter)"
|
||||
family = "claude-4"
|
||||
billing_policy = "anthropic"
|
||||
default = true
|
||||
|
||||
[models."anthropic/claude-sonnet-4-6".limits]
|
||||
[providers.openrouter.models."claude-sonnet-4-6".limits]
|
||||
context_window = 1000000
|
||||
max_output = 64000
|
||||
|
||||
[models."anthropic/claude-sonnet-4-6".features]
|
||||
[providers.openrouter.models."claude-sonnet-4-6".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
|
||||
[models."anthropic/claude-sonnet-4-6".costs]
|
||||
[providers.openrouter.models."claude-sonnet-4-6".costs]
|
||||
input_cost_per_mtok = 3.0
|
||||
output_cost_per_mtok = 15.0
|
||||
cache_input_cost_per_mtok = 0.3
|
||||
|
||||
[models."anthropic/claude-haiku-4-5"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."claude-haiku-4-5"]
|
||||
api_id = "anthropic/claude-haiku-4.5"
|
||||
display_name = "Claude Haiku 4.5 (via OpenRouter)"
|
||||
family = "claude-4"
|
||||
billing_policy = "anthropic"
|
||||
small_default = true
|
||||
|
||||
[models."anthropic/claude-haiku-4-5".limits]
|
||||
[providers.openrouter.models."claude-haiku-4-5".limits]
|
||||
context_window = 200000
|
||||
max_output = 8192
|
||||
|
||||
[models."anthropic/claude-haiku-4-5".features]
|
||||
[providers.openrouter.models."claude-haiku-4-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
prompt_cache = true
|
||||
|
||||
[models."anthropic/claude-haiku-4-5".costs]
|
||||
[providers.openrouter.models."claude-haiku-4-5".costs]
|
||||
input_cost_per_mtok = 1.0
|
||||
output_cost_per_mtok = 5.0
|
||||
cache_input_cost_per_mtok = 0.1
|
||||
|
||||
# ---------- OpenAI via OpenRouter ----------
|
||||
|
||||
[models."openai/gpt-5.4"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."gpt-5.4"]
|
||||
api_id = "openai/gpt-5.4"
|
||||
display_name = "GPT-5.4 (via OpenRouter)"
|
||||
family = "gpt-5"
|
||||
|
||||
[models."openai/gpt-5.4".limits]
|
||||
[providers.openrouter.models."gpt-5.4".limits]
|
||||
context_window = 1050000
|
||||
max_output = 32768
|
||||
|
||||
[models."openai/gpt-5.4".features]
|
||||
[providers.openrouter.models."gpt-5.4".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
|
||||
[models."openai/gpt-5.4".costs]
|
||||
[providers.openrouter.models."gpt-5.4".costs]
|
||||
input_cost_per_mtok = 2.5
|
||||
output_cost_per_mtok = 15.0
|
||||
|
||||
[models."openai/gpt-5.5"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."gpt-5.5"]
|
||||
api_id = "openai/gpt-5.5"
|
||||
display_name = "GPT-5.5 (via OpenRouter)"
|
||||
family = "gpt-5"
|
||||
|
||||
[models."openai/gpt-5.5".limits]
|
||||
[providers.openrouter.models."gpt-5.5".limits]
|
||||
context_window = 1050000
|
||||
max_output = 32768
|
||||
|
||||
[models."openai/gpt-5.5".features]
|
||||
[providers.openrouter.models."gpt-5.5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
|
||||
[models."openai/gpt-5.5".costs]
|
||||
[providers.openrouter.models."gpt-5.5".costs]
|
||||
input_cost_per_mtok = 5.0
|
||||
output_cost_per_mtok = 30.0
|
||||
|
||||
# ---------- Google Gemini via OpenRouter ----------
|
||||
|
||||
[models."google/gemini-3.1-pro-preview"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."gemini-3.1-pro-preview"]
|
||||
api_id = "google/gemini-3.1-pro-preview"
|
||||
display_name = "Gemini 3.1 Pro Preview (via OpenRouter)"
|
||||
family = "gemini-3"
|
||||
|
||||
[models."google/gemini-3.1-pro-preview".limits]
|
||||
[providers.openrouter.models."gemini-3.1-pro-preview".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[models."google/gemini-3.1-pro-preview".features]
|
||||
[providers.openrouter.models."gemini-3.1-pro-preview".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
|
||||
[models."google/gemini-3.1-pro-preview".costs]
|
||||
[providers.openrouter.models."gemini-3.1-pro-preview".costs]
|
||||
input_cost_per_mtok = 2.0
|
||||
output_cost_per_mtok = 12.0
|
||||
|
||||
[models."google/gemini-3.5-flash"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."gemini-3.5-flash"]
|
||||
api_id = "google/gemini-3.5-flash"
|
||||
display_name = "Gemini 3.5 Flash (via OpenRouter)"
|
||||
family = "gemini-3"
|
||||
|
||||
[models."google/gemini-3.5-flash".limits]
|
||||
[providers.openrouter.models."gemini-3.5-flash".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[models."google/gemini-3.5-flash".features]
|
||||
[providers.openrouter.models."gemini-3.5-flash".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
|
||||
[models."google/gemini-3.5-flash".costs]
|
||||
[providers.openrouter.models."gemini-3.5-flash".costs]
|
||||
input_cost_per_mtok = 1.5
|
||||
output_cost_per_mtok = 9.0
|
||||
|
||||
# ---------- Open-weights models ----------
|
||||
|
||||
[models."xiaomi/mimo-v2.5-pro"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."mimo-v2.5-pro"]
|
||||
api_id = "xiaomi/mimo-v2.5-pro"
|
||||
display_name = "Xiaomi MiMo v2.5 Pro"
|
||||
family = "mimo-v2"
|
||||
|
||||
[models."xiaomi/mimo-v2.5-pro".limits]
|
||||
[providers.openrouter.models."mimo-v2.5-pro".limits]
|
||||
context_window = 1050000
|
||||
max_output = 16384
|
||||
|
||||
[models."xiaomi/mimo-v2.5-pro".features]
|
||||
[providers.openrouter.models."mimo-v2.5-pro".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."xiaomi/mimo-v2.5-pro".costs]
|
||||
[providers.openrouter.models."mimo-v2.5-pro".costs]
|
||||
input_cost_per_mtok = 0.435
|
||||
output_cost_per_mtok = 0.87
|
||||
|
||||
[models."minimax/minimax-m2.7"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."minimax-m2.7"]
|
||||
api_id = "minimax/minimax-m2.7"
|
||||
display_name = "MiniMax M2.7"
|
||||
family = "minimax-m2"
|
||||
|
||||
[models."minimax/minimax-m2.7".limits]
|
||||
[providers.openrouter.models."minimax-m2.7".limits]
|
||||
context_window = 200000
|
||||
max_output = 16384
|
||||
|
||||
[models."minimax/minimax-m2.7".features]
|
||||
[providers.openrouter.models."minimax-m2.7".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."minimax/minimax-m2.7".costs]
|
||||
[providers.openrouter.models."minimax-m2.7".costs]
|
||||
input_cost_per_mtok = 0.28
|
||||
output_cost_per_mtok = 1.20
|
||||
|
||||
[models."deepseek/deepseek-v4-pro"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."deepseek-v4-pro"]
|
||||
api_id = "deepseek/deepseek-v4-pro"
|
||||
display_name = "DeepSeek V4 Pro"
|
||||
family = "deepseek-v4"
|
||||
|
||||
[models."deepseek/deepseek-v4-pro".limits]
|
||||
[providers.openrouter.models."deepseek-v4-pro".limits]
|
||||
context_window = 1050000
|
||||
max_output = 16384
|
||||
|
||||
[models."deepseek/deepseek-v4-pro".features]
|
||||
[providers.openrouter.models."deepseek-v4-pro".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
|
||||
[models."deepseek/deepseek-v4-pro".costs]
|
||||
[providers.openrouter.models."deepseek-v4-pro".costs]
|
||||
input_cost_per_mtok = 0.435
|
||||
output_cost_per_mtok = 0.87
|
||||
|
||||
[models."deepseek/deepseek-v4-flash"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."deepseek-v4-flash"]
|
||||
api_id = "deepseek/deepseek-v4-flash"
|
||||
display_name = "DeepSeek V4 Flash"
|
||||
family = "deepseek-v4"
|
||||
|
||||
[models."deepseek/deepseek-v4-flash".limits]
|
||||
[providers.openrouter.models."deepseek-v4-flash".limits]
|
||||
context_window = 1050000
|
||||
max_output = 16384
|
||||
|
||||
[models."deepseek/deepseek-v4-flash".features]
|
||||
[providers.openrouter.models."deepseek-v4-flash".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."deepseek/deepseek-v4-flash".costs]
|
||||
[providers.openrouter.models."deepseek-v4-flash".costs]
|
||||
input_cost_per_mtok = 0.10
|
||||
output_cost_per_mtok = 0.20
|
||||
|
||||
[models."moonshotai/kimi-k2.6"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."kimi-k2.6"]
|
||||
api_id = "moonshotai/kimi-k2.6"
|
||||
display_name = "Kimi K2.6"
|
||||
family = "kimi-k2"
|
||||
|
||||
[models."moonshotai/kimi-k2.6".limits]
|
||||
[providers.openrouter.models."kimi-k2.6".limits]
|
||||
context_window = 262144
|
||||
max_output = 16384
|
||||
|
||||
[models."moonshotai/kimi-k2.6".features]
|
||||
[providers.openrouter.models."kimi-k2.6".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."moonshotai/kimi-k2.6".costs]
|
||||
[providers.openrouter.models."kimi-k2.6".costs]
|
||||
input_cost_per_mtok = 0.73
|
||||
output_cost_per_mtok = 3.49
|
||||
|
||||
[models."moonshotai/kimi-k3"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."kimi-k3"]
|
||||
api_id = "moonshotai/kimi-k3"
|
||||
display_name = "Kimi K3 (via OpenRouter)"
|
||||
family = "kimi-k3"
|
||||
|
||||
[models."moonshotai/kimi-k3".limits]
|
||||
[providers.openrouter.models."kimi-k3".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[models."moonshotai/kimi-k3".features]
|
||||
[providers.openrouter.models."kimi-k3".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
|
|
@ -296,47 +283,45 @@ reasoning_effort = "always_adaptive"
|
|||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[models."moonshotai/kimi-k3".controls]
|
||||
[providers.openrouter.models."kimi-k3".controls]
|
||||
reasoning_effort = ["low", "high", "max"]
|
||||
|
||||
[models."moonshotai/kimi-k3".costs]
|
||||
[providers.openrouter.models."kimi-k3".costs]
|
||||
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"
|
||||
[providers.openrouter.models."laguna-s-2.1"]
|
||||
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]
|
||||
[providers.openrouter.models."laguna-s-2.1".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[models."poolside/laguna-s-2.1".features]
|
||||
[providers.openrouter.models."laguna-s-2.1".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
sampling_params = true
|
||||
|
||||
[models."poolside/laguna-s-2.1".costs]
|
||||
[providers.openrouter.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."poolside/laguna-xs-2.1"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."laguna-xs-2.1"]
|
||||
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]
|
||||
[providers.openrouter.models."laguna-xs-2.1".limits]
|
||||
context_window = 262144
|
||||
max_output = 32768
|
||||
|
||||
[models."poolside/laguna-xs-2.1".features]
|
||||
[providers.openrouter.models."laguna-xs-2.1".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
|
|
@ -345,127 +330,121 @@ 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]
|
||||
[providers.openrouter.models."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"
|
||||
[providers.openrouter.models."qwen3-coder"]
|
||||
api_id = "qwen/qwen3-coder"
|
||||
display_name = "Qwen3 Coder"
|
||||
family = "qwen3"
|
||||
|
||||
[models."qwen/qwen3-coder".limits]
|
||||
[providers.openrouter.models."qwen3-coder".limits]
|
||||
context_window = 1050000
|
||||
max_output = 16384
|
||||
|
||||
[models."qwen/qwen3-coder".features]
|
||||
[providers.openrouter.models."qwen3-coder".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."qwen/qwen3-coder".costs]
|
||||
[providers.openrouter.models."qwen3-coder".costs]
|
||||
input_cost_per_mtok = 0.22
|
||||
output_cost_per_mtok = 1.80
|
||||
|
||||
[models."qwen/qwen3.6-flash"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."qwen3.6-flash"]
|
||||
api_id = "qwen/qwen3.6-flash"
|
||||
display_name = "Qwen3.6 Flash"
|
||||
family = "qwen3"
|
||||
|
||||
[models."qwen/qwen3.6-flash".limits]
|
||||
[providers.openrouter.models."qwen3.6-flash".limits]
|
||||
context_window = 1000000
|
||||
max_output = 16384
|
||||
|
||||
[models."qwen/qwen3.6-flash".features]
|
||||
[providers.openrouter.models."qwen3.6-flash".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."qwen/qwen3.6-flash".costs]
|
||||
[providers.openrouter.models."qwen3.6-flash".costs]
|
||||
input_cost_per_mtok = 0.1875
|
||||
output_cost_per_mtok = 1.125
|
||||
|
||||
[models."z-ai/glm-5.2"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."glm-5.2"]
|
||||
api_id = "z-ai/glm-5.2"
|
||||
display_name = "GLM 5.2 (via OpenRouter)"
|
||||
family = "glm-5"
|
||||
|
||||
[models."z-ai/glm-5.2".limits]
|
||||
[providers.openrouter.models."glm-5.2".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[models."z-ai/glm-5.2".features]
|
||||
[providers.openrouter.models."glm-5.2".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[models."z-ai/glm-5.2".controls]
|
||||
[providers.openrouter.models."glm-5.2".controls]
|
||||
reasoning_effort = ["high", "xhigh"]
|
||||
|
||||
[models."z-ai/glm-5.2".costs]
|
||||
[providers.openrouter.models."glm-5.2".costs]
|
||||
input_cost_per_mtok = 0.784
|
||||
output_cost_per_mtok = 2.464
|
||||
cache_input_cost_per_mtok = 0.1456
|
||||
|
||||
[models."z-ai/glm-4.6"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."glm-4.6"]
|
||||
api_id = "z-ai/glm-4.6"
|
||||
display_name = "GLM 4.6"
|
||||
family = "glm-4"
|
||||
|
||||
[models."z-ai/glm-4.6".limits]
|
||||
[providers.openrouter.models."glm-4.6".limits]
|
||||
context_window = 203000
|
||||
max_output = 16384
|
||||
|
||||
[models."z-ai/glm-4.6".features]
|
||||
[providers.openrouter.models."glm-4.6".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."z-ai/glm-4.6".costs]
|
||||
[providers.openrouter.models."glm-4.6".costs]
|
||||
input_cost_per_mtok = 0.43
|
||||
output_cost_per_mtok = 1.74
|
||||
|
||||
[models."nvidia/nemotron-3-super-120b-a12b"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."nemotron-3-super"]
|
||||
api_id = "nvidia/nemotron-3-super-120b-a12b"
|
||||
display_name = "NVIDIA Nemotron 3 Super 120B"
|
||||
family = "nemotron-3"
|
||||
|
||||
[models."nvidia/nemotron-3-super-120b-a12b".limits]
|
||||
[providers.openrouter.models."nemotron-3-super".limits]
|
||||
context_window = 1000000
|
||||
max_output = 16384
|
||||
|
||||
[models."nvidia/nemotron-3-super-120b-a12b".features]
|
||||
[providers.openrouter.models."nemotron-3-super".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."nvidia/nemotron-3-super-120b-a12b".costs]
|
||||
[providers.openrouter.models."nemotron-3-super".costs]
|
||||
input_cost_per_mtok = 0.09
|
||||
output_cost_per_mtok = 0.45
|
||||
|
||||
[models."mistralai/devstral-2512"]
|
||||
provider = "openrouter"
|
||||
[providers.openrouter.models."devstral-2"]
|
||||
api_id = "mistralai/devstral-2512"
|
||||
display_name = "Devstral 2512"
|
||||
family = "devstral"
|
||||
|
||||
[models."mistralai/devstral-2512".limits]
|
||||
[providers.openrouter.models."devstral-2".limits]
|
||||
context_window = 262144
|
||||
max_output = 16384
|
||||
|
||||
[models."mistralai/devstral-2512".features]
|
||||
[providers.openrouter.models."devstral-2".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."mistralai/devstral-2512".costs]
|
||||
[providers.openrouter.models."devstral-2".costs]
|
||||
input_cost_per_mtok = 0.40
|
||||
output_cost_per_mtok = 2.00
|
||||
|
|
|
|||
|
|
@ -8,19 +8,18 @@ priority = 65
|
|||
[providers.poolside.auth]
|
||||
credentials = ["env:POOLSIDE_API_KEY", "vault:POOLSIDE_API_KEY"]
|
||||
|
||||
[models."laguna-s-2.1"]
|
||||
provider = "poolside"
|
||||
[providers.poolside.models."laguna-s-2.1"]
|
||||
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]
|
||||
[providers.poolside.models."laguna-s-2.1".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[models."laguna-s-2.1".features]
|
||||
[providers.poolside.models."laguna-s-2.1".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
|
|
@ -30,13 +29,12 @@ 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]
|
||||
[providers.poolside.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"
|
||||
[providers.poolside.models."laguna-xs-2.1"]
|
||||
api_id = "poolside/laguna-xs-2.1"
|
||||
display_name = "Laguna XS 2.1"
|
||||
family = "laguna-2"
|
||||
|
|
@ -44,11 +42,11 @@ small_default = true
|
|||
probe = true
|
||||
aliases = ["laguna-xs"]
|
||||
|
||||
[models."laguna-xs-2.1".limits]
|
||||
[providers.poolside.models."laguna-xs-2.1".limits]
|
||||
context_window = 262144
|
||||
max_output = 32768
|
||||
|
||||
[models."laguna-xs-2.1".features]
|
||||
[providers.poolside.models."laguna-xs-2.1".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
|
|
@ -57,7 +55,7 @@ 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]
|
||||
[providers.poolside.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
|
||||
|
|
|
|||
|
|
@ -8,43 +8,39 @@ aliases = ["venice-ai"]
|
|||
[providers.venice.auth]
|
||||
credentials = ["env:VENICE_API_KEY", "vault:VENICE_API_KEY"]
|
||||
|
||||
[models."venice-uncensored-1-2"]
|
||||
provider = "venice"
|
||||
api_id = "venice-uncensored-1-2"
|
||||
[providers.venice.models."venice-uncensored-1-2"]
|
||||
display_name = "Venice Uncensored 1.2"
|
||||
family = "venice-uncensored"
|
||||
default = true
|
||||
aliases = ["venice-uncensored", "vu"]
|
||||
|
||||
[models."venice-uncensored-1-2".limits]
|
||||
[providers.venice.models."venice-uncensored-1-2".limits]
|
||||
context_window = 128000
|
||||
max_output = 8192
|
||||
|
||||
[models."venice-uncensored-1-2".features]
|
||||
[providers.venice.models."venice-uncensored-1-2".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
|
||||
[models."venice-uncensored-1-2".costs]
|
||||
[providers.venice.models."venice-uncensored-1-2".costs]
|
||||
input_cost_per_mtok = 0.2
|
||||
output_cost_per_mtok = 0.9
|
||||
|
||||
[models."venice-uncensored-role-play"]
|
||||
provider = "venice"
|
||||
api_id = "venice-uncensored-role-play"
|
||||
[providers.venice.models."venice-uncensored-role-play"]
|
||||
display_name = "Venice Uncensored Role Play"
|
||||
family = "venice-uncensored"
|
||||
aliases = ["venice-roleplay", "vrp"]
|
||||
|
||||
[models."venice-uncensored-role-play".limits]
|
||||
[providers.venice.models."venice-uncensored-role-play".limits]
|
||||
context_window = 128000
|
||||
max_output = 4096
|
||||
|
||||
[models."venice-uncensored-role-play".features]
|
||||
[providers.venice.models."venice-uncensored-role-play".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
|
||||
[models."venice-uncensored-role-play".costs]
|
||||
[providers.venice.models."venice-uncensored-role-play".costs]
|
||||
input_cost_per_mtok = 0.5
|
||||
output_cost_per_mtok = 2.0
|
||||
|
|
|
|||
|
|
@ -8,50 +8,46 @@ priority = 60
|
|||
[providers.zai.auth]
|
||||
credentials = ["env:ZAI_API_KEY", "vault:ZAI_API_KEY"]
|
||||
|
||||
[models."glm-5.2"]
|
||||
provider = "zai"
|
||||
api_id = "glm-5.2"
|
||||
[providers.zai.models."glm-5.2"]
|
||||
display_name = "GLM 5.2"
|
||||
family = "glm-5"
|
||||
default = true
|
||||
aliases = ["glm", "glm5"]
|
||||
|
||||
[models."glm-5.2".limits]
|
||||
[providers.zai.models."glm-5.2".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[models."glm-5.2".features]
|
||||
[providers.zai.models."glm-5.2".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[models."glm-5.2".controls]
|
||||
[providers.zai.models."glm-5.2".controls]
|
||||
reasoning_effort = ["high", "max"]
|
||||
|
||||
[models."glm-5.2".costs]
|
||||
[providers.zai.models."glm-5.2".costs]
|
||||
input_cost_per_mtok = 1.4
|
||||
output_cost_per_mtok = 4.4
|
||||
cache_input_cost_per_mtok = 0.26
|
||||
|
||||
[models."glm-4.7"]
|
||||
provider = "zai"
|
||||
api_id = "glm-4.7"
|
||||
[providers.zai.models."glm-4.7"]
|
||||
display_name = "GLM 4.7"
|
||||
family = "glm-4"
|
||||
estimated_output_tps = 100
|
||||
aliases = ["glm4"]
|
||||
|
||||
[models."glm-4.7".limits]
|
||||
[providers.zai.models."glm-4.7".limits]
|
||||
context_window = 202752
|
||||
max_output = 16384
|
||||
|
||||
[models."glm-4.7".features]
|
||||
[providers.zai.models."glm-4.7".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."glm-4.7".costs]
|
||||
[providers.zai.models."glm-4.7".costs]
|
||||
input_cost_per_mtok = 0.6
|
||||
output_cost_per_mtok = 2.2
|
||||
|
|
|
|||
|
|
@ -101,9 +101,12 @@ impl AsRef<str> for ProviderId {
|
|||
}
|
||||
}
|
||||
|
||||
/// Stable model identifier — either the canonical catalog ID or one of its
|
||||
/// declared aliases.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
/// Stable, canonical human-facing model slug.
|
||||
///
|
||||
/// Aliases are selectors that resolve to a `ModelId`; they are never model
|
||||
/// IDs themselves. The same canonical slug may identify one offering on each
|
||||
/// provider, so an offering's full identity is `(ProviderId, ModelId)`.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ModelId(String);
|
||||
|
||||
|
|
@ -123,12 +126,32 @@ impl ModelId {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::borrow::Borrow<str> for ModelId {
|
||||
fn borrow(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for ModelId {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ModelId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ModelId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Debug::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ModelId {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_string())
|
||||
|
|
@ -147,6 +170,30 @@ impl AsRef<str> for ModelId {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialEq<str> for ModelId {
|
||||
fn eq(&self, other: &str) -> bool {
|
||||
self.as_str() == other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&str> for ModelId {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
self.as_str() == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<ModelId> for str {
|
||||
fn eq(&self, other: &ModelId) -> bool {
|
||||
self == other.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<ModelId> for &str {
|
||||
fn eq(&self, other: &ModelId) -> bool {
|
||||
*self == other.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ids::ProviderId;
|
||||
use crate::ids::{ModelId, ProviderId};
|
||||
|
||||
// --- 2.9 Model ---
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ pub struct ModelCosts {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Model {
|
||||
pub id: String,
|
||||
pub id: ModelId,
|
||||
pub provider: ProviderId,
|
||||
pub family: String,
|
||||
pub display_name: String,
|
||||
|
|
@ -210,7 +210,7 @@ mod tests {
|
|||
#[test]
|
||||
fn inherent_methods_return_correct_values() {
|
||||
let info = Model {
|
||||
id: "model-id".to_string(),
|
||||
id: ModelId::new("model-id"),
|
||||
provider: ProviderId::new("provider-id"),
|
||||
family: "family".to_string(),
|
||||
display_name: "Display Name".to_string(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue