mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
Delete fabro-model and document the lithos catalog overlay
fabro-model's ids and billing rollup now live in fabro-types, and its pricing, catalog, provider TOMLs, and legacy index are replaced by the lithos built-in catalog plus the Fabro policy layer. Regenerate the configuration reference for the `[llm]` overlay and `metadata.fabro`, and rewrite the SDK, models, and integration docs for the lithos provider and model shapes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
75e5f34c0d
commit
5f49cdf9da
43 changed files with 408 additions and 13968 deletions
|
|
@ -78,97 +78,77 @@ 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. Models are nested under their provider, so two providers can expose the same model slug without overwriting each other.
|
||||
Fabro's catalog is the [lithos-llm](https://docs.rs/lithos-llm) built-in catalog with Fabro's policy layer applied. The `[llm]` table in settings is a third layer over both: a lithos catalog overlay that adds providers and models or changes existing entries. Later layers win. Tables merge key by key and every other value replaces. Models are nested under their provider, so two providers can expose the same model id without overwriting each other.
|
||||
|
||||
Provider and model facts use lithos field names: `adapter`, `codec`, `base_url`, `auth`, `limits`, `capabilities`, `pricing`. Fabro policy lives under `metadata.fabro` on the provider or model: credentials, agent profile, `enabled`, default roles, and display metadata. See [Settings Configuration](/reference/user-configuration#llm) for every key.
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.proxy]
|
||||
display_name = "Acme Gateway"
|
||||
adapter = "openai_compatible"
|
||||
adapter = "openai-compatible"
|
||||
codec = "openai-chat"
|
||||
base_url = "https://llm-gateway.example.com/v1"
|
||||
auth = { type = "bearer" }
|
||||
aliases = ["gateway"]
|
||||
default_model = "team-code-large"
|
||||
|
||||
[llm.providers.proxy.auth]
|
||||
[llm.providers.proxy.metadata.fabro]
|
||||
agent_profile = "anthropic"
|
||||
credentials = ["env:ACME_GATEWAY_API_KEY", "vault:ACME_GATEWAY_API_KEY"]
|
||||
|
||||
[llm.providers.proxy.extra_headers]
|
||||
[llm.providers.proxy.metadata.fabro.extra_headers]
|
||||
x-portkey-api-key = "{{ secrets.PORTKEY_API_KEY }}"
|
||||
x-portkey-config = "@bedrock-prod"
|
||||
|
||||
[llm.providers.proxy.models."team-code-large"]
|
||||
api_id = "provider-wire-model-name"
|
||||
agent_profile = "anthropic"
|
||||
display_name = "Team Code Large"
|
||||
family = "team-code"
|
||||
default = true
|
||||
small_default = true
|
||||
aliases = ["team-code"]
|
||||
api_model = "provider-wire-model-name"
|
||||
limits = { context_tokens = 200000, max_output_tokens = 32000 }
|
||||
capabilities = { text = true, tools = true, reasoning = true, caching = true, reasoning_effort = { low = true, medium = true, high = true } }
|
||||
protocol_options = { reasoning_effort_levels = true }
|
||||
pricing = { input_usd_micros_per_million = 1500000, output_usd_micros_per_million = 8000000, cached_input_usd_micros_per_million = 300000 }
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".metadata.fabro]
|
||||
family = "team-code"
|
||||
small_default = true
|
||||
estimated_output_tps = 80
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".limits]
|
||||
context_window = 200000
|
||||
max_output = 32000
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".features]
|
||||
tools = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".controls]
|
||||
reasoning_effort = ["low", "medium", "high"]
|
||||
speed = ["fast"]
|
||||
|
||||
[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.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
|
||||
```
|
||||
|
||||
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"
|
||||
[llm.providers.litellm]
|
||||
enabled = true
|
||||
base_url = "http://localhost:4000/v1"
|
||||
default_model = "litellm-gpt-5"
|
||||
|
||||
[llm.providers.litellm.metadata.fabro]
|
||||
enabled = true
|
||||
|
||||
[llm.providers.litellm.models."litellm-gpt-5"]
|
||||
api_id = "gpt-5"
|
||||
display_name = "LiteLLM GPT-5"
|
||||
family = "litellm"
|
||||
default = true
|
||||
|
||||
[llm.providers.litellm.models."litellm-gpt-5".limits]
|
||||
context_window = 128000
|
||||
max_output = 8192
|
||||
|
||||
[llm.providers.litellm.models."litellm-gpt-5".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
api_model = "gpt-5"
|
||||
limits = { context_tokens = 128000, max_output_tokens = 8192 }
|
||||
capabilities = { text = true, tools = true }
|
||||
```
|
||||
|
||||
`api_id` is the opaque model name sent to that provider's API. It defaults to the exact model slug, so omit it when the two strings match. Fabro does not infer vendor prefixes or rewrite the value.
|
||||
`api_model` is the model name sent to that provider's API. It defaults to the exact model id, so omit it when the two strings match. Fabro does not infer vendor prefixes or rewrite the value.
|
||||
|
||||
<Note>
|
||||
Historical built-in catalog keys that exposed provider API IDs remain accepted as compatibility selectors. Fabro normalizes a primary or node selector such as `openai/gpt-5.6-sol` to the canonical `gpt-5.6-sol` slug before normal provider-aware selection. With no provider pin, the highest-priority ready offering wins; a separate `provider = "openrouter"` pin selects the OpenRouter offering. Fabro also normalizes these keys in legacy top-level `[llm.models]` rows without rewriting the settings file.
|
||||
A `provider/model` selector such as `openai/gpt-5.6-sol` pins the provider and names the model by id, alias, or wire id. A bare selector with no provider pin picks the highest-priority ready offering; a separate `provider = "openrouter"` pin selects the OpenRouter offering. Providers with `allow_passthrough = true` also accept `provider/model` selectors for models the catalog does not list.
|
||||
</Note>
|
||||
|
||||
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.
|
||||
Model roles are separate: the provider's `default_model` controls normal model selection for workflow execution, while `small_default = true` under `metadata.fabro` marks the provider's small utility model for metadata tasks such as generated run titles. If a provider has no small default, Fabro falls back to that provider's default model.
|
||||
|
||||
Provider auth is declared in `[llm.providers.<id>.auth]` with ordered `env:<NAME>` or `vault:<NAME>` refs. The primary auth header defaults to `bearer`; override with `header = { custom = "Header-Name" }` for providers like Anthropic that use `x-api-key`. Omit the `[llm.providers.<id>.auth]` block entirely for providers that need no API key (e.g. Ollama). Custom headers for any provider — including providers that need only interpolation headers and no API-key auth — go in `extra_headers` as literal text or `{{ secrets.NAME }}` tokens. Put credentials in secrets and reference them with `{{ secrets.NAME }}` instead of a bare literal.
|
||||
Provider auth has two parts. The lithos `auth` scheme says how a credential is sent: `{ type = "bearer" }`, `{ type = "header", name = "x-api-key" }`, `{ type = "headers" }` for providers that take several secret headers, `{ type = "none" }`, or `{ type = "aws" }`. Fabro's `metadata.fabro.credentials` says where the secret comes from, as ordered `env:<NAME>`, `vault:<NAME>`, or `aws_sigv4` refs; the first that resolves wins. Custom headers for any provider go in `metadata.fabro.extra_headers` as literal text or `{{ secrets.NAME }}` tokens. Put credentials in secrets and reference them with `{{ secrets.NAME }}` instead of a bare literal.
|
||||
|
||||
Workflow runs also add `x-session-id: <run-id>` to every LLM request so compatible gateways can group requests from the same run. An explicitly configured `x-session-id` in provider `extra_headers` takes precedence.
|
||||
|
||||
Provider `agent_profile` defaults from `adapter` and controls profile-specific behavior such as which tools the agent registers, project-memory filenames, CLI/ACP command selection, and native session routing. Valid values are `anthropic`, `openai`, `gemini`, `kimi`, and `gpt56`; model-level values override provider-level values.
|
||||
Provider `metadata.fabro.agent_profile` defaults from `adapter` and controls profile-specific behavior such as which tools the agent registers, project-memory filenames, CLI/ACP command selection, and native session routing. Valid values are `anthropic`, `openai`, `gemini`, `kimi`, and `gpt56`; model-level values override provider-level values.
|
||||
|
||||
Two profiles are selected per model rather than per provider, because they follow the model wherever it is served: `kimi` for Kimi models, and `gpt56` for the GPT-5.6 models (Sol, Terra, Luna). The `gpt56` profile uses Codex's narrow core surface — `shell_command`, `apply_patch`, and `update_plan`, plus optional credential-backed `web_search` — instead of fabro's dedicated file-read, discovery, and `web_fetch` tools. On OpenAI-compatible routes that cannot carry the freeform `apply_patch` grammar, it substitutes the JSON-schema `edit_file` tool. Session features may add their own question, skill, or subagent tools separately.
|
||||
|
||||
Provider `billing_policy` defaults from `adapter` and controls usage-cost estimation. Use `openai`, `anthropic`, `gemini`, or `none`. Model rows may override it for models whose billing family differs from their provider's — for example, Claude models served through OpenRouter set `billing_policy = "anthropic"` so cache reads and writes price correctly.
|
||||
Costs come from the lithos `pricing` table on each model row. Each token bucket (input, output, reasoning, cache read, cache write) prices at its own rate, with optional long-context and speed tiers. Providers that return an authoritative charge, such as OpenRouter, override the catalog estimate; the billing record says which source it came from.
|
||||
|
||||
<Note>
|
||||
Provider fields in configuration, APIs, and model routing are provider ID strings. Built-in names like `anthropic`, `openai`, and `gemini` still work, but custom IDs like `proxy` work anywhere a provider ID is accepted.
|
||||
|
|
@ -180,14 +160,14 @@ Fabro ships a built-in [Venice](/integrations/venice) provider with a curated ca
|
|||
|
||||
### Poolside
|
||||
|
||||
Fabro ships a built-in [Poolside](/integrations/poolside) provider for Laguna S 2.1 and Laguna XS 2.1 over Poolside's OpenAI-compatible API. Store a direct API key with `fabro provider login --provider poolside`. The same model slugs are also available through the opt-in OpenRouter provider; its vendor-namespaced strings remain provider-only `api_id` values.
|
||||
Fabro ships a built-in [Poolside](/integrations/poolside) provider for Laguna S 2.1 and Laguna XS 2.1 over Poolside's OpenAI-compatible API. Store a direct API key with `fabro provider login --provider poolside`. The same model slugs are also available through the opt-in OpenRouter provider; its vendor-namespaced strings remain provider-only `api_model` values.
|
||||
|
||||
### OpenRouter
|
||||
|
||||
Fabro ships an [OpenRouter](/integrations/openrouter) provider definition with a curated model catalog, disabled by default. Enable it in settings and store an API key with `fabro provider login --provider openrouter`:
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.openrouter]
|
||||
[llm.providers.openrouter.metadata.fabro]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
|
|
@ -197,8 +177,10 @@ Fabro ships a [Modal](/integrations/modal) provider definition for Kimi K3, disa
|
|||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.modal]
|
||||
enabled = true
|
||||
base_url = "https://your-endpoint.modal.run/v1"
|
||||
|
||||
[llm.providers.modal.metadata.fabro]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
Store both token values in the Fabro server vault:
|
||||
|
|
@ -214,8 +196,10 @@ Fabro ships an [Amazon Bedrock](/integrations/bedrock) provider definition with
|
|||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.bedrock]
|
||||
enabled = true
|
||||
base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
|
||||
|
||||
[llm.providers.bedrock.metadata.fabro]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
### Ollama
|
||||
|
|
@ -223,11 +207,11 @@ base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
|
|||
Fabro ships an Ollama provider definition that is disabled by default. Enable it in settings when you want Fabro to route through a local Ollama server:
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.ollama]
|
||||
[llm.providers.ollama.metadata.fabro]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
Enabling the provider alone does not expose any models — until #267 adds auto-discovery, add explicit `[llm.providers.ollama.models."<model-slug>"]` 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 explicit `[llm.providers.ollama.models."<model-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`.
|
||||
|
||||
## Default models
|
||||
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ speed = "fast"
|
|||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `reasoning_effort` | Native reasoning-effort value to request when the selected model allows it, such as `"low"`, `"medium"`, `"high"`, `"xhigh"`, or `"max"`. |
|
||||
| `reasoning_effort` | Native reasoning-effort value to request when the selected model allows it, such as `"minimal"`, `"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. |
|
||||
|
||||
#### Fallback lists with splice
|
||||
|
|
|
|||
|
|
@ -27,8 +27,10 @@ Add the provider override to `~/.fabro/settings.toml`:
|
|||
_version = 1
|
||||
|
||||
[llm.providers.bedrock]
|
||||
enabled = true
|
||||
base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
|
||||
|
||||
[llm.providers.bedrock.metadata.fabro]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
The SigV4 signing region is derived from `base_url` — change it to your Region's endpoint (`https://bedrock-runtime.<region>.amazonaws.com`, FIPS and China endpoints included).
|
||||
|
|
@ -50,7 +52,7 @@ Runs read the bearer token from the vault only. Workers start from a cleared env
|
|||
**AWS SigV4** (IAM-scoped): with no API key configured, Fabro signs each request using the AWS default credential chain — environment keys, shared profile, EC2/ECS instance roles, IRSA/web identity, SSO. Expiring session credentials refresh automatically. The catalog declares this as the `aws_sigv4` credential source:
|
||||
|
||||
```toml
|
||||
[llm.providers.bedrock.auth]
|
||||
[llm.providers.bedrock.metadata.fabro]
|
||||
credentials = ["env:AWS_BEARER_TOKEN_BEDROCK", "env:BEDROCK_API_KEY", "vault:AWS_BEARER_TOKEN_BEDROCK", "vault:BEDROCK_API_KEY", "aws_sigv4"]
|
||||
```
|
||||
|
||||
|
|
@ -60,7 +62,7 @@ The key resolves from the process environment first (either name), then the serv
|
|||
**Bearer-vs-SigV4 precedence.** Because the bearer key is tried before SigV4, setting `AWS_BEARER_TOKEN_BEDROCK` makes the `bedrock` (Converse) provider authenticate with that key too — not just the `bedrock-openai` mantle provider below. If your key is valid only for mantle (it lacks `bedrock:InvokeModel*` on the runtime), every Converse model then fails with *"Authentication failed."* To run Converse models on SigV4 while using a mantle-only bearer key for GPT-5.x, pin the Converse provider to SigV4 explicitly:
|
||||
|
||||
```toml
|
||||
[llm.providers.bedrock.auth]
|
||||
[llm.providers.bedrock.metadata.fabro]
|
||||
credentials = ["aws_sigv4"]
|
||||
```
|
||||
</Warning>
|
||||
|
|
@ -83,7 +85,7 @@ The built-in catalog curates Converse-capable models, using cross-region inferen
|
|||
| `moonshotai.kimi-k2.5`, `zai.glm-5` | |
|
||||
| `minimax.minimax-m2.5`, `nvidia.nemotron-3-super` | |
|
||||
|
||||
Any other Converse-capable Bedrock model can be added as a settings model entry with `provider = "bedrock"` and the Bedrock model or inference-profile id as `api_id`.
|
||||
Any other Converse-capable Bedrock model can be added under `[llm.providers.bedrock.models."<model-id>"]` with the Bedrock model or inference-profile id as `api_model`.
|
||||
|
||||
Not included on this provider: Claude Mythos 5 (Anthropic-Messages-only on `bedrock-mantle`, limited preview). OpenAI's frontier models live on the companion `bedrock-openai` provider below.
|
||||
|
||||
|
|
@ -92,7 +94,7 @@ Not included on this provider: Claude Mythos 5 (Anthropic-Messages-only on `bedr
|
|||
GPT-5.5 and GPT-5.4 on Bedrock are served only by the `bedrock-mantle` endpoint's OpenAI Responses API — a different surface than Converse. Fabro ships a companion `bedrock-openai` provider for them: the same AWS account and `AWS_BEARER_TOKEN_BEDROCK` key, pointed at the mantle endpoint over the OpenAI dialect.
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.bedrock-openai]
|
||||
[llm.providers.bedrock-openai.metadata.fabro]
|
||||
enabled = true
|
||||
# regional: change to https://bedrock-mantle.<region>.api.aws/openai/v1
|
||||
```
|
||||
|
|
@ -113,7 +115,7 @@ fabro run workflow.fabro --model deepseek.v3-2
|
|||
|
||||
## Prompt caching
|
||||
|
||||
Claude models cache automatically when the catalog row declares `prompt_cache`: Fabro places Converse `cachePoint` blocks after the system prompt, the tool definitions, and the conversation prefix — the same placement as the direct Anthropic provider. Cache reads and writes price Anthropic-style via the per-model `billing_policy`.
|
||||
Claude models cache automatically when the catalog row declares `prompt_cache`: Fabro places Converse `cachePoint` blocks after the system prompt, the tool definitions, and the conversation prefix — the same placement as the direct Anthropic provider. Cache reads and writes price at the row's `cached_input_usd_micros_per_million` and `cache_write_usd_micros_per_million` rates.
|
||||
|
||||
## Converse extensions
|
||||
|
||||
|
|
@ -142,7 +144,7 @@ Bedrock-specific request fields pass through verbatim via `provider_options.bedr
|
|||
|
||||
**"data retention mode 'default' is not available for this model"** — Fable 5 / Mythos-class models require opting into data sharing first; see [Model access and approvals](#model-access-and-approvals).
|
||||
|
||||
**"The provided model identifier is invalid"** — The wire id sent to Bedrock isn't a recognized model or inference-profile id. Set an explicit `api_id` (from `aws bedrock list-inference-profiles`) on the model entry.
|
||||
**"The provided model identifier is invalid"** — The wire id sent to Bedrock isn't a recognized model or inference-profile id. Set an explicit `api_model` (from `aws bedrock list-inference-profiles`) on the model entry.
|
||||
|
||||
**`ValidationException` mentioning on-demand throughput** — The model requires an inference-profile id; use the `us.`/`global.`-prefixed id from the catalog rather than the bare model id.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ Fabro runs execute through a Fabro server. Add the provider override to the sett
|
|||
```toml title="settings.toml"
|
||||
_version = 1
|
||||
|
||||
[llm.providers.fireworks]
|
||||
[llm.providers.fireworks.metadata.fabro]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ export FIREWORKS_API_KEY=fw_...
|
|||
|
||||
## Included models
|
||||
|
||||
The built-in catalog gives Fireworks offerings the same human-facing model slugs used by other providers. Fireworks account-scoped model paths remain opaque `api_id` values:
|
||||
The built-in catalog gives Fireworks offerings the same human-facing model slugs used by other providers. Fireworks account-scoped model paths remain opaque `api_model` values:
|
||||
|
||||
| Fabro model slug | Fireworks API ID / notes |
|
||||
| --- | --- |
|
||||
|
|
@ -59,21 +59,14 @@ The built-in catalog gives Fireworks offerings the same human-facing model slugs
|
|||
| `gpt-oss-120b` | `accounts/fireworks/models/gpt-oss-120b` |
|
||||
| `gpt-oss-20b` | `accounts/fireworks/models/gpt-oss-20b`; provider small default |
|
||||
|
||||
Any other Fireworks serverless model can be added under the provider. Choose a stable Fabro model slug as the table key and put the Fireworks account-scoped path in `api_id` (dots in upstream model names become `p`, e.g. `glm-5.2` → `glm-5p2`):
|
||||
Any other Fireworks serverless model can be added under the provider. Choose a stable Fabro model slug as the table key and put the Fireworks account-scoped path in `api_model` (dots in upstream model names become `p`, e.g. `glm-5.2` → `glm-5p2`):
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.fireworks.models."llama-4-maverick"]
|
||||
api_id = "accounts/fireworks/models/llama4-maverick-instruct-basic"
|
||||
display_name = "Llama 4 Maverick"
|
||||
family = "llama-4"
|
||||
|
||||
[llm.providers.fireworks.models."llama-4-maverick".limits]
|
||||
context_window = 1000000
|
||||
|
||||
[llm.providers.fireworks.models."llama-4-maverick".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
api_model = "accounts/fireworks/models/llama4-maverick-instruct-basic"
|
||||
limits = { context_tokens = 1000000, max_output_tokens = 16384 }
|
||||
capabilities = { text = true, tools = true }
|
||||
```
|
||||
|
||||
Note that Fireworks' `GET /v1/models` endpoint only returns a featured subset of serverless models; a model absent from that list may still be servable. Verify custom additions with `fabro model test`.
|
||||
|
|
@ -117,7 +110,7 @@ Fireworks caches prompt prefixes automatically — no cache breakpoints or reque
|
|||
|
||||
## Costs
|
||||
|
||||
Catalog prices mirror [Fireworks serverless pricing](https://docs.fireworks.ai/serverless/pricing). Fireworks does not return in-band billing, so Fabro reports `cost_source = "estimated"` from catalog rates. `kimi-k3-fast` uses the published 50% Fast tier premium. Other Fast model variants and the Priority service tier are not included in the built-in catalog.
|
||||
Catalog prices mirror [Fireworks serverless pricing](https://docs.fireworks.ai/serverless/pricing). Fireworks does not return in-band billing, so Fabro reports the cost source as `catalog`. `kimi-k3-fast` uses the published 50% Fast tier premium. Other Fast model variants and the Priority service tier are not included in the built-in catalog.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
@ -127,7 +120,7 @@ Catalog prices mirror [Fireworks serverless pricing](https://docs.fireworks.ai/s
|
|||
|
||||
**402 / insufficient credits** — Serverless inference requires prepaid credit; check your balance in the [Fireworks billing dashboard](https://app.fireworks.ai/settings/billing).
|
||||
|
||||
**Unknown model** — Confirm the model's `api_id` matches a Fireworks account-scoped model or router path exactly (`accounts/fireworks/models/...` or `accounts/fireworks/routers/...`), then run `fabro model test --model <fabro-model-id>`. Remember that `GET /v1/models` only lists a featured subset, so absence from that list is not conclusive.
|
||||
**Unknown model** — Confirm the model's `api_model` matches a Fireworks account-scoped model or router path exactly (`accounts/fireworks/models/...` or `accounts/fireworks/routers/...`), then run `fabro model test --model <fabro-model-id>`. Remember that `GET /v1/models` only lists a featured subset, so absence from that list is not conclusive.
|
||||
|
||||
## Further reading
|
||||
|
||||
|
|
|
|||
|
|
@ -21,26 +21,20 @@ Add the provider override and one or more model entries to `~/.fabro/settings.to
|
|||
_version = 1
|
||||
|
||||
[llm.providers.litellm]
|
||||
enabled = true
|
||||
base_url = "http://localhost:4000/v1"
|
||||
default_model = "litellm-gpt-5"
|
||||
|
||||
[llm.providers.litellm.metadata.fabro]
|
||||
enabled = true
|
||||
|
||||
[llm.providers.litellm.models."litellm-gpt-5"]
|
||||
api_id = "gpt-5"
|
||||
display_name = "LiteLLM GPT-5"
|
||||
family = "litellm"
|
||||
default = true
|
||||
|
||||
[llm.providers.litellm.models."litellm-gpt-5".limits]
|
||||
context_window = 128000
|
||||
max_output = 8192
|
||||
|
||||
[llm.providers.litellm.models."litellm-gpt-5".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
api_model = "gpt-5"
|
||||
limits = { context_tokens = 128000, max_output_tokens = 8192 }
|
||||
capabilities = { text = true, tools = true }
|
||||
```
|
||||
|
||||
`api_id` is the model name Fabro sends to LiteLLM. It should match a model name configured in your LiteLLM proxy.
|
||||
`api_model` is the model name Fabro sends to LiteLLM. It should match a model name configured in your LiteLLM proxy.
|
||||
|
||||
## Configure credentials
|
||||
|
||||
|
|
@ -94,22 +88,14 @@ Declare each LiteLLM-routed model explicitly so Fabro knows its provider, contex
|
|||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.litellm.models."litellm-fast"]
|
||||
api_id = "fast-model"
|
||||
display_name = "LiteLLM Fast"
|
||||
family = "litellm"
|
||||
aliases = ["fast"]
|
||||
|
||||
[llm.providers.litellm.models."litellm-fast".limits]
|
||||
context_window = 64000
|
||||
max_output = 4096
|
||||
|
||||
[llm.providers.litellm.models."litellm-fast".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
api_model = "fast-model"
|
||||
limits = { context_tokens = 64000, max_output_tokens = 4096 }
|
||||
capabilities = { text = true, tools = true }
|
||||
```
|
||||
|
||||
Only one model for a provider should set `default = true`. You may also mark one small/cheap utility model with `small_default = true`; Fabro uses it for metadata tasks such as generated run titles and falls back to the provider default when it is omitted.
|
||||
The provider's `default_model` names its default. You may also mark one small utility model with `small_default = true` under its `metadata.fabro` table; Fabro uses it for metadata tasks such as generated run titles and falls back to the provider default when it is omitted.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
@ -117,7 +103,7 @@ Only one model for a provider should set `default = true`. You may also mark one
|
|||
|
||||
**Connection refused** — Confirm the LiteLLM proxy is running and that `base_url` is reachable from the Fabro process. For Docker deployments, `localhost` means the Fabro container unless you point it at a host or service name.
|
||||
|
||||
**Unknown model from LiteLLM** — Check that the model's `api_id` matches the model name configured in LiteLLM, then run `fabro model test --model <fabro-model-id>`.
|
||||
**Unknown model from LiteLLM** — Check that the model's `api_model` matches the model name configured in LiteLLM, then run `fabro model test --model <fabro-model-id>`.
|
||||
|
||||
## Further reading
|
||||
|
||||
|
|
|
|||
|
|
@ -45,8 +45,10 @@ Add the provider override to the settings file used by the Fabro server. Include
|
|||
_version = 1
|
||||
|
||||
[llm.providers.modal]
|
||||
enabled = true
|
||||
base_url = "https://your-endpoint.modal.run/v1"
|
||||
|
||||
[llm.providers.modal.metadata.fabro]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
The endpoint URL is not built into Fabro because Modal assigns it to your Shared API or Auto Endpoint.
|
||||
|
|
@ -113,45 +115,27 @@ digraph Example {
|
|||
|
||||
## Direct SDK environment credentials
|
||||
|
||||
The built-in Modal provider reads its two headers from the Fabro vault. `EnvCredentialSource` does not configure Modal automatically because Modal uses two headers instead of one API-key reference.
|
||||
The built-in Modal provider authenticates with two headers, `Modal-Key` and `Modal-Secret`, read from the vault secrets `MODAL_TOKEN_ID` and `MODAL_TOKEN_SECRET`. `EnvCredentialSource` does not configure Modal automatically because Modal uses two headers instead of one API-key reference.
|
||||
|
||||
For direct SDK use, enable Modal and set its endpoint URL in the catalog:
|
||||
For direct SDK use, enable Modal and set its endpoint URL in the `[llm]` overlay, then build the client with `fabro_llm::build_client` over a `VaultCredentialSource` whose vault holds both secrets. The catalog you pass to the client must be built from the same settings file with `fabro_llm::build_catalog`.
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.modal]
|
||||
enabled = true
|
||||
base_url = "https://your-endpoint.modal.run/v1"
|
||||
```
|
||||
|
||||
Then read both environment variables explicitly and create a typed credential after constructing `catalog` from those settings:
|
||||
|
||||
```rust
|
||||
use fabro_auth::ApiCredential;
|
||||
use fabro_llm::client::Client;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let credential = ApiCredential::with_extra_headers(
|
||||
"modal",
|
||||
HashMap::from([
|
||||
("Modal-Key".to_string(), std::env::var("MODAL_TOKEN_ID")?),
|
||||
(
|
||||
"Modal-Secret".to_string(),
|
||||
std::env::var("MODAL_TOKEN_SECRET")?,
|
||||
),
|
||||
]),
|
||||
);
|
||||
let client = Client::from_credentials(vec![credential], catalog).await?;
|
||||
[llm.providers.modal.metadata.fabro]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
## Costs
|
||||
|
||||
Fabro estimates Shared API costs from Modal's published Kimi K3 prices. Completion and reasoning tokens use the output rate. Modal responses do not include an authoritative charge, so Fabro reports `cost_source = "estimated"`.
|
||||
Fabro estimates Shared API costs from Modal's published Kimi K3 prices. Completion and reasoning tokens use the output rate. Modal responses do not include an authoritative charge, so Fabro reports the cost source as `catalog`.
|
||||
|
||||
Dedicated Auto Endpoints use Modal compute billing instead of the Shared API token prices. The Fabro estimate does not represent that compute bill.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"provider 'modal' uses openai_compatible adapter but does not configure base_url"** — Add the Modal endpoint URL under `[llm.providers.modal]`. Include `/v1`.
|
||||
**Modal requests fail with 404** — Add the Modal endpoint URL as `base_url` under `[llm.providers.modal]`. Include `/v1`.
|
||||
|
||||
**Modal is not configured** — Set both `MODAL_TOKEN_ID` and `MODAL_TOKEN_SECRET` in the target server vault. One value is not sufficient.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ Fabro runs execute through a Fabro server. Add the provider override to the sett
|
|||
```toml title="settings.toml"
|
||||
_version = 1
|
||||
|
||||
[llm.providers.openrouter]
|
||||
[llm.providers.openrouter.metadata.fabro]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ export OPENROUTER_API_KEY=sk-or-v1-...
|
|||
|
||||
## Included models
|
||||
|
||||
The built-in catalog gives OpenRouter offerings the same human-facing model slugs used by direct providers. Vendor-namespaced OpenRouter IDs remain opaque `api_id` values:
|
||||
The built-in catalog gives OpenRouter offerings the same human-facing model slugs used by direct providers. Vendor-namespaced OpenRouter IDs remain opaque `api_model` values:
|
||||
|
||||
| Fabro model slug | OpenRouter API ID / notes |
|
||||
| --- | --- |
|
||||
|
|
@ -60,21 +60,14 @@ The built-in catalog gives OpenRouter offerings the same human-facing model slug
|
|||
| `minimax-m2.7`, `mimo-v2.5-pro` | Vendor-prefixed API IDs |
|
||||
| `nemotron-3-super-120b-a12b`, `devstral-2512` | Vendor-prefixed API IDs |
|
||||
|
||||
Any other OpenRouter model can be added under the provider. Choose a stable Fabro model slug as the table key and put OpenRouter's exact vendor/model string in `api_id`:
|
||||
Any other OpenRouter model can be added under the provider. Choose a stable Fabro model slug as the table key and put OpenRouter's exact vendor/model string in `api_model`:
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.openrouter.models."llama-4-maverick"]
|
||||
api_id = "meta-llama/llama-4-maverick"
|
||||
display_name = "Llama 4 Maverick"
|
||||
family = "llama-4"
|
||||
|
||||
[llm.providers.openrouter.models."llama-4-maverick".limits]
|
||||
context_window = 1000000
|
||||
|
||||
[llm.providers.openrouter.models."llama-4-maverick".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
api_model = "meta-llama/llama-4-maverick"
|
||||
limits = { context_tokens = 1000000, max_output_tokens = 16384 }
|
||||
capabilities = { text = true, tools = true }
|
||||
```
|
||||
|
||||
## Use OpenRouter models
|
||||
|
|
@ -112,7 +105,7 @@ digraph Example {
|
|||
|
||||
## Cost telemetry
|
||||
|
||||
Every OpenRouter response includes an inline `usage.cost` with authoritative USD billing. Fabro surfaces it as `cost_usd` with `cost_source = "authoritative"` on completion responses. Other providers populate the same fields from catalog price estimates with `cost_source = "estimated"`.
|
||||
Every OpenRouter response includes an inline `usage.cost` with authoritative USD billing. Fabro surfaces it as the response `cost` with source `provider`. Other providers populate the same field from catalog price estimates with source `catalog`.
|
||||
|
||||
The catalog prices on OpenRouter model rows are best-effort estimates used only before the authoritative figure arrives (for example, mid-stream rollups).
|
||||
|
||||
|
|
@ -137,7 +130,7 @@ OpenRouter's [provider routing preferences](https://openrouter.ai/docs/guides/ro
|
|||
Fabro does not send OpenRouter's optional attribution headers (`HTTP-Referer`, `X-Title`) by default, so self-hosted installations stay anonymous on OpenRouter's public app leaderboard. Workflow runs do send `x-session-id: <run-id>` for request grouping; an explicit provider `extra_headers` value for that header takes precedence. To opt in to attribution:
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.openrouter.extra_headers]
|
||||
[llm.providers.openrouter.metadata.fabro.extra_headers]
|
||||
"HTTP-Referer" = "https://your-site.example"
|
||||
"X-Title" = "Your App"
|
||||
```
|
||||
|
|
@ -150,7 +143,7 @@ Fabro does not send OpenRouter's optional attribution headers (`HTTP-Referer`, `
|
|||
|
||||
**402 / insufficient credits** — Paid OpenRouter models require prepaid credit; check your balance at [openrouter.ai/credits](https://openrouter.ai/credits).
|
||||
|
||||
**Unknown model** — Confirm the model's `api_id` matches an OpenRouter slug exactly (including the vendor prefix), then run `fabro model test --model <fabro-model-id>`.
|
||||
**Unknown model** — Confirm the model's `api_model` matches an OpenRouter slug exactly (including the vendor prefix), then run `fabro model test --model <fabro-model-id>`.
|
||||
|
||||
## Further reading
|
||||
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ For direct API or SDK requests, disable thinking through `provider_options.pools
|
|||
Enable OpenRouter and configure its separate API key as described in the [OpenRouter integration](/integrations/openrouter):
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.openrouter]
|
||||
[llm.providers.openrouter.metadata.fabro]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -698,7 +698,7 @@ fabro model test [OPTIONS]
|
|||
| `-j, --jobs <jobs>` | Number of model tests to run concurrently in bulk mode<br />Default: `4` |
|
||||
| `-m, --model <model>` | Test a specific model |
|
||||
| `-p, --provider <provider>` | Filter by provider |
|
||||
| `--reasoning-effort <reasoning_effort>` | Request a reasoning-effort level<br />Values: `low`, `medium`, `high`, `xhigh`, `max` |
|
||||
| `--reasoning-effort <reasoning_effort>` | Request a reasoning-effort level (minimal, low, medium, high, xhigh, max) |
|
||||
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
|
||||
| `--tools` | Run a multi-turn tool-use test |
|
||||
|
||||
|
|
|
|||
|
|
@ -19,30 +19,41 @@ The `fabro-agent` crate provides a session-based AI agent that runs an LLM with
|
|||
fabro-auth = { git = "https://github.com/fabro-sh/fabro" }
|
||||
fabro-agent = { git = "https://github.com/fabro-sh/fabro" }
|
||||
fabro-llm = { git = "https://github.com/fabro-sh/fabro" }
|
||||
fabro-model = { git = "https://github.com/fabro-sh/fabro" }
|
||||
fabro-types = { git = "https://github.com/fabro-sh/fabro" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
```
|
||||
|
||||
### Quick start
|
||||
|
||||
```rust
|
||||
use fabro_agent::{
|
||||
AnthropicProfile, LocalSandbox, Session, SessionOptions,
|
||||
};
|
||||
use fabro_auth::EnvCredentialSource;
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_model::catalog::LlmCatalogSettings;
|
||||
use fabro_model::Catalog;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fabro_agent::{AgentProfile, AgentProfileBuilder, LocalSandbox, Session, SessionOptions};
|
||||
use fabro_auth::EnvCredentialSource;
|
||||
use fabro_llm::ClientOptions;
|
||||
use fabro_types::{AgentProfileKind, provider_ids};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let source = EnvCredentialSource::new();
|
||||
let catalog = Arc::new(Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default())?);
|
||||
let client = Client::from_source(&source, Arc::clone(&catalog)).await?;
|
||||
let catalog = Arc::new(fabro_llm::default_catalog());
|
||||
let client = fabro_llm::build_client(
|
||||
(*catalog).clone(),
|
||||
Arc::new(EnvCredentialSource::new()),
|
||||
ClientOptions::standard(),
|
||||
)
|
||||
.await?
|
||||
.client;
|
||||
let sandbox = Arc::new(LocalSandbox::new(PathBuf::from(".")));
|
||||
let profile = Arc::new(AnthropicProfile::new("claude-sonnet-4-5"));
|
||||
let profile: Arc<dyn AgentProfile> = Arc::from(
|
||||
AgentProfileBuilder::new(
|
||||
AgentProfileKind::Anthropic,
|
||||
provider_ids::anthropic(),
|
||||
"claude-sonnet-4.5",
|
||||
Arc::clone(&catalog),
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
let config = SessionOptions::default();
|
||||
|
||||
let mut session = Session::new(client, profile, sandbox, config);
|
||||
|
|
@ -181,7 +192,7 @@ pub trait AgentProfile: Send + Sync {
|
|||
}
|
||||
```
|
||||
|
||||
Built-in profiles: `AnthropicProfile`, `OpenAiProfile`, `GeminiProfile`.
|
||||
Profiles are built with `AgentProfileBuilder::new(kind, provider, model, catalog)`. The `AgentProfileKind` values are `anthropic`, `openai`, `gemini`, `kimi`, and `gpt56`; the catalog's `metadata.fabro.agent_profile` picks one per provider or model.
|
||||
|
||||
### Events
|
||||
|
||||
|
|
@ -289,7 +300,7 @@ All fallible `Session` methods return `Result<T, AgentError>`:
|
|||
|
||||
| Variant | Description |
|
||||
|---|---|
|
||||
| `Llm(SdkError)` | An error from the LLM provider (wraps `fabro_llm::error::SdkError`). |
|
||||
| `Llm(LlmError)` | An error from the LLM provider (the stored form of a lithos `Error`). |
|
||||
| `SessionClosed` | `process_input` was called on a closed session. |
|
||||
| `InvalidState(String)` | The session is in an unexpected state. |
|
||||
| `ToolExecution(String)` | A tool execution failed. |
|
||||
|
|
@ -299,410 +310,110 @@ All fallible `Session` methods return `Result<T, AgentError>`:
|
|||
|
||||
## LLM client (`fabro-llm`)
|
||||
|
||||
The `fabro-llm` crate is a standalone Rust library for calling LLM providers. It provides a unified client that routes requests to Anthropic, OpenAI, Gemini, and other providers, with built-in streaming, tool execution, retries, and middleware.
|
||||
The `fabro-llm` crate is Fabro's integration layer over [lithos-llm](https://docs.rs/lithos-llm), a provider-neutral LLM catalog and client. lithos owns the request and response vocabulary, the provider catalog, the wire codecs, streaming, and retries. `fabro-llm` adds what Fabro needs on top: building the catalog from lithos built-ins plus Fabro policy and the operator `[llm]` overlay, constructing a client from a Fabro credential source, inlining local file attachments, normalizing reasoning output, one-shot structured output, model probes, and the `fabro exec` server gateway adapter.
|
||||
|
||||
You can use it independently of Fabro's workflow engine — add it as a dependency in any Rust project.
|
||||
Everything below the Fabro layer is the lithos API. `fabro_llm` re-exports the pieces Fabro code touches most: `Client`, `Request`, `Response`, `StreamEvent`, `Error`, `ErrorKind`, `FinishReason`, and the `lithos_catalog`, `types`, `middleware`, `adapter`, and `credentials` modules. See the lithos-llm README for the full client, middleware, and streaming contract.
|
||||
|
||||
```toml title="Cargo.toml"
|
||||
[dependencies]
|
||||
fabro-auth = { git = "https://github.com/fabro-sh/fabro" }
|
||||
fabro-llm = { git = "https://github.com/fabro-sh/fabro" }
|
||||
fabro-model = { git = "https://github.com/fabro-sh/fabro" }
|
||||
fabro-types = { git = "https://github.com/fabro-sh/fabro" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde_json = "1"
|
||||
```
|
||||
|
||||
### Quick start
|
||||
|
||||
The simplest path is an environment-backed `CredentialSource`, an explicit `Arc<Catalog>`, then `Client::from_source(&source, catalog)`. That keeps credential and model resolution explicit while still auto-reading environment variables such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and `GEMINI_API_KEY`.
|
||||
Build a catalog, build a client over a credential source, then send a lithos `Request`. `EnvCredentialSource` reads provider keys such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and `GEMINI_API_KEY` from the process environment.
|
||||
|
||||
```rust
|
||||
use fabro_auth::EnvCredentialSource;
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_llm::generate::{generate, GenerateParams};
|
||||
use fabro_model::catalog::LlmCatalogSettings;
|
||||
use fabro_model::Catalog;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fabro_auth::EnvCredentialSource;
|
||||
use fabro_llm::{ClientOptions, Request};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let source = EnvCredentialSource::new();
|
||||
let catalog = Arc::new(Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default())?);
|
||||
let client = Client::from_source(&source, Arc::clone(&catalog)).await?;
|
||||
let catalog = fabro_llm::default_catalog();
|
||||
let built = fabro_llm::build_client(
|
||||
catalog,
|
||||
Arc::new(EnvCredentialSource::new()),
|
||||
ClientOptions::standard(),
|
||||
)
|
||||
.await?;
|
||||
for issue in &built.build_issues {
|
||||
eprintln!("provider {} is unavailable: {}", issue.provider, issue.cause);
|
||||
}
|
||||
let client = built.client;
|
||||
|
||||
let result = generate(
|
||||
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
||||
.prompt("Explain ownership in Rust in two sentences.")
|
||||
).await?;
|
||||
let request = Request::builder()
|
||||
.model("claude-sonnet-4.5")
|
||||
.user("Explain ownership in Rust in two sentences.")
|
||||
.build()?;
|
||||
let response = client.complete(request).await?;
|
||||
|
||||
println!("{}", result.text());
|
||||
println!("Tokens used: {}", result.total_usage.total_tokens);
|
||||
println!("{}", response.text());
|
||||
println!("Tokens used: {}", response.usage.input + response.usage.billable_output());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Catalog
|
||||
|
||||
`fabro_llm::default_catalog()` is the lithos built-in catalog with Fabro's policy layer applied. `fabro_llm::build_catalog(&overlay, &env_lookup)` adds an operator `[llm]` overlay on top, the same layering the server and CLI use. `fabro_config::load_llm_overlay(None)` reads that overlay from the active settings file.
|
||||
|
||||
```rust
|
||||
use fabro_config::load_llm_overlay;
|
||||
|
||||
let overlay = load_llm_overlay(None)?;
|
||||
let catalog = fabro_llm::build_catalog(&overlay, &|name| std::env::var(name).ok())?;
|
||||
```
|
||||
|
||||
The `fabro_llm::catalog` module reads Fabro policy from the catalog: `enabled_providers`, `models`, `model_on_provider`, `default_model`, `probe_model`, `small_default_for_ready`, and `agent_profile`. Disabled providers and models are invisible to every query. `fabro_llm::selection` chooses a provider and model before a request exists, the way run creation and validation do: a known selector resolves to its canonical offering, `provider/model` pins the provider, and an unknown selector on a passthrough provider passes through verbatim.
|
||||
|
||||
### Client
|
||||
|
||||
`Client` is the core type that holds provider adapters and middleware. It routes each request to the appropriate provider.
|
||||
`fabro_llm::build_client(catalog, source, options)` returns a `FabroClient`: the lithos `Client`, the providers that are ready, the providers whose credentials could not be used, and the providers lithos could not build an adapter for. Credentials are read from the `CredentialSource` on every provider attempt, so a refreshed OAuth token is picked up without rebuilding the client.
|
||||
|
||||
#### Creating from a credential source
|
||||
`ClientOptions::standard()` turns on the lithos retry middleware (three attempts with short exponential backoff) and local attachment inlining. Add middleware with `with_middleware`, replace a provider's adapter with `with_adapter`, or set `http` to inject a configured HTTP client. `fabro_llm::build_offline_client(catalog, options)` builds a client whose only providers are custom adapters, which is how `fabro exec --server` routes every call through a Fabro server.
|
||||
|
||||
Credential sources live in `fabro-auth`: `EnvCredentialSource` for the process environment, `VaultCredentialSource` for a Fabro vault with optional environment fallback, and `SqlVaultCredentialSource` for the server's secret store. Fabro looks up a provider's secret through the `metadata.fabro.credentials` refs on its catalog entry.
|
||||
|
||||
#### Requests and responses
|
||||
|
||||
`Request::builder()` is the lithos request builder. `model` takes a `provider/model` route, a model id or alias, or a provider id. `system`, `user`, and `message` add messages; `tool`, `tool_choice`, `response_format`, `max_output_tokens`, `temperature`, `reasoning_effort`, and `speed` set controls. `client.complete(request)` returns a `Response` whose `content` is a list of `ContentPart` values, with `text()` and `tool_calls()` helpers, plus `finish_reason`, `usage`, and `cost`.
|
||||
|
||||
```rust
|
||||
use fabro_auth::EnvCredentialSource;
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_model::catalog::LlmCatalogSettings;
|
||||
use fabro_model::Catalog;
|
||||
use std::sync::Arc;
|
||||
use fabro_llm::Request;
|
||||
use fabro_types::{Message, Role};
|
||||
|
||||
let source = EnvCredentialSource::new();
|
||||
let catalog = Arc::new(Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default())?);
|
||||
let client = Client::from_source(&source, Arc::clone(&catalog)).await?;
|
||||
```
|
||||
let request = Request::builder()
|
||||
.model("openai/gpt-5.4")
|
||||
.system("You are a helpful assistant.")
|
||||
.message(Message::text(Role::User, "What is the capital of France?"))
|
||||
.temperature(0.0)
|
||||
.build()?;
|
||||
|
||||
For env-backed usage, `EnvCredentialSource` checks for API key environment variables and registers adapters for each provider found:
|
||||
|
||||
| Environment variable | Provider |
|
||||
|---|---|
|
||||
| `ANTHROPIC_API_KEY` | Anthropic |
|
||||
| `OPENAI_API_KEY` | OpenAI |
|
||||
| `GEMINI_API_KEY` or `GOOGLE_API_KEY` | Gemini |
|
||||
| `MOONSHOT_API_KEY` or `KIMI_API_KEY` | Moonshot AI; `MOONSHOT_API_KEY` takes precedence |
|
||||
| `ZAI_API_KEY` | ZAI |
|
||||
| `MINIMAX_API_KEY` | Minimax |
|
||||
| `INCEPTION_API_KEY` | Inception |
|
||||
| `POOLSIDE_API_KEY` | Poolside |
|
||||
| `DEEPSEEK_API_KEY` | DeepSeek |
|
||||
| `OPENROUTER_API_KEY` | OpenRouter, when enabled in settings |
|
||||
|
||||
The first provider registered becomes the default. Provider base URLs come from the model catalog. For vault-backed usage inside Fabro, use `fabro_auth::VaultCredentialSource` instead.
|
||||
|
||||
The built-in Modal definition reads two proxy-token headers from the vault, so `EnvCredentialSource` does not configure it automatically. For direct SDK use, enable Modal and set its endpoint URL in the catalog:
|
||||
|
||||
```toml
|
||||
[llm.providers.modal]
|
||||
enabled = true
|
||||
base_url = "https://your-endpoint.modal.run/v1"
|
||||
```
|
||||
|
||||
Then read the two environment variables explicitly and create a typed credential after constructing `catalog` from those settings:
|
||||
|
||||
```rust
|
||||
use fabro_auth::ApiCredential;
|
||||
use fabro_llm::client::Client;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let credential = ApiCredential::with_extra_headers(
|
||||
"modal",
|
||||
HashMap::from([
|
||||
("Modal-Key".to_string(), std::env::var("MODAL_TOKEN_ID")?),
|
||||
(
|
||||
"Modal-Secret".to_string(),
|
||||
std::env::var("MODAL_TOKEN_SECRET")?,
|
||||
),
|
||||
]),
|
||||
);
|
||||
let client = Client::from_credentials(vec![credential], catalog).await?;
|
||||
```
|
||||
|
||||
#### Creating manually
|
||||
|
||||
```rust
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_llm::providers::AnthropicAdapter;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
let adapter = AnthropicAdapter::new("sk-ant-...")
|
||||
.with_base_url("https://custom-proxy.example.com");
|
||||
|
||||
let mut providers = HashMap::new();
|
||||
providers.insert("anthropic".to_string(), Arc::new(adapter) as _);
|
||||
|
||||
let client = Client::new(providers, Some("anthropic".to_string()), vec![]);
|
||||
```
|
||||
|
||||
#### Low-level calls
|
||||
|
||||
For direct control without the tool loop, use `complete()` and `stream()` on the client:
|
||||
|
||||
```rust
|
||||
use fabro_llm::types::{Request, Message};
|
||||
|
||||
let request = Request {
|
||||
model: "claude-sonnet-4-5".into(),
|
||||
messages: vec![Message::user("Hello")],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let response = client.complete(&request).await?;
|
||||
let response = client.complete(request).await?;
|
||||
println!("{}", response.text());
|
||||
```
|
||||
|
||||
### High-level generation
|
||||
|
||||
The `generate()` function wraps the client with automatic tool execution loops, retries, and timeouts. It is the recommended entry point for most use cases.
|
||||
|
||||
#### Basic completion
|
||||
|
||||
```rust
|
||||
use fabro_auth::EnvCredentialSource;
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_llm::generate::{generate, GenerateParams};
|
||||
|
||||
# let source = EnvCredentialSource::new();
|
||||
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
||||
# let client = Client::from_source(&source, catalog).await?;
|
||||
let result = generate(
|
||||
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
||||
.system("You are a helpful assistant.")
|
||||
.prompt("What is the capital of France?")
|
||||
.temperature(0.0)
|
||||
).await?;
|
||||
|
||||
println!("{}", result.text());
|
||||
```
|
||||
|
||||
#### Multi-turn conversations
|
||||
|
||||
Use `.messages()` instead of `.prompt()` to pass a full conversation history:
|
||||
|
||||
```rust
|
||||
use fabro_auth::EnvCredentialSource;
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_llm::types::Message;
|
||||
|
||||
# let source = EnvCredentialSource::new();
|
||||
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
||||
# let client = Client::from_source(&source, catalog).await?;
|
||||
let result = generate(
|
||||
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
||||
.messages(vec![
|
||||
Message::user("My name is Alice."),
|
||||
Message::assistant("Hello Alice! How can I help you?"),
|
||||
Message::user("What's my name?"),
|
||||
])
|
||||
).await?;
|
||||
```
|
||||
|
||||
<Note>
|
||||
You cannot use both `.prompt()` and `.messages()` on the same request — this returns `SdkError::Configuration`.
|
||||
</Note>
|
||||
|
||||
#### GenerateParams reference
|
||||
|
||||
| Method | Type | Description |
|
||||
|---|---|---|
|
||||
| `new(model, client)` | `(impl Into<String>, Arc<Client>)` | Required. Model ID or alias plus the client to use |
|
||||
| `.prompt(text)` | `impl Into<String>` | Convenience: sends a single user message |
|
||||
| `.messages(msgs)` | `Vec<Message>` | Full conversation history |
|
||||
| `.system(text)` | `impl Into<String>` | System prompt |
|
||||
| `.tools(tools)` | `Vec<Tool>` | Tools available to the model |
|
||||
| `.tool_choice(choice)` | `ToolChoice` | How the model selects tools |
|
||||
| `.max_tool_rounds(n)` | `u32` | Max tool execution rounds (default: 1) |
|
||||
| `.temperature(t)` | `f64` | Sampling temperature |
|
||||
| `.top_p(p)` | `f64` | Nucleus sampling |
|
||||
| `.max_tokens(n)` | `i64` | Maximum output tokens |
|
||||
| `.stop_sequences(seqs)` | `Vec<String>` | Stop sequences |
|
||||
| `.reasoning_effort(level)` | `impl Into<String>` | e.g. `"low"`, `"medium"`, `"high"` |
|
||||
| `.provider(name)` | `impl Into<String>` | Force a specific provider |
|
||||
| `.max_retries(n)` | `u32` | Retry count for transient errors (default: 2) |
|
||||
| `.timeout(config)` | `TimeoutConfig` | Total and per-step timeouts |
|
||||
| `.abort_signal(token)` | `CancellationToken` | Cancel generation |
|
||||
| `.stop_when(f)` | `Fn(&[StepResult]) -> bool` | Custom stop condition after each tool round |
|
||||
|
||||
#### GenerateResult
|
||||
|
||||
`GenerateResult` dereferences to `Response`, so you can call response methods directly:
|
||||
|
||||
```rust
|
||||
let result = generate(params).await?;
|
||||
|
||||
// Response methods (via Deref)
|
||||
result.text(); // concatenated text output
|
||||
result.tool_calls(); // Vec<ToolCall> from the final response
|
||||
result.reasoning(); // Option<String> — extended thinking content
|
||||
|
||||
// GenerateResult fields
|
||||
result.response; // Response — the final LLM response
|
||||
result.tool_results; // Vec<ToolResult> — from the final step
|
||||
result.total_usage; // Usage — aggregated across all steps
|
||||
result.steps; // Vec<StepResult> — one per tool round
|
||||
result.output; // Option<Value> — for structured output
|
||||
```
|
||||
|
||||
### Tools
|
||||
|
||||
Tools let the model call functions during generation. There are two kinds:
|
||||
|
||||
- **Active tools** have an execute handler — Fabro runs them automatically and feeds results back to the model.
|
||||
- **Passive tools** have no handler — Fabro returns the tool calls to you in the response.
|
||||
|
||||
#### Defining an active tool
|
||||
|
||||
```rust
|
||||
use fabro_auth::EnvCredentialSource;
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_llm::tools::Tool;
|
||||
use serde_json::json;
|
||||
|
||||
let weather = Tool::active(
|
||||
"get_weather",
|
||||
"Get the current weather for a city",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": { "type": "string", "description": "City name" }
|
||||
},
|
||||
"required": ["city"]
|
||||
}),
|
||||
|args, _ctx| async move {
|
||||
let city = args["city"].as_str().unwrap_or("unknown");
|
||||
Ok(json!({ "temperature": "72°F", "city": city }))
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
#### Using tools with generate
|
||||
|
||||
```rust
|
||||
# use fabro_auth::EnvCredentialSource;
|
||||
# use fabro_llm::client::Client;
|
||||
# let source = EnvCredentialSource::new();
|
||||
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
||||
# let client = Client::from_source(&source, catalog).await?;
|
||||
let result = generate(
|
||||
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
||||
.prompt("What's the weather in San Francisco?")
|
||||
.tools(vec![weather])
|
||||
.max_tool_rounds(5)
|
||||
).await?;
|
||||
|
||||
// Inspect the tool execution history
|
||||
for (i, step) in result.steps.iter().enumerate() {
|
||||
let calls = step.response.tool_calls();
|
||||
println!("Step {i}: {} tool calls, {} results", calls.len(), step.tool_results.len());
|
||||
}
|
||||
```
|
||||
|
||||
The `generate()` function loops automatically: the model calls tools, Fabro executes them, feeds results back, and repeats until the model stops or `max_tool_rounds` is reached.
|
||||
|
||||
#### Tool choice
|
||||
|
||||
Control how the model selects tools:
|
||||
|
||||
```rust
|
||||
use fabro_llm::types::ToolChoice;
|
||||
|
||||
// Let the model decide (default)
|
||||
# use fabro_auth::EnvCredentialSource;
|
||||
# use fabro_llm::client::Client;
|
||||
# let source = EnvCredentialSource::new();
|
||||
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
||||
# let client = Client::from_source(&source, catalog).await?;
|
||||
GenerateParams::new("opus", client.clone()).tool_choice(ToolChoice::Auto);
|
||||
|
||||
// Force a specific tool
|
||||
GenerateParams::new("opus", client.clone()).tool_choice(ToolChoice::Named {
|
||||
tool_name: "get_weather".into()
|
||||
});
|
||||
|
||||
// Force the model to use some tool
|
||||
GenerateParams::new("opus", client.clone()).tool_choice(ToolChoice::Required);
|
||||
|
||||
// Prevent tool use
|
||||
GenerateParams::new("opus", client.clone()).tool_choice(ToolChoice::None);
|
||||
```
|
||||
|
||||
#### Passive tools
|
||||
|
||||
Passive tools let you handle execution yourself:
|
||||
|
||||
```rust
|
||||
# use fabro_auth::EnvCredentialSource;
|
||||
# use fabro_llm::client::Client;
|
||||
# let source = EnvCredentialSource::new();
|
||||
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
||||
# let client = Client::from_source(&source, catalog).await?;
|
||||
let search = Tool::passive(
|
||||
"search",
|
||||
"Search the codebase",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": { "type": "string" }
|
||||
},
|
||||
"required": ["query"]
|
||||
}),
|
||||
);
|
||||
|
||||
let result = generate(
|
||||
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
||||
.prompt("Find all uses of the Config struct")
|
||||
.tools(vec![search])
|
||||
).await?;
|
||||
|
||||
// Handle tool calls yourself
|
||||
for call in result.tool_calls() {
|
||||
println!("Model wants to call {} with {}", call.name, call.arguments);
|
||||
}
|
||||
```
|
||||
There is no tool-execution loop in `fabro-llm`. The agent loop lives in `fabro-agent`, which decides when to run a tool and feeds results back as `Role::Tool` messages.
|
||||
|
||||
### Streaming
|
||||
|
||||
#### Text stream
|
||||
|
||||
For simple cases where you only need the text deltas:
|
||||
`client.stream(request)` returns a lithos `ResponseStream`, a `Stream` of `StreamEvent` values. Events are discriminated by `type` on the wire: `started`, `content_block_start`, `text_delta`, `reasoning_delta`, `tool_call_delta`, `content_block_end`, `usage`, `rate_limits`, and `ended`, which carries the complete `Response`.
|
||||
|
||||
```rust
|
||||
use fabro_auth::EnvCredentialSource;
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_llm::generate::{stream, GenerateParams};
|
||||
use fabro_llm::StreamEvent;
|
||||
use futures::StreamExt;
|
||||
|
||||
# let source = EnvCredentialSource::new();
|
||||
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
||||
# let client = Client::from_source(&source, catalog).await?;
|
||||
let stream_result = stream(
|
||||
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
||||
.prompt("Write a haiku about Rust")
|
||||
).await?;
|
||||
|
||||
let mut text_stream = stream_result.text_stream();
|
||||
while let Some(chunk) = text_stream.next().await {
|
||||
print!("{}", chunk?);
|
||||
}
|
||||
```
|
||||
|
||||
#### Full event stream
|
||||
|
||||
For fine-grained control, consume `StreamEvent` variants directly:
|
||||
|
||||
```rust
|
||||
use fabro_auth::EnvCredentialSource;
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_llm::generate::{stream, GenerateParams};
|
||||
use fabro_llm::types::StreamEvent;
|
||||
use futures::StreamExt;
|
||||
|
||||
# let source = EnvCredentialSource::new();
|
||||
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
||||
# let client = Client::from_source(&source, catalog).await?;
|
||||
let mut stream_result = stream(
|
||||
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
||||
.prompt("Explain monads")
|
||||
).await?;
|
||||
|
||||
while let Some(event) = stream_result.next().await {
|
||||
let mut stream = client.stream(request).await?;
|
||||
while let Some(event) = stream.next().await {
|
||||
match event? {
|
||||
StreamEvent::TextDelta { delta, .. } => print!("{delta}"),
|
||||
StreamEvent::ReasoningDelta { delta } => eprint!("[thinking] {delta}"),
|
||||
StreamEvent::ToolCallStart { tool_call } => {
|
||||
println!("\n> Calling tool: {}", tool_call.name);
|
||||
}
|
||||
StreamEvent::StepFinish { usage, .. } => {
|
||||
println!("\n[step done, {} tokens]", usage.total_tokens);
|
||||
}
|
||||
StreamEvent::Finish { response, .. } => {
|
||||
StreamEvent::TextDelta { text, .. } => print!("{text}"),
|
||||
StreamEvent::Ended { response } => {
|
||||
println!("\n[done: {:?}]", response.finish_reason);
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -710,282 +421,95 @@ while let Some(event) = stream_result.next().await {
|
|||
}
|
||||
```
|
||||
|
||||
#### StreamEvent variants
|
||||
|
||||
| Variant | Description |
|
||||
|---|---|
|
||||
| `StreamStart` | Stream opened |
|
||||
| `TextStart { text_id }` | Text block started |
|
||||
| `TextDelta { delta, text_id }` | Incremental text chunk |
|
||||
| `TextEnd { text_id }` | Text block ended |
|
||||
| `ReasoningStart` | Extended thinking started |
|
||||
| `ReasoningDelta { delta }` | Incremental reasoning chunk |
|
||||
| `ReasoningEnd` | Extended thinking ended |
|
||||
| `ToolCallStart { tool_call }` | Tool call started |
|
||||
| `ToolCallDelta { tool_call }` | Incremental tool call arguments |
|
||||
| `ToolCallEnd { tool_call }` | Tool call complete |
|
||||
| `StepFinish { finish_reason, usage, response, tool_calls, tool_results }` | A tool round completed (more rounds may follow) |
|
||||
| `Finish { finish_reason, usage, response }` | Generation complete |
|
||||
| `Error { error, raw }` | Provider error |
|
||||
A turn that ends with `FinishReason::Length` or `FinishReason::Incomplete` is not complete. Tool calls from such a turn arrive in `response.suppressed_tool_calls` and must not be executed. `fabro-agent` treats both as a retryable failure of the turn.
|
||||
|
||||
### Structured output
|
||||
|
||||
Generate typed JSON objects that conform to a JSON Schema:
|
||||
`fabro_llm::structured::complete_object` attaches a JSON Schema as the request's response format and parses the reply:
|
||||
|
||||
```rust
|
||||
use fabro_auth::EnvCredentialSource;
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_llm::generate::{generate_object, GenerateParams};
|
||||
use fabro_llm::{Request, structured};
|
||||
use serde_json::json;
|
||||
|
||||
# let source = EnvCredentialSource::new();
|
||||
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
||||
# let client = Client::from_source(&source, catalog).await?;
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"age": { "type": "integer" },
|
||||
"hobbies": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
"age": { "type": "integer" }
|
||||
},
|
||||
"required": ["name", "age", "hobbies"]
|
||||
"required": ["name", "age"]
|
||||
});
|
||||
|
||||
let result = generate_object(
|
||||
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
||||
.prompt("Generate a profile for a fictional character"),
|
||||
schema,
|
||||
).await?;
|
||||
|
||||
let profile = result.output.expect("structured output");
|
||||
println!("Name: {}", profile["name"]);
|
||||
let request = Request::builder()
|
||||
.model("claude-sonnet-4.5")
|
||||
.user("Generate a profile for a fictional character")
|
||||
.build()?;
|
||||
let completion = structured::complete_object(&client, request, "profile", schema).await?;
|
||||
println!("Name: {}", completion.object["name"]);
|
||||
```
|
||||
|
||||
### Reasoning
|
||||
|
||||
`fabro_llm::reasoning::normalize(&response.content)` folds a response's readable reasoning parts into a `fabro_types::ReasoningOutput` with a summary and a trace. Provider replay data such as signatures and encrypted reasoning never appears in it.
|
||||
|
||||
### Middleware
|
||||
|
||||
Middleware intercepts requests and responses for logging, caching, or transformation:
|
||||
|
||||
```rust
|
||||
use fabro_llm::middleware::{Middleware, NextFn, NextStreamFn};
|
||||
use fabro_llm::provider::StreamEventStream;
|
||||
use fabro_llm::types::{Request, Response};
|
||||
use fabro_llm::error::SdkError;
|
||||
use async_trait::async_trait;
|
||||
|
||||
struct LoggingMiddleware;
|
||||
|
||||
#[async_trait]
|
||||
impl Middleware for LoggingMiddleware {
|
||||
async fn handle_complete(
|
||||
&self,
|
||||
request: Request,
|
||||
next: NextFn,
|
||||
) -> Result<Response, SdkError> {
|
||||
println!("Request to model: {}", request.model);
|
||||
let response = next(request).await?;
|
||||
println!("Response: {} tokens", response.usage.total_tokens);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_stream(
|
||||
&self,
|
||||
request: Request,
|
||||
next: NextStreamFn,
|
||||
) -> Result<StreamEventStream, SdkError> {
|
||||
println!("Streaming request to model: {}", request.model);
|
||||
next(request).await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add middleware to the client:
|
||||
|
||||
```rust
|
||||
use fabro_auth::EnvCredentialSource;
|
||||
use fabro_llm::client::Client;
|
||||
use fabro_model::catalog::LlmCatalogSettings;
|
||||
use fabro_model::Catalog;
|
||||
|
||||
let source = EnvCredentialSource::new();
|
||||
let catalog = std::sync::Arc::new(Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default())?);
|
||||
let mut client = Client::from_source(&source, catalog).await?;
|
||||
client.add_middleware(std::sync::Arc::new(LoggingMiddleware));
|
||||
```
|
||||
|
||||
### Model catalog
|
||||
|
||||
The crate embeds a catalog of known models with metadata:
|
||||
|
||||
```rust
|
||||
use fabro_llm::catalog;
|
||||
|
||||
// Look up a model by ID or alias
|
||||
let info = catalog::get_model_info("opus").unwrap();
|
||||
println!("{} ({})", info.display_name, info.provider);
|
||||
println!("Context: {} tokens", info.limits.context_window);
|
||||
println!("Tools: {}, Vision: {}", info.features.tools, info.features.vision);
|
||||
|
||||
// List all models for a provider
|
||||
let models = catalog::list_models(Some("anthropic"));
|
||||
|
||||
// Get the default model for a provider
|
||||
let default = catalog::default_model_for_provider("openai").unwrap();
|
||||
|
||||
// Find a capability-matched model on a different provider
|
||||
let equivalent = catalog::closest_model("gemini", &info);
|
||||
```
|
||||
|
||||
See [Models](/core-concepts/models) for the full catalog table.
|
||||
Middleware is the lithos `Middleware` trait: `handle(&self, call: Call, next: Next)` sees the resolved route and request and returns an `Output` that is either a complete response or a stream. `fabro_llm::attachments::InlineLocalAttachments` is Fabro's own middleware; it rewrites local file references in messages into inline media before dispatch.
|
||||
|
||||
### Error handling
|
||||
|
||||
All fallible operations return `Result<T, SdkError>`. The error type classifies failures to enable retry and failover decisions:
|
||||
Every fallible operation returns `Result<T, fabro_llm::Error>`, the lithos error. `error.kind()` is an `ErrorKind` such as `Authentication`, `RateLimit`, `Server`, `ContextLength`, `ContentFilter`, `Timeout`, `StreamDecode`, or `Cancelled`. `error.data()` is the `ErrorData` snapshot Fabro stores in run events; `fabro_llm::LlmError` wraps it.
|
||||
|
||||
```rust
|
||||
use fabro_llm::error::SdkError;
|
||||
The `fabro_llm::ErrorFacts` trait is implemented for `Error`, `ErrorData`, and `LlmError`, and the classification helpers take any of them:
|
||||
|
||||
match result {
|
||||
Err(SdkError::Provider { kind, detail }) => {
|
||||
println!("Provider error ({}): {}", detail.provider, detail.message);
|
||||
if let Some(code) = detail.status_code {
|
||||
println!("HTTP {code}");
|
||||
}
|
||||
}
|
||||
Err(SdkError::RequestTimeout { message, .. }) => println!("Timeout: {message}"),
|
||||
Err(SdkError::Network { message, .. }) => println!("Network: {message}"),
|
||||
Err(SdkError::Interrupt { message }) => println!("Cancelled: {message}"),
|
||||
Err(e) => println!("Other: {e}"),
|
||||
Ok(_) => {}
|
||||
}
|
||||
```
|
||||
|
||||
#### Error classification
|
||||
|
||||
Every `SdkError` exposes classification methods:
|
||||
|
||||
| Method | Returns | Description |
|
||||
|---|---|---|
|
||||
| `retryable()` | `bool` | Safe to retry with the same provider (e.g. rate limit, server error) |
|
||||
| `failover_eligible()` | `bool` | Safe to try a different provider |
|
||||
| `retry_after()` | `Option<f64>` | Seconds to wait before retrying (from provider `Retry-After` header) |
|
||||
| `status_code()` | `Option<u16>` | HTTP status code, if applicable |
|
||||
| `provider_name()` | `&str` | Which provider returned the error |
|
||||
|
||||
#### Provider error kinds
|
||||
|
||||
| Kind | HTTP status | Retryable | Failover |
|
||||
|---|---|---|---|
|
||||
| `Authentication` | 401 | No | No |
|
||||
| `AccessDenied` | 403 | No | No |
|
||||
| `NotFound` | 404 | No | No |
|
||||
| `InvalidRequest` | 400 | No | No |
|
||||
| `RateLimit` | 429 | Yes | Yes |
|
||||
| `Server` | 500, 502, 503 | Yes | Yes |
|
||||
| `ContentFilter` | varies | No | No |
|
||||
| `ContextLength` | varies | No | No |
|
||||
| `QuotaExceeded` | varies | No | Yes |
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `is_retryable(&error)` | Safe to retry with the same provider, from lithos's retry classification |
|
||||
| `failover_eligible(&error)` | Safe to try a different provider |
|
||||
| `is_auth_error(&error)` | The credential was missing or rejected |
|
||||
| `is_cancelled(&error)` | The caller cancelled the call |
|
||||
| `failure_signature_hint(&error)` | A stable string for loop and restart detection |
|
||||
|
||||
### Retries
|
||||
|
||||
The `generate()` function retries automatically based on `max_retries` (default: 2). For low-level use, the `retry` function wraps any async operation:
|
||||
|
||||
```rust
|
||||
use fabro_llm::retry::retry;
|
||||
use fabro_llm::types::RetryPolicy;
|
||||
|
||||
let policy = RetryPolicy {
|
||||
max_retries: 3,
|
||||
base_delay: 1.0,
|
||||
max_delay: 60.0,
|
||||
backoff_multiplier: 2.0,
|
||||
jitter: true,
|
||||
on_retry: None,
|
||||
};
|
||||
|
||||
let response = retry(&policy, || {
|
||||
let c = client.clone();
|
||||
let r = request.clone();
|
||||
async move { c.complete(&r).await }
|
||||
}).await?;
|
||||
```
|
||||
|
||||
Retry only fires when `error.retryable()` returns `true` and respects `Retry-After` headers.
|
||||
The lithos `RetryMiddleware` installed by `ClientOptions::standard()` retries a request until its stream delivers visible output. After visible output the client never replays on its own; `fabro-agent` decides whether to replay a turn using `RetryPolicy::next_delay`, the same decision the middleware uses. Insert a `fabro_llm::RetryListener` into a call's context extensions to be told about each retry the middleware performs.
|
||||
|
||||
### Cancellation
|
||||
|
||||
Pass a `CancellationToken` to interrupt long-running generation:
|
||||
Pass a `CallContext` with a cancellation token through `complete_with_context` or `stream_with_context`. Cancelling the token ends the call with `ErrorKind::Cancelled`.
|
||||
|
||||
```rust
|
||||
use fabro_auth::EnvCredentialSource;
|
||||
use fabro_llm::client::Client;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use fabro_llm::CallContext;
|
||||
|
||||
# let source = EnvCredentialSource::new();
|
||||
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
||||
# let client = Client::from_source(&source, catalog).await?;
|
||||
let token = CancellationToken::new();
|
||||
let token_clone = token.clone();
|
||||
|
||||
// Cancel after 30 seconds
|
||||
let context = CallContext::new();
|
||||
let cancel = context.cancellation().clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
||||
token_clone.cancel();
|
||||
cancel.cancel();
|
||||
});
|
||||
|
||||
let result = generate(
|
||||
GenerateParams::new("opus", client.clone())
|
||||
.prompt("Write a novel")
|
||||
.abort_signal(token)
|
||||
).await;
|
||||
// Returns SdkError::Interrupt if cancelled
|
||||
let result = client.complete_with_context(request, context).await;
|
||||
```
|
||||
|
||||
### Probes
|
||||
|
||||
`fabro_llm::probe::run_model_test(&client, "provider/model", mode, reasoning_effort, timeout)` sends the lithos model probe: one word in `Basic` mode, a two-step tool exchange in `Deep` mode. `probe_provider_with_api_key` validates an operator-supplied key against a provider's probe model before it is stored.
|
||||
|
||||
### Provider adapters
|
||||
|
||||
Each provider has a dedicated adapter. All adapters implement the `ProviderAdapter` trait and are interchangeable.
|
||||
Providers are lithos adapters selected by the catalog `adapter` id: `anthropic`, `openai`, `gemini`, `openai-compatible`, and `bedrock`. A new OpenAI-compatible endpoint needs a catalog entry, not code.
|
||||
|
||||
| Adapter | Provider | Constructor |
|
||||
|---|---|---|
|
||||
| `AnthropicAdapter` | Anthropic Messages API | `::new(api_key)` |
|
||||
| `OpenAiAdapter` | OpenAI Responses API | `::new(api_key)` |
|
||||
| `GeminiAdapter` | Google Gemini API | `::new(api_key)` |
|
||||
| `OpenAiCompatibleAdapter` | Any OpenAI-compatible endpoint | `::new(api_key, base_url)` |
|
||||
|
||||
All adapters support `.with_base_url()` for proxies or custom endpoints. `OpenAiAdapter` also supports `.with_org_id()` and `.with_project_id()`.
|
||||
|
||||
#### Custom provider
|
||||
|
||||
Implement the `ProviderAdapter` trait to add a new provider:
|
||||
To add a custom transport, implement the lithos `ProviderAdapter` trait and register it with `ClientOptions::with_adapter`. `fabro_llm::gateway::GatewayAdapter` is Fabro's own example: it posts each request to a Fabro server's completions endpoint, which returns lithos `Response` JSON and streams lithos `StreamEvent` JSON verbatim.
|
||||
|
||||
```rust
|
||||
use fabro_llm::provider::{ProviderAdapter, StreamEventStream};
|
||||
use fabro_llm::types::{Request, Response};
|
||||
use fabro_llm::error::SdkError;
|
||||
use async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
|
||||
struct MyProvider;
|
||||
use fabro_llm::ClientOptions;
|
||||
use fabro_llm::gateway::GatewayAdapter;
|
||||
use fabro_types::ProviderId;
|
||||
|
||||
#[async_trait]
|
||||
impl ProviderAdapter for MyProvider {
|
||||
fn name(&self) -> &str { "my-provider" }
|
||||
|
||||
async fn complete(&self, request: &Request) -> Result<Response, SdkError> {
|
||||
// Call your provider's API
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn stream(&self, request: &Request) -> Result<StreamEventStream, SdkError> {
|
||||
// Return a stream of events
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Register it on the client:
|
||||
|
||||
```rust
|
||||
client.register_provider(Arc::new(MyProvider)).await?;
|
||||
let adapter = Arc::new(GatewayAdapter::new(Box::new(my_transport)));
|
||||
let built = fabro_llm::build_offline_client(
|
||||
catalog,
|
||||
ClientOptions::default().with_adapter(ProviderId::new("anthropic"), adapter),
|
||||
)?;
|
||||
```
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ Files that omit `_version` are treated as version `1`. The legacy top-level `ver
|
|||
|---|---|
|
||||
| CLI-only | `[cli.target]`, `[cli.auth]`, `[cli.exec]`, `[cli.output]`, `[cli.updates]`, `[cli.logging]` |
|
||||
| Server-side run policy | `[run.model]`, `[run.environment]`, `[environments.<slug>]`, `[run.checkpoint]`, `[run.inputs]`, `[run.prepare]`, `[run.pull_request]`, `[run.integrations.github]`, `[run.hooks]`, `[run.agent.mcps]` |
|
||||
| Shared LLM catalog | `[llm.providers.<id>]`, provider-scoped `[llm.providers.<id>.models.<slug>]` offerings, limits, features, controls, and costs |
|
||||
| Shared LLM catalog | `[llm]`, a lithos-llm catalog overlay: `[llm.providers.<id>]`, `[llm.providers.<id>.models.<id>]`, and Fabro policy under `metadata.fabro` |
|
||||
| Server-only | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` |
|
||||
|
||||
`[cli.*]` and `[server.*]` stanzas are owner-specific: they are only consumed from `~/.fabro/settings.toml` (plus process-local flags and env overrides). The same stanzas in `.fabro/project.toml` or `workflow.toml` remain schema-valid but runtime-inert.
|
||||
|
|
@ -89,36 +89,29 @@ level = "info"
|
|||
|
||||
[llm.providers.proxy]
|
||||
display_name = "Acme Gateway"
|
||||
adapter = "openai_compatible"
|
||||
adapter = "openai-compatible"
|
||||
codec = "openai-chat"
|
||||
base_url = "https://llm-gateway.example.com/v1"
|
||||
auth = { type = "bearer" }
|
||||
aliases = ["gateway"]
|
||||
default_model = "team-code-large"
|
||||
|
||||
[llm.providers.proxy.auth]
|
||||
[llm.providers.proxy.metadata.fabro]
|
||||
agent_profile = "anthropic"
|
||||
credentials = ["env:ACME_GATEWAY_API_KEY", "vault:ACME_GATEWAY_API_KEY"]
|
||||
|
||||
[llm.providers.proxy.extra_headers]
|
||||
[llm.providers.proxy.metadata.fabro.extra_headers]
|
||||
x-portkey-api-key = "{{ secrets.PORTKEY_API_KEY }}"
|
||||
x-portkey-config = "@bedrock-prod"
|
||||
|
||||
[llm.providers.proxy.models."team-code-large"]
|
||||
api_id = "provider-wire-model-name"
|
||||
agent_profile = "anthropic"
|
||||
display_name = "Team Code Large"
|
||||
default = true
|
||||
aliases = ["team-code"]
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".controls]
|
||||
reasoning_effort = ["low", "medium", "high"]
|
||||
speed = ["fast"]
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".costs]
|
||||
input_cost_per_mtok = 1.50
|
||||
output_cost_per_mtok = 8.00
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".costs.speed.fast]
|
||||
input_cost_per_mtok = 3.00
|
||||
output_cost_per_mtok = 16.00
|
||||
|
||||
api_model = "provider-wire-model-name"
|
||||
limits = { context_tokens = 200000, max_output_tokens = 32000 }
|
||||
capabilities = { text = true, tools = true, reasoning = true, reasoning_effort = { low = true, medium = true, high = true } }
|
||||
protocol_options = { reasoning_effort_levels = true }
|
||||
pricing = { input_usd_micros_per_million = 1500000, output_usd_micros_per_million = 8000000 }
|
||||
```
|
||||
|
||||
All fields are optional. Include only the sections and keys you want to override. A single file can still include both CLI and server sections when you run both processes on one machine, but explicit remote targets do not read remote server state from the local machine.
|
||||
|
|
@ -147,146 +140,114 @@ url = "https://fabro.example.com/api/v1"
|
|||
| `url` | string | None | Required for `type = "http"`; the API base URL. |
|
||||
| `path` | string | None | Required for `type = "unix"`; the absolute Unix socket path. |
|
||||
|
||||
## `[llm.providers.<id>]`
|
||||
## `[llm]`
|
||||
|
||||
Define or override an LLM provider. Provider IDs are strings, so custom
|
||||
providers can be added when they use an adapter Fabro already supports.
|
||||
The `[llm]` table is a [lithos-llm](https://docs.rs/lithos-llm) catalog
|
||||
overlay. Fabro builds its model catalog from three layers: the lithos built-in
|
||||
providers and models, Fabro's policy layer, and this table. Later layers win;
|
||||
tables merge key by key and every other value replaces. Fabro does not
|
||||
interpret the table itself. lithos validates it when the catalog is built, and
|
||||
rejects unknown provider or model fields.
|
||||
|
||||
Fabro-specific policy lives under `metadata.fabro` on a provider or model.
|
||||
lithos carries that namespace verbatim.
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.proxy]
|
||||
display_name = "Acme Gateway"
|
||||
adapter = "openai_compatible"
|
||||
adapter = "openai-compatible"
|
||||
codec = "openai-chat"
|
||||
base_url = "https://llm-gateway.example.com/v1"
|
||||
auth = { type = "bearer" }
|
||||
priority = 50
|
||||
enabled = true
|
||||
aliases = ["gateway"]
|
||||
default_model = "team-code-large"
|
||||
|
||||
[llm.providers.proxy.auth]
|
||||
[llm.providers.proxy.metadata.fabro]
|
||||
enabled = true
|
||||
agent_profile = "anthropic"
|
||||
credentials = ["env:ACME_GATEWAY_API_KEY", "vault:ACME_GATEWAY_API_KEY"]
|
||||
|
||||
[llm.providers.proxy.extra_headers]
|
||||
x-portkey-api-key = "{{ secrets.portkey_api_key }}"
|
||||
[llm.providers.proxy.metadata.fabro.extra_headers]
|
||||
x-portkey-api-key = "{{ secrets.PORTKEY_API_KEY }}"
|
||||
x-portkey-config = "@bedrock-prod"
|
||||
x-team-secret = "{{ secrets.gateway_team_secret }}"
|
||||
|
||||
[llm.providers.proxy.models."team-code-large"]
|
||||
display_name = "Team Code Large"
|
||||
aliases = ["team-code"]
|
||||
api_model = "provider-wire-model-name"
|
||||
limits = { context_tokens = 200000, max_output_tokens = 32000 }
|
||||
capabilities = { text = true, tools = true, reasoning = true, caching = true, reasoning_effort = { low = true, medium = true, high = true } }
|
||||
protocol_options = { reasoning_effort_levels = true }
|
||||
pricing = { input_usd_micros_per_million = 1500000, output_usd_micros_per_million = 8000000, cached_input_usd_micros_per_million = 300000 }
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".metadata.fabro]
|
||||
family = "team-code"
|
||||
small_default = true
|
||||
estimated_output_tps = 80
|
||||
```
|
||||
|
||||
## `[llm.providers.<id>]`
|
||||
|
||||
Define or override an LLM provider. The keys are the lithos provider record.
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `display_name` | string | provider ID | Human-readable provider name. |
|
||||
| `adapter` | string | built-in value | Adapter registry key, such as `"anthropic"`, `"openai"`, `"gemini"`, or `"openai_compatible"`. Required for new providers. |
|
||||
| `agent_profile` | `"anthropic"` \| `"openai"` \| `"gemini"` | derived from `adapter` | Agent profile used for project memory, CLI/ACP command selection, and native session routing. Override only when a provider needs profile behavior different from its adapter. |
|
||||
| `billing_policy` | `"openai"` \| `"anthropic"` \| `"gemini"` \| `"none"` | derived from `adapter` | Provider-owned billing algorithm for usage estimates. Override for exceptional providers such as local no-billing runtimes. |
|
||||
| `base_url` | string | built-in value or adapter runtime default | Provider API base URL. Required for most custom OpenAI-compatible providers. |
|
||||
| `auth` | table | omitted | API-key auth config. Omit the table entirely for providers that need no API key; any `extra_headers` are still attached. |
|
||||
| `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 literal text or `{{ secrets.NAME }}` interpolation strings. Put credentials in a secret and reference them with a token, not a bare literal. |
|
||||
| `priority` | integer | `0` | Higher-priority ready providers win unqualified model and default selection; ties use canonical provider ID. |
|
||||
| `enabled` | boolean | `true` | Set `false` to disable a provider after lower-precedence layers define it. |
|
||||
| `display_name` | string | required for new providers | Human-readable provider name. |
|
||||
| `adapter` | string | required for new providers | lithos adapter id: `anthropic`, `openai`, `gemini`, `openai-compatible`, or `bedrock`. |
|
||||
| `codec` | string | required for new providers | Wire codec: `anthropic-messages`, `openai-responses`, `openai-chat`, `gemini-generate`, or `bedrock-converse`. |
|
||||
| `base_url` | string | required for new providers | Provider API base URL. The `openai-compatible` adapter appends `/v1/chat/completions` unless the URL already ends in a version segment. |
|
||||
| `auth` | table | required for new providers | Auth scheme: `{ type = "bearer" }`, `{ type = "header", name = "x-api-key" }`, `{ type = "headers" }`, `{ type = "none" }`, or `{ type = "aws" }`. |
|
||||
| `priority` | integer | `0` | Higher-priority ready providers win unqualified model and default selection. |
|
||||
| `aliases` | array<string> | `[]` | Additional provider names accepted by model routing and fallback config. |
|
||||
| `default_model` | string | None | The provider's default model id. |
|
||||
| `allow_passthrough` | boolean | `false` | Whether `provider/model` selectors may name models the catalog does not list. |
|
||||
| `default_headers` | table | `{}` | Literal headers attached to every request. Secret-bearing headers belong in `metadata.fabro.extra_headers`. |
|
||||
|
||||
## `[llm.providers.<provider>.models.<model-slug>]`
|
||||
## `[llm.providers.<id>.metadata.fabro]`
|
||||
|
||||
Fabro's provider policy. Every key is optional.
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `enabled` | boolean | `true` | Set `false` to hide a provider from Fabro. Several built-in providers ship disabled. |
|
||||
| `agent_profile` | `"anthropic"` \| `"openai"` \| `"gemini"` \| `"kimi"` \| `"gpt56"` | derived from `adapter` | Agent profile for models on this provider. |
|
||||
| `api_key_url` | string | None | Where an operator obtains an API key. |
|
||||
| `credentials` | array<string> | `[]` | Ordered credential refs: `vault:<NAME>`, `env:<NAME>`, or `aws_sigv4`. The first that resolves wins. |
|
||||
| `extra_headers` | table | `{}` | Extra request headers. Values are literal text or `{{ secrets.NAME }}` interpolation strings resolved against the vault. |
|
||||
|
||||
## `[llm.providers.<provider>.models.<model-id>]`
|
||||
|
||||
Define or override one provider's offering of a model. The table key is the
|
||||
canonical model slug Fabro users reference. An offering's identity is the
|
||||
pair `(provider, model slug)`, so different providers may use the same slug
|
||||
and aliases. `api_id` is the opaque model string sent to this provider's API
|
||||
and defaults to the exact model slug.
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.proxy.models."team-code-large"]
|
||||
api_id = "provider-wire-model-name"
|
||||
agent_profile = "anthropic"
|
||||
display_name = "Team Code Large"
|
||||
family = "team-code"
|
||||
default = true
|
||||
probe = true
|
||||
enabled = true
|
||||
aliases = ["team-code"]
|
||||
estimated_output_tps = 80
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".limits]
|
||||
context_window = 200000
|
||||
max_output = 32000
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".controls]
|
||||
reasoning_effort = ["low", "medium", "high"]
|
||||
speed = ["fast"]
|
||||
|
||||
[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.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
|
||||
```
|
||||
model id Fabro users reference. An offering's identity is the pair
|
||||
`(provider, model id)`, so different providers may use the same id and
|
||||
aliases. `api_model` is the string sent to the provider and defaults to the id.
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `api_id` | string | model slug | Opaque identifier sent to this provider's API. An explicitly empty value is invalid. |
|
||||
| `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 | required for new models | Human-readable model name. |
|
||||
| `aliases` | array<string> | `[]` | Additional selectors. Aliases may repeat across providers. |
|
||||
| `api_model` | string | model id | Wire model identifier sent to this provider. |
|
||||
| `limits` | `{ context_tokens, max_output_tokens }` | None | Token limits. |
|
||||
| `capabilities` | table | unknown | Per-capability `true`, `false`, or `"unknown"`: `text`, `images`, `audio`, `documents`, `tools`, `reasoning`, `caching`, `cache_routing`, `sampling`, plus `tool_choice = { required, named }`, `response_format = { json_object, json_schema }`, `reasoning_effort = { minimal, low, medium, high, xhigh, max }`, and `speed = { fast, balanced, economical }`. |
|
||||
| `protocol_options` | table | `{}` | Encoding flags: `reasoning_effort_levels`, `cache_breakpoints`, `system_turns`. |
|
||||
| `pricing` | table | None | USD micros per million tokens: `input_usd_micros_per_million`, `output_usd_micros_per_million`, `cached_input_usd_micros_per_million`, `cache_write_usd_micros_per_million`, plus optional `long_context` and `speed` tiers. |
|
||||
|
||||
## `[llm.providers.<provider>.models.<model-id>.metadata.fabro]`
|
||||
|
||||
Fabro's model policy. Every key is optional.
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `enabled` | boolean | `true` | Set `false` to hide a model from Fabro. |
|
||||
| `agent_profile` | profile name | provider profile | Agent profile override for this model. |
|
||||
| `family` | string | model id | Family label for display and grouping. |
|
||||
| `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 selectors accepted by routing and fallback config. Aliases may repeat across providers, but one selector cannot identify two models within the same provider. |
|
||||
| `estimated_output_tps` | number | None | Estimated output tokens per second for catalog display and planning. |
|
||||
|
||||
## `[llm.providers.<provider>.models.<model-slug>.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.providers.<provider>.models.<model-slug>.features]`
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `tools` | boolean | `false` | Whether the model supports tool calls. |
|
||||
| `vision` | boolean | `false` | Whether the model accepts image inputs. |
|
||||
| `reasoning` | boolean | `false` | Whether the model has reasoning behavior. |
|
||||
| `reasoning_by_default` | boolean | effort-capable models: `true`; other models: `false` | Whether requests reason when no `reasoning_effort` is supplied. Set this explicitly for always-reasoning routes that do not expose an effort control, or for effort-capable routes whose provider defaults reasoning off. |
|
||||
| `reasoning_effort` | `"levels"` \| `"always_adaptive"` \| `"none"` | `"none"` | Whether the model endpoint supports a native reasoning-effort parameter. `levels` accepts discrete effort levels; `always_adaptive` accepts effort levels with natively always-on adaptive thinking; `none` has no native effort parameter. |
|
||||
| `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.providers.<provider>.models.<model-slug>.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.providers.<provider>.models.<model-slug>.costs]`
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `input_cost_per_mtok` | number | None | Input cost in USD per million tokens. |
|
||||
| `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.providers.<provider>.models.<model-slug>.costs.speed.<speed>]`
|
||||
|
||||
Per-speed cost overrides use the same keys as
|
||||
`[llm.providers.<provider>.models.<model-slug>.costs]`. Each `<speed>` key
|
||||
must be declared in
|
||||
`[llm.providers.<provider>.models.<model-slug>.controls].speed`.
|
||||
The `standard` speed is implicit and always uses the base cost table.
|
||||
| `knowledge_cutoff` | string | None | Public knowledge cutoff label. |
|
||||
| `estimated_output_tps` | number | None | Estimated output tokens per second. |
|
||||
| `small_default` | boolean | `false` | Preferred for small utility calls such as generated run titles. |
|
||||
| `probe` | boolean | `false` | Preferred for provider connectivity probes. |
|
||||
| `reasoning_by_default` | boolean | reasoning models with effort levels: `true` | Whether requests reason when no `reasoning_effort` is supplied. |
|
||||
|
||||
## `[cli.updates]`
|
||||
|
||||
|
|
|
|||
|
|
@ -214,146 +214,114 @@ url = "https://fabro.example.com/api/v1"
|
|||
|
||||
fn render_manual_llm_catalog(output: &mut String) {
|
||||
output.push_str(
|
||||
r#"## `[llm.providers.<id>]`
|
||||
r#"## `[llm]`
|
||||
|
||||
Define or override an LLM provider. Provider IDs are strings, so custom
|
||||
providers can be added when they use an adapter Fabro already supports.
|
||||
The `[llm]` table is a [lithos-llm](https://docs.rs/lithos-llm) catalog
|
||||
overlay. Fabro builds its model catalog from three layers: the lithos built-in
|
||||
providers and models, Fabro's policy layer, and this table. Later layers win;
|
||||
tables merge key by key and every other value replaces. Fabro does not
|
||||
interpret the table itself. lithos validates it when the catalog is built, and
|
||||
rejects unknown provider or model fields.
|
||||
|
||||
Fabro-specific policy lives under `metadata.fabro` on a provider or model.
|
||||
lithos carries that namespace verbatim.
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.proxy]
|
||||
display_name = "Acme Gateway"
|
||||
adapter = "openai_compatible"
|
||||
adapter = "openai-compatible"
|
||||
codec = "openai-chat"
|
||||
base_url = "https://llm-gateway.example.com/v1"
|
||||
auth = { type = "bearer" }
|
||||
priority = 50
|
||||
enabled = true
|
||||
aliases = ["gateway"]
|
||||
default_model = "team-code-large"
|
||||
|
||||
[llm.providers.proxy.auth]
|
||||
[llm.providers.proxy.metadata.fabro]
|
||||
enabled = true
|
||||
agent_profile = "anthropic"
|
||||
credentials = ["env:ACME_GATEWAY_API_KEY", "vault:ACME_GATEWAY_API_KEY"]
|
||||
|
||||
[llm.providers.proxy.extra_headers]
|
||||
x-portkey-api-key = "{{ secrets.portkey_api_key }}"
|
||||
[llm.providers.proxy.metadata.fabro.extra_headers]
|
||||
x-portkey-api-key = "{{ secrets.PORTKEY_API_KEY }}"
|
||||
x-portkey-config = "@bedrock-prod"
|
||||
x-team-secret = "{{ secrets.gateway_team_secret }}"
|
||||
|
||||
[llm.providers.proxy.models."team-code-large"]
|
||||
display_name = "Team Code Large"
|
||||
aliases = ["team-code"]
|
||||
api_model = "provider-wire-model-name"
|
||||
limits = { context_tokens = 200000, max_output_tokens = 32000 }
|
||||
capabilities = { text = true, tools = true, reasoning = true, caching = true, reasoning_effort = { low = true, medium = true, high = true } }
|
||||
protocol_options = { reasoning_effort_levels = true }
|
||||
pricing = { input_usd_micros_per_million = 1500000, output_usd_micros_per_million = 8000000, cached_input_usd_micros_per_million = 300000 }
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".metadata.fabro]
|
||||
family = "team-code"
|
||||
small_default = true
|
||||
estimated_output_tps = 80
|
||||
```
|
||||
|
||||
## `[llm.providers.<id>]`
|
||||
|
||||
Define or override an LLM provider. The keys are the lithos provider record.
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `display_name` | string | provider ID | Human-readable provider name. |
|
||||
| `adapter` | string | built-in value | Adapter registry key, such as `"anthropic"`, `"openai"`, `"gemini"`, or `"openai_compatible"`. Required for new providers. |
|
||||
| `agent_profile` | `"anthropic"` \| `"openai"` \| `"gemini"` | derived from `adapter` | Agent profile used for project memory, CLI/ACP command selection, and native session routing. Override only when a provider needs profile behavior different from its adapter. |
|
||||
| `billing_policy` | `"openai"` \| `"anthropic"` \| `"gemini"` \| `"none"` | derived from `adapter` | Provider-owned billing algorithm for usage estimates. Override for exceptional providers such as local no-billing runtimes. |
|
||||
| `base_url` | string | built-in value or adapter runtime default | Provider API base URL. Required for most custom OpenAI-compatible providers. |
|
||||
| `auth` | table | omitted | API-key auth config. Omit the table entirely for providers that need no API key; any `extra_headers` are still attached. |
|
||||
| `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 literal text or `{{ secrets.NAME }}` interpolation strings. Put credentials in a secret and reference them with a token, not a bare literal. |
|
||||
| `priority` | integer | `0` | Higher-priority ready providers win unqualified model and default selection; ties use canonical provider ID. |
|
||||
| `enabled` | boolean | `true` | Set `false` to disable a provider after lower-precedence layers define it. |
|
||||
| `display_name` | string | required for new providers | Human-readable provider name. |
|
||||
| `adapter` | string | required for new providers | lithos adapter id: `anthropic`, `openai`, `gemini`, `openai-compatible`, or `bedrock`. |
|
||||
| `codec` | string | required for new providers | Wire codec: `anthropic-messages`, `openai-responses`, `openai-chat`, `gemini-generate`, or `bedrock-converse`. |
|
||||
| `base_url` | string | required for new providers | Provider API base URL. The `openai-compatible` adapter appends `/v1/chat/completions` unless the URL already ends in a version segment. |
|
||||
| `auth` | table | required for new providers | Auth scheme: `{ type = "bearer" }`, `{ type = "header", name = "x-api-key" }`, `{ type = "headers" }`, `{ type = "none" }`, or `{ type = "aws" }`. |
|
||||
| `priority` | integer | `0` | Higher-priority ready providers win unqualified model and default selection. |
|
||||
| `aliases` | array<string> | `[]` | Additional provider names accepted by model routing and fallback config. |
|
||||
| `default_model` | string | None | The provider's default model id. |
|
||||
| `allow_passthrough` | boolean | `false` | Whether `provider/model` selectors may name models the catalog does not list. |
|
||||
| `default_headers` | table | `{}` | Literal headers attached to every request. Secret-bearing headers belong in `metadata.fabro.extra_headers`. |
|
||||
|
||||
## `[llm.providers.<provider>.models.<model-slug>]`
|
||||
## `[llm.providers.<id>.metadata.fabro]`
|
||||
|
||||
Fabro's provider policy. Every key is optional.
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `enabled` | boolean | `true` | Set `false` to hide a provider from Fabro. Several built-in providers ship disabled. |
|
||||
| `agent_profile` | `"anthropic"` \| `"openai"` \| `"gemini"` \| `"kimi"` \| `"gpt56"` | derived from `adapter` | Agent profile for models on this provider. |
|
||||
| `api_key_url` | string | None | Where an operator obtains an API key. |
|
||||
| `credentials` | array<string> | `[]` | Ordered credential refs: `vault:<NAME>`, `env:<NAME>`, or `aws_sigv4`. The first that resolves wins. |
|
||||
| `extra_headers` | table | `{}` | Extra request headers. Values are literal text or `{{ secrets.NAME }}` interpolation strings resolved against the vault. |
|
||||
|
||||
## `[llm.providers.<provider>.models.<model-id>]`
|
||||
|
||||
Define or override one provider's offering of a model. The table key is the
|
||||
canonical model slug Fabro users reference. An offering's identity is the
|
||||
pair `(provider, model slug)`, so different providers may use the same slug
|
||||
and aliases. `api_id` is the opaque model string sent to this provider's API
|
||||
and defaults to the exact model slug.
|
||||
|
||||
```toml title="settings.toml"
|
||||
[llm.providers.proxy.models."team-code-large"]
|
||||
api_id = "provider-wire-model-name"
|
||||
agent_profile = "anthropic"
|
||||
display_name = "Team Code Large"
|
||||
family = "team-code"
|
||||
default = true
|
||||
probe = true
|
||||
enabled = true
|
||||
aliases = ["team-code"]
|
||||
estimated_output_tps = 80
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".limits]
|
||||
context_window = 200000
|
||||
max_output = 32000
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[llm.providers.proxy.models."team-code-large".controls]
|
||||
reasoning_effort = ["low", "medium", "high"]
|
||||
speed = ["fast"]
|
||||
|
||||
[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.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
|
||||
```
|
||||
model id Fabro users reference. An offering's identity is the pair
|
||||
`(provider, model id)`, so different providers may use the same id and
|
||||
aliases. `api_model` is the string sent to the provider and defaults to the id.
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `api_id` | string | model slug | Opaque identifier sent to this provider's API. An explicitly empty value is invalid. |
|
||||
| `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 | required for new models | Human-readable model name. |
|
||||
| `aliases` | array<string> | `[]` | Additional selectors. Aliases may repeat across providers. |
|
||||
| `api_model` | string | model id | Wire model identifier sent to this provider. |
|
||||
| `limits` | `{ context_tokens, max_output_tokens }` | None | Token limits. |
|
||||
| `capabilities` | table | unknown | Per-capability `true`, `false`, or `"unknown"`: `text`, `images`, `audio`, `documents`, `tools`, `reasoning`, `caching`, `cache_routing`, `sampling`, plus `tool_choice = { required, named }`, `response_format = { json_object, json_schema }`, `reasoning_effort = { minimal, low, medium, high, xhigh, max }`, and `speed = { fast, balanced, economical }`. |
|
||||
| `protocol_options` | table | `{}` | Encoding flags: `reasoning_effort_levels`, `cache_breakpoints`, `system_turns`. |
|
||||
| `pricing` | table | None | USD micros per million tokens: `input_usd_micros_per_million`, `output_usd_micros_per_million`, `cached_input_usd_micros_per_million`, `cache_write_usd_micros_per_million`, plus optional `long_context` and `speed` tiers. |
|
||||
|
||||
## `[llm.providers.<provider>.models.<model-id>.metadata.fabro]`
|
||||
|
||||
Fabro's model policy. Every key is optional.
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `enabled` | boolean | `true` | Set `false` to hide a model from Fabro. |
|
||||
| `agent_profile` | profile name | provider profile | Agent profile override for this model. |
|
||||
| `family` | string | model id | Family label for display and grouping. |
|
||||
| `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 selectors accepted by routing and fallback config. Aliases may repeat across providers, but one selector cannot identify two models within the same provider. |
|
||||
| `estimated_output_tps` | number | None | Estimated output tokens per second for catalog display and planning. |
|
||||
|
||||
## `[llm.providers.<provider>.models.<model-slug>.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.providers.<provider>.models.<model-slug>.features]`
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `tools` | boolean | `false` | Whether the model supports tool calls. |
|
||||
| `vision` | boolean | `false` | Whether the model accepts image inputs. |
|
||||
| `reasoning` | boolean | `false` | Whether the model has reasoning behavior. |
|
||||
| `reasoning_by_default` | boolean | effort-capable models: `true`; other models: `false` | Whether requests reason when no `reasoning_effort` is supplied. Set this explicitly for always-reasoning routes that do not expose an effort control, or for effort-capable routes whose provider defaults reasoning off. |
|
||||
| `reasoning_effort` | `"levels"` \| `"always_adaptive"` \| `"none"` | `"none"` | Whether the model endpoint supports a native reasoning-effort parameter. `levels` accepts discrete effort levels; `always_adaptive` accepts effort levels with natively always-on adaptive thinking; `none` has no native effort parameter. |
|
||||
| `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.providers.<provider>.models.<model-slug>.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.providers.<provider>.models.<model-slug>.costs]`
|
||||
|
||||
| Key | Type / values | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `input_cost_per_mtok` | number | None | Input cost in USD per million tokens. |
|
||||
| `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.providers.<provider>.models.<model-slug>.costs.speed.<speed>]`
|
||||
|
||||
Per-speed cost overrides use the same keys as
|
||||
`[llm.providers.<provider>.models.<model-slug>.costs]`. Each `<speed>` key
|
||||
must be declared in
|
||||
`[llm.providers.<provider>.models.<model-slug>.controls].speed`.
|
||||
The `standard` speed is implicit and always uses the base cost table.
|
||||
| `knowledge_cutoff` | string | None | Public knowledge cutoff label. |
|
||||
| `estimated_output_tps` | number | None | Estimated output tokens per second. |
|
||||
| `small_default` | boolean | `false` | Preferred for small utility calls such as generated run titles. |
|
||||
| `probe` | boolean | `false` | Preferred for provider connectivity probes. |
|
||||
| `reasoning_by_default` | boolean | reasoning models with effort levels: `true` | Whether requests reason when no `reasoning_effort` is supplied. |
|
||||
|
||||
"#,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,48 +8,6 @@ use walkdir::WalkDir;
|
|||
|
||||
use crate::workspace_root;
|
||||
|
||||
/// `fabro_model::bootstrap_catalog` (and its module) is the install/API-key
|
||||
/// validation hatch from the settings-driven LLM catalog plan. It must
|
||||
/// **not** appear in request-serving paths — server handlers, workflow
|
||||
/// operations, agent runtime, hooks, or completion handlers — because those
|
||||
/// must use the resolved `Arc<Catalog>` threaded through their state.
|
||||
///
|
||||
/// The allowed-callers list below is the policy boundary. Adding a new
|
||||
/// caller is intentional and requires updating this list.
|
||||
///
|
||||
/// The walker only descends into `lib/`, so non-`lib/` paths (docs, top-level
|
||||
/// markdown) are not part of the allowlist.
|
||||
const BOOTSTRAP_CATALOG_ALLOWED_PATH_FRAGMENTS: &[&str] = &[
|
||||
// The bootstrap module itself.
|
||||
"lib/foundation/fabro-model/src/bootstrap_catalog",
|
||||
// Public module declaration for the bootstrap hatch.
|
||||
"lib/foundation/fabro-model/src/lib.rs",
|
||||
// Install / first-run / API-key validation flows that legitimately need
|
||||
// a built-in catalog before any project settings have been loaded.
|
||||
"lib/components/fabro-install/",
|
||||
"lib/apps/fabro-cli/src/commands/install/",
|
||||
"lib/apps/fabro-cli/src/shared/install_",
|
||||
"lib/apps/fabro-cli/src/shared/api_key_validation",
|
||||
// Test support modules.
|
||||
"tests/",
|
||||
"test_support",
|
||||
"/tests/it/",
|
||||
"/tests/policy.rs",
|
||||
];
|
||||
|
||||
/// Production runtime code should build catalogs from resolved settings and
|
||||
/// thread the resulting `Arc<Catalog>` through state. Direct use of
|
||||
/// `Catalog::builtin()` is reserved for `fabro-model` internals and tests.
|
||||
const CATALOG_BUILTIN_ALLOWED_PATH_FRAGMENTS: &[&str] = &[
|
||||
// The catalog owner may define and test the built-in/default catalog.
|
||||
"lib/foundation/fabro-model/",
|
||||
// Tests and test support may use built-ins as fixtures.
|
||||
"/tests/",
|
||||
"/tests/it/",
|
||||
"test_support",
|
||||
"/tests/policy.rs",
|
||||
];
|
||||
|
||||
const TEMPLATE_RENDER_ALLOWED_PATH_FRAGMENTS: &[&str] = &[
|
||||
// The template crate owns the rendering API and its tests.
|
||||
"lib/foundation/fabro-template/src/lib.rs",
|
||||
|
|
@ -70,32 +28,6 @@ const TEMPLATE_RENDER_FORBIDDEN_PATTERNS: &[&str] = &[
|
|||
"fabro_template::{",
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn bootstrap_catalog_references_stay_in_allowlist() {
|
||||
let violations = source_symbol_violations(
|
||||
"bootstrap_catalog",
|
||||
BOOTSTRAP_CATALOG_ALLOWED_PATH_FRAGMENTS,
|
||||
);
|
||||
|
||||
assert!(
|
||||
violations.is_empty(),
|
||||
"bootstrap_catalog (install-only) referenced from non-allowlisted source files:\n{}\n\nIf this is intentional, add the path fragment to BOOTSTRAP_CATALOG_ALLOWED_PATH_FRAGMENTS in lib/foundation/fabro-dev/tests/it/policy.rs.",
|
||||
format_violations(violations),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalog_builtin_references_stay_in_allowlist() {
|
||||
let violations =
|
||||
source_symbol_violations("Catalog::builtin()", CATALOG_BUILTIN_ALLOWED_PATH_FRAGMENTS);
|
||||
|
||||
assert!(
|
||||
violations.is_empty(),
|
||||
"Catalog::builtin() referenced from non-allowlisted production source files:\n{}\n\nRuntime code should use a resolved settings catalog via `Catalog::from_builtin_with_overrides(...)` or an injected `Arc<Catalog>`. If this is intentional test/bootstrap code, add the path fragment to CATALOG_BUILTIN_ALLOWED_PATH_FRAGMENTS in lib/foundation/fabro-dev/tests/it/policy.rs.",
|
||||
format_violations(violations),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workflow_template_rendering_call_sites_stay_in_allowlist() {
|
||||
let mut violations = Vec::new();
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
[package]
|
||||
name = "fabro-model"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
publish = false
|
||||
license.workspace = true
|
||||
description = "LLM model catalog: provider identity, model metadata, and resolution"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
clap = ["dep:clap"]
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true, optional = true }
|
||||
fabro-static.workspace = true
|
||||
http = "1"
|
||||
rust-embed.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
insta.workspace = true
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
//! Adapter registry keys shared by the model catalog and LLM factories.
|
||||
//!
|
||||
//! Provider/model catalog rows parse adapter strings into [`AdapterKind`].
|
||||
//! Runtime code should carry the typed kind instead of re-matching on strings.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumString, IntoStaticStr, VariantArray};
|
||||
|
||||
/// Stable adapter identity for protocol/client behavior.
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
IntoStaticStr,
|
||||
VariantArray,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum AdapterKind {
|
||||
Anthropic,
|
||||
#[serde(rename = "openai")]
|
||||
#[strum(to_string = "openai")]
|
||||
OpenAi,
|
||||
Gemini,
|
||||
#[serde(rename = "openai_compatible")]
|
||||
#[strum(to_string = "openai_compatible")]
|
||||
OpenAiCompatible,
|
||||
Bedrock,
|
||||
}
|
||||
|
||||
impl AdapterKind {
|
||||
#[must_use]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for AdapterKind {
|
||||
fn as_ref(&self) -> &str {
|
||||
(*self).as_str()
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal dispatch key that `fabro-agent` maps to a concrete agent profile.
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
IntoStaticStr,
|
||||
VariantArray,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum AgentProfileKind {
|
||||
Anthropic,
|
||||
/// Claude 5 models trained against Anthropic's current coding-agent
|
||||
/// harness. This remains model-scoped so older Claude models keep the
|
||||
/// established Anthropic profile.
|
||||
#[serde(rename = "claude-5")]
|
||||
#[strum(to_string = "claude-5")]
|
||||
Claude5,
|
||||
#[serde(rename = "openai")]
|
||||
#[strum(to_string = "openai")]
|
||||
OpenAi,
|
||||
Gemini,
|
||||
/// Kimi (Moonshot) models, wherever they are served from. Selected per
|
||||
/// model rather than per provider, so a Kimi model reached through a
|
||||
/// gateway such as OpenRouter gets the same profile as one reached
|
||||
/// directly at `api.moonshot.ai`.
|
||||
Kimi,
|
||||
/// GPT-5.6 models (Sol, Terra, Luna), which Codex drives with a narrower
|
||||
/// core tool set than earlier GPT models: a shell, a file editor, and
|
||||
/// `update_plan`, plus optional web search. The profile omits dedicated
|
||||
/// file-read, discovery, and fetch tools. Selected per model rather than
|
||||
/// per provider, so other models on the `openai` provider keep
|
||||
/// [`Self::OpenAi`].
|
||||
Gpt56,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn adapter_kind_round_trips_as_snake_case() {
|
||||
for kind in AdapterKind::VARIANTS {
|
||||
let json = serde_json::to_string(kind).unwrap();
|
||||
assert_eq!(json, format!("\"{}\"", kind.as_str()));
|
||||
let parsed: AdapterKind = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed, *kind);
|
||||
assert_eq!(kind.as_str().parse::<AdapterKind>().unwrap(), *kind);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bedrock_adapter_kind_roundtrips() {
|
||||
assert_eq!(AdapterKind::Bedrock.as_str(), "bedrock");
|
||||
assert_eq!(
|
||||
"bedrock".parse::<AdapterKind>().unwrap(),
|
||||
AdapterKind::Bedrock
|
||||
);
|
||||
assert!(AdapterKind::VARIANTS.contains(&AdapterKind::Bedrock));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_profile_kind_round_trips_as_settings_strings() {
|
||||
for kind in AgentProfileKind::VARIANTS {
|
||||
let expected = kind.to_string();
|
||||
let json = serde_json::to_string(&kind).unwrap();
|
||||
assert_eq!(json, format!("\"{expected}\""));
|
||||
let parsed: AgentProfileKind = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed, *kind);
|
||||
assert_eq!(expected.parse::<AgentProfileKind>().unwrap(), *kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,22 +0,0 @@
|
|||
//! Install/API-key validation access to the built-in catalog.
|
||||
//!
|
||||
//! Runtime request-serving paths should use a resolved catalog threaded
|
||||
//! through their state. This module is the explicit hatch for setup flows that
|
||||
//! need built-in provider/model metadata before project settings are loaded.
|
||||
|
||||
use crate::Catalog;
|
||||
|
||||
#[must_use]
|
||||
pub fn catalog() -> &'static Catalog {
|
||||
Catalog::builtin()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bootstrap_catalog_is_the_builtin_catalog() {
|
||||
assert!(std::ptr::eq(catalog(), Catalog::builtin()));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,261 +0,0 @@
|
|||
[providers.anthropic]
|
||||
display_name = "Anthropic"
|
||||
adapter = "anthropic"
|
||||
api_key_url = "https://console.anthropic.com/settings/keys"
|
||||
base_url = "https://api.anthropic.com/v1"
|
||||
priority = 100
|
||||
|
||||
[providers.anthropic.auth]
|
||||
credentials = ["env:ANTHROPIC_API_KEY", "vault:ANTHROPIC_API_KEY"]
|
||||
header = { custom = "x-api-key" }
|
||||
|
||||
[providers.anthropic.models."claude-fable-5"]
|
||||
display_name = "Claude Fable 5"
|
||||
family = "claude-5"
|
||||
aliases = ["fable", "claude-fable"]
|
||||
agent_profile = "claude-5"
|
||||
|
||||
[providers.anthropic.models."claude-fable-5".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[providers.anthropic.models."claude-fable-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "always_adaptive"
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[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
|
||||
|
||||
[providers.anthropic.models."claude-opus-5"]
|
||||
display_name = "Claude Opus 5"
|
||||
family = "claude-5"
|
||||
training = "2026-05-01"
|
||||
knowledge_cutoff = "May 2026"
|
||||
aliases = ["opus", "claude-opus"]
|
||||
agent_profile = "claude-5"
|
||||
|
||||
[providers.anthropic.models."claude-opus-5".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[providers.anthropic.models."claude-opus-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.anthropic.models."claude-opus-5".controls]
|
||||
speed = ["fast"]
|
||||
|
||||
[providers.anthropic.models."claude-opus-5".costs]
|
||||
input_cost_per_mtok = 5.0
|
||||
output_cost_per_mtok = 25.0
|
||||
cache_input_cost_per_mtok = 0.5
|
||||
|
||||
[providers.anthropic.models."claude-opus-5".costs.speed.fast]
|
||||
input_cost_per_mtok = 10.0
|
||||
output_cost_per_mtok = 50.0
|
||||
cache_input_cost_per_mtok = 1.0
|
||||
|
||||
[providers.anthropic.models."claude-sonnet-5"]
|
||||
display_name = "Claude Sonnet 5"
|
||||
family = "claude-5"
|
||||
training = "2026-01-01"
|
||||
knowledge_cutoff = "Jan 2026"
|
||||
default = true
|
||||
aliases = ["sonnet", "claude-sonnet"]
|
||||
agent_profile = "claude-5"
|
||||
|
||||
[providers.anthropic.models."claude-sonnet-5".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[providers.anthropic.models."claude-sonnet-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
# Introductory pricing through August 31, 2026.
|
||||
[providers.anthropic.models."claude-sonnet-5".costs]
|
||||
input_cost_per_mtok = 2.0
|
||||
output_cost_per_mtok = 10.0
|
||||
cache_input_cost_per_mtok = 0.2
|
||||
|
||||
[providers.anthropic.models."claude-opus-4-8"]
|
||||
display_name = "Claude Opus 4.8"
|
||||
family = "claude-4"
|
||||
training = "2026-01-01"
|
||||
knowledge_cutoff = "Jan 2026"
|
||||
estimated_output_tps = 25
|
||||
|
||||
[providers.anthropic.models."claude-opus-4-8".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[providers.anthropic.models."claude-opus-4-8".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.anthropic.models."claude-opus-4-8".controls]
|
||||
speed = ["fast"]
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[providers.anthropic.models."claude-opus-4-7".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[providers.anthropic.models."claude-opus-4-7".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.anthropic.models."claude-opus-4-7".controls]
|
||||
speed = ["fast"]
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[providers.anthropic.models."claude-opus-4-6".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[providers.anthropic.models."claude-opus-4-6".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[providers.anthropic.models."claude-opus-4-6".controls]
|
||||
speed = ["fast"]
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[providers.anthropic.models."claude-sonnet-4-5".limits]
|
||||
context_window = 200000
|
||||
max_output = 64000
|
||||
|
||||
[providers.anthropic.models."claude-sonnet-4-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
|
||||
[providers.anthropic.models."claude-sonnet-4-5".controls]
|
||||
reasoning_effort = ["low", "medium", "high", "xhigh", "max"]
|
||||
|
||||
[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
|
||||
|
||||
[providers.anthropic.models."claude-sonnet-4-6"]
|
||||
display_name = "Claude Sonnet 4.6"
|
||||
family = "claude-4"
|
||||
training = "2025-08-01"
|
||||
knowledge_cutoff = "May 2025"
|
||||
estimated_output_tps = 50
|
||||
|
||||
[providers.anthropic.models."claude-sonnet-4-6".limits]
|
||||
context_window = 200000
|
||||
max_output = 64000
|
||||
|
||||
[providers.anthropic.models."claude-sonnet-4-6".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[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
|
||||
|
||||
[providers.anthropic.models."claude-haiku-4-5"]
|
||||
display_name = "Claude Haiku 4.5"
|
||||
family = "claude-4"
|
||||
training = "2025-08-01"
|
||||
knowledge_cutoff = "May 2025"
|
||||
estimated_output_tps = 100
|
||||
aliases = ["haiku", "claude-haiku"]
|
||||
probe = true
|
||||
small_default = true
|
||||
|
||||
[providers.anthropic.models."claude-haiku-4-5".limits]
|
||||
context_window = 200000
|
||||
max_output = 8192
|
||||
|
||||
[providers.anthropic.models."claude-haiku-4-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
prompt_cache = true
|
||||
|
||||
[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
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
[providers.bedrock-openai]
|
||||
display_name = "Amazon Bedrock (OpenAI frontier)"
|
||||
adapter = "openai"
|
||||
api_key_url = "https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html"
|
||||
base_url = "https://bedrock-mantle.us-east-1.api.aws/openai/v1"
|
||||
priority = 19
|
||||
enabled = false
|
||||
|
||||
[providers.bedrock-openai.auth]
|
||||
credentials = [
|
||||
"env:AWS_BEARER_TOKEN_BEDROCK",
|
||||
"env:BEDROCK_API_KEY",
|
||||
"vault:AWS_BEARER_TOKEN_BEDROCK",
|
||||
"vault:BEDROCK_API_KEY",
|
||||
]
|
||||
|
||||
# OpenAI's frontier models on Bedrock (GPT-5.5/5.4) are served ONLY by the
|
||||
# bedrock-mantle endpoint's OpenAI Responses API — they are not reachable
|
||||
# through Converse or InvokeModel on bedrock-runtime. That surface speaks
|
||||
# the openai_responses dialect with a Bedrock API key as the bearer token,
|
||||
# so this companion provider row is pure configuration over the existing
|
||||
# openai adapter: same AWS account and key as the `bedrock` provider, a
|
||||
# different endpoint and wire dialect.
|
||||
#
|
||||
# Notes:
|
||||
# - Auth is Bedrock-API-key only on this row (SigV4 on mantle uses the
|
||||
# `bedrock-mantle` signing name, which the openai adapter does not do).
|
||||
# - bedrock-mantle is regional (13 regions); change base_url to
|
||||
# `https://bedrock-mantle.<region>.api.aws/openai/v1` as needed.
|
||||
# - Responses state: Fabro always sends `store: false`, so nothing is
|
||||
# retained under mantle's default 30-day Project retention.
|
||||
#
|
||||
# To enable, add to ~/.fabro/settings.toml:
|
||||
#
|
||||
# [llm.providers.bedrock-openai]
|
||||
# enabled = true
|
||||
|
||||
[providers.bedrock-openai.models."gpt-5.5"]
|
||||
api_id = "openai.gpt-5.5"
|
||||
display_name = "GPT-5.5 (Bedrock)"
|
||||
family = "gpt-5"
|
||||
default = true
|
||||
|
||||
[providers.bedrock-openai.models."gpt-5.5".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[providers.bedrock-openai.models."gpt-5.5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[providers.bedrock-openai.models."gpt-5.5".costs]
|
||||
input_cost_per_mtok = 5.5
|
||||
output_cost_per_mtok = 33.0
|
||||
|
||||
[providers.bedrock-openai.models."gpt-5.4"]
|
||||
api_id = "openai.gpt-5.4"
|
||||
display_name = "GPT-5.4 (Bedrock)"
|
||||
family = "gpt-5"
|
||||
|
||||
[providers.bedrock-openai.models."gpt-5.4".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[providers.bedrock-openai.models."gpt-5.4".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[providers.bedrock-openai.models."gpt-5.4".costs]
|
||||
input_cost_per_mtok = 2.75
|
||||
output_cost_per_mtok = 16.5
|
||||
|
|
@ -1,409 +0,0 @@
|
|||
[providers.bedrock]
|
||||
display_name = "Amazon Bedrock"
|
||||
adapter = "bedrock"
|
||||
api_key_url = "https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html"
|
||||
base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
|
||||
priority = 20
|
||||
enabled = false
|
||||
|
||||
[providers.bedrock.auth]
|
||||
# An explicit Bedrock API key wins (from the process env, or the server
|
||||
# vault via `fabro secret set <name>`, matching every other provider's
|
||||
# env-then-vault order); SigV4 (the AWS default credential chain, resolved
|
||||
# at request time) is the fallback. `aws_sigv4` always resolves, which is
|
||||
# why this provider ships disabled: enabling it is the operator's statement
|
||||
# that AWS credentials are expected to work. The key is read from either
|
||||
# `AWS_BEARER_TOKEN_BEDROCK` (the AWS-canonical name, also honored by the
|
||||
# AWS SDKs/CLI) or `BEDROCK_API_KEY` (Fabro's `<PROVIDER>_API_KEY`
|
||||
# convention); the env names are checked before the vault.
|
||||
credentials = [
|
||||
"env:AWS_BEARER_TOKEN_BEDROCK",
|
||||
"env:BEDROCK_API_KEY",
|
||||
"vault:AWS_BEARER_TOKEN_BEDROCK",
|
||||
"vault:BEDROCK_API_KEY",
|
||||
"aws_sigv4",
|
||||
]
|
||||
|
||||
# To enable Bedrock, add the following to ~/.fabro/settings.toml:
|
||||
#
|
||||
# [llm.providers.bedrock]
|
||||
# enabled = true
|
||||
# base_url = "https://bedrock-runtime.<your-region>.amazonaws.com"
|
||||
#
|
||||
# The signing region is derived from the base_url. Authenticate with
|
||||
# either a Bedrock API key (AWS_BEARER_TOKEN_BEDROCK or BEDROCK_API_KEY) or
|
||||
# any AWS default credential chain source (env keys, profile, IMDS, IRSA, SSO).
|
||||
#
|
||||
# Model ids use cross-region inference profiles (`us.` / `global.`
|
||||
# prefixes) where on-demand access requires them. Pricing rows are
|
||||
# best-effort estimates from June 2026 list prices.
|
||||
|
||||
# ---------- Anthropic Claude ----------
|
||||
#
|
||||
# Claude bills Anthropic-style cache reads/writes, so these rows override
|
||||
# the provider's billing default. Claude 5 models appear at the end of this
|
||||
# file because their Bedrock deployments pin sampling parameters and require
|
||||
# extra endpoint-specific handling.
|
||||
|
||||
[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"
|
||||
|
||||
[providers.bedrock.models."claude-sonnet-4-6".limits]
|
||||
context_window = 1000000
|
||||
max_output = 64000
|
||||
|
||||
[providers.bedrock.models."claude-sonnet-4-6".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
|
||||
[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
|
||||
|
||||
[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"
|
||||
|
||||
[providers.bedrock.models."claude-opus-4-8".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[providers.bedrock.models."claude-opus-4-8".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[providers.bedrock.models."claude-haiku-4-5".limits]
|
||||
context_window = 200000
|
||||
max_output = 64000
|
||||
|
||||
[providers.bedrock.models."claude-haiku-4-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
prompt_cache = true
|
||||
|
||||
[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
|
||||
|
||||
# ---------- OpenAI open-weights ----------
|
||||
#
|
||||
# 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.
|
||||
|
||||
[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"
|
||||
|
||||
[providers.bedrock.models."gpt-oss-120b".limits]
|
||||
context_window = 128000
|
||||
max_output = 16384
|
||||
|
||||
[providers.bedrock.models."gpt-oss-120b".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
|
||||
[providers.bedrock.models."gpt-oss-120b".costs]
|
||||
input_cost_per_mtok = 0.15
|
||||
output_cost_per_mtok = 0.60
|
||||
|
||||
[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"
|
||||
|
||||
[providers.bedrock.models."gpt-oss-20b".limits]
|
||||
context_window = 128000
|
||||
max_output = 16384
|
||||
|
||||
[providers.bedrock.models."gpt-oss-20b".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
|
||||
[providers.bedrock.models."gpt-oss-20b".costs]
|
||||
input_cost_per_mtok = 0.07
|
||||
output_cost_per_mtok = 0.30
|
||||
|
||||
# ---------- Amazon Nova ----------
|
||||
|
||||
[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"
|
||||
|
||||
[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
|
||||
|
||||
[providers.bedrock.models."nova-2-lite".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
|
||||
[providers.bedrock.models."nova-2-lite".costs]
|
||||
input_cost_per_mtok = 0.30
|
||||
output_cost_per_mtok = 2.50
|
||||
|
||||
# ---------- Open-weights ----------
|
||||
|
||||
[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"
|
||||
|
||||
[providers.bedrock.models."llama-4-maverick".limits]
|
||||
context_window = 1000000
|
||||
max_output = 8192
|
||||
|
||||
[providers.bedrock.models."llama-4-maverick".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
|
||||
[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"
|
||||
|
||||
[providers.bedrock.models."mistral-large-3".limits]
|
||||
context_window = 256000
|
||||
max_output = 32768
|
||||
|
||||
[providers.bedrock.models."mistral-large-3".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
|
||||
[providers.bedrock.models."mistral-large-3".costs]
|
||||
input_cost_per_mtok = 0.50
|
||||
output_cost_per_mtok = 1.50
|
||||
|
||||
[providers.bedrock.models."devstral-2"]
|
||||
api_id = "mistral.devstral-2-123b"
|
||||
display_name = "Devstral 2 (Bedrock)"
|
||||
family = "devstral"
|
||||
billing_policy = "openai"
|
||||
agent_profile = "openai"
|
||||
|
||||
[providers.bedrock.models."devstral-2".limits]
|
||||
context_window = 256000
|
||||
max_output = 32768
|
||||
|
||||
[providers.bedrock.models."devstral-2".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[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"
|
||||
|
||||
[providers.bedrock.models."deepseek-v3.2".limits]
|
||||
context_window = 164000
|
||||
max_output = 8192
|
||||
|
||||
[providers.bedrock.models."deepseek-v3.2".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
|
||||
[providers.bedrock.models."deepseek-v3.2".costs]
|
||||
input_cost_per_mtok = 0.62
|
||||
output_cost_per_mtok = 1.85
|
||||
|
||||
# Qwen3 Coder Next: omitted pending a verified Bedrock model/inference-profile
|
||||
# id. The fabro id is not itself a valid Bedrock identifier (Converse returns
|
||||
# "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:
|
||||
# [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"
|
||||
# [providers.bedrock.models."qwen3-coder-next".limits]
|
||||
# context_window = 256000
|
||||
# max_output = 16384
|
||||
# [providers.bedrock.models."qwen3-coder-next".features]
|
||||
# tools = true
|
||||
|
||||
[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"
|
||||
|
||||
[providers.bedrock.models."kimi-k2.5".limits]
|
||||
context_window = 262144
|
||||
max_output = 16384
|
||||
|
||||
[providers.bedrock.models."kimi-k2.5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
|
||||
[providers.bedrock.models."kimi-k2.5".costs]
|
||||
input_cost_per_mtok = 0.60
|
||||
output_cost_per_mtok = 3.00
|
||||
|
||||
[providers.bedrock.models."glm-5"]
|
||||
api_id = "zai.glm-5"
|
||||
display_name = "GLM 5 (Bedrock)"
|
||||
family = "glm"
|
||||
billing_policy = "openai"
|
||||
agent_profile = "openai"
|
||||
|
||||
[providers.bedrock.models."glm-5".limits]
|
||||
context_window = 200000
|
||||
max_output = 128000
|
||||
|
||||
[providers.bedrock.models."glm-5".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[providers.bedrock.models."glm-5".costs]
|
||||
input_cost_per_mtok = 1.00
|
||||
output_cost_per_mtok = 3.20
|
||||
|
||||
[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"
|
||||
|
||||
[providers.bedrock.models."minimax-m2.5".limits]
|
||||
context_window = 196000
|
||||
max_output = 8192
|
||||
|
||||
[providers.bedrock.models."minimax-m2.5".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[providers.bedrock.models."minimax-m2.5".costs]
|
||||
input_cost_per_mtok = 0.30
|
||||
output_cost_per_mtok = 1.20
|
||||
|
||||
[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"
|
||||
|
||||
[providers.bedrock.models."nemotron-3-super".limits]
|
||||
context_window = 256000
|
||||
max_output = 32768
|
||||
|
||||
[providers.bedrock.models."nemotron-3-super".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
# Claude Fable 5: adaptive thinking is always on server-side; the row pins
|
||||
# sampling_params = false so the Converse encoder drops temperature/top_p
|
||||
# (Bedrock rejects them for this model). Requires the account-level
|
||||
# provider_data_share opt-in in the Bedrock console. Effort-level mapping
|
||||
# through additionalModelRequestFields is a named follow-up, so
|
||||
# reasoning_effort stays undeclared here (requests carrying one are
|
||||
# rejected up front rather than silently dropped).
|
||||
|
||||
[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"
|
||||
agent_profile = "claude-5"
|
||||
|
||||
[providers.bedrock.models."claude-fable-5".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[providers.bedrock.models."claude-fable-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[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
|
||||
|
||||
# Claude Sonnet 5 uses adaptive thinking by default and rejects non-default
|
||||
# sampling parameters. Effort-level mapping through
|
||||
# additionalModelRequestFields is a named follow-up, as for Fable 5.
|
||||
|
||||
[providers.bedrock.models."claude-sonnet-5"]
|
||||
api_id = "us.anthropic.claude-sonnet-5"
|
||||
display_name = "Claude Sonnet 5 (Bedrock)"
|
||||
family = "claude-5"
|
||||
billing_policy = "anthropic"
|
||||
default = true
|
||||
agent_profile = "claude-5"
|
||||
|
||||
[providers.bedrock.models."claude-sonnet-5".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[providers.bedrock.models."claude-sonnet-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
# Introductory pricing through August 31, 2026.
|
||||
[providers.bedrock.models."claude-sonnet-5".costs]
|
||||
input_cost_per_mtok = 2.0
|
||||
output_cost_per_mtok = 10.0
|
||||
cache_input_cost_per_mtok = 0.2
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
[providers.deepseek]
|
||||
display_name = "DeepSeek"
|
||||
adapter = "openai_compatible"
|
||||
api_key_url = "https://platform.deepseek.com/api_keys"
|
||||
base_url = "https://api.deepseek.com"
|
||||
priority = 75
|
||||
|
||||
[providers.deepseek.auth]
|
||||
credentials = ["env:DEEPSEEK_API_KEY", "vault:DEEPSEEK_API_KEY"]
|
||||
|
||||
# DeepSeek V4 uses thinking mode by default. The API accepts sampling
|
||||
# parameters in that mode but ignores them, so Fabro omits those parameters.
|
||||
# Prompt caching is automatic and usage reports prompt_cache_hit_tokens.
|
||||
# Prices are from api-docs.deepseek.com/quick_start/pricing, verified
|
||||
# 2026-07-31.
|
||||
|
||||
[providers.deepseek.models."deepseek-v4-flash"]
|
||||
display_name = "DeepSeek V4 Flash"
|
||||
family = "deepseek-v4"
|
||||
aliases = ["deepseek-v4", "deepseek", "deepseek-flash"]
|
||||
# DeepSeek uses a general coding prompt and standard JSON function tools.
|
||||
agent_profile = "openai"
|
||||
default = true
|
||||
small_default = true
|
||||
probe = true
|
||||
|
||||
[providers.deepseek.models."deepseek-v4-flash".limits]
|
||||
context_window = 1048576
|
||||
max_output = 384000
|
||||
|
||||
[providers.deepseek.models."deepseek-v4-flash".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.deepseek.models."deepseek-v4-flash".controls]
|
||||
reasoning_effort = ["low", "high", "max"]
|
||||
|
||||
[providers.deepseek.models."deepseek-v4-flash".costs]
|
||||
input_cost_per_mtok = 0.14
|
||||
output_cost_per_mtok = 0.28
|
||||
cache_input_cost_per_mtok = 0.0028
|
||||
|
||||
[providers.deepseek.models."deepseek-v4-pro"]
|
||||
display_name = "DeepSeek V4 Pro"
|
||||
family = "deepseek-v4"
|
||||
agent_profile = "openai"
|
||||
|
||||
[providers.deepseek.models."deepseek-v4-pro".limits]
|
||||
context_window = 1048576
|
||||
max_output = 384000
|
||||
|
||||
[providers.deepseek.models."deepseek-v4-pro".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.deepseek.models."deepseek-v4-pro".controls]
|
||||
# V4 Pro currently maps low to high. Keep only its distinct effort levels;
|
||||
# DeepSeek says it plans to change Pro's mapping in early August 2026.
|
||||
reasoning_effort = ["high", "max"]
|
||||
|
||||
[providers.deepseek.models."deepseek-v4-pro".costs]
|
||||
input_cost_per_mtok = 0.435
|
||||
output_cost_per_mtok = 0.87
|
||||
cache_input_cost_per_mtok = 0.003625
|
||||
|
|
@ -1,281 +0,0 @@
|
|||
[providers.fireworks]
|
||||
display_name = "Fireworks AI"
|
||||
adapter = "openai_compatible"
|
||||
api_key_url = "https://app.fireworks.ai/settings/users/api-keys"
|
||||
base_url = "https://api.fireworks.ai/inference/v1"
|
||||
priority = 30
|
||||
enabled = false
|
||||
|
||||
[providers.fireworks.auth]
|
||||
credentials = ["env:FIREWORKS_API_KEY", "vault:FIREWORKS_API_KEY"]
|
||||
|
||||
# To enable Fireworks, add the following to ~/.fabro/settings.toml:
|
||||
#
|
||||
# [llm.providers.fireworks]
|
||||
# enabled = true
|
||||
#
|
||||
# Then run `fabro provider login --provider fireworks` to store the API key,
|
||||
# or set the FIREWORKS_API_KEY environment variable.
|
||||
#
|
||||
# api_id values use Fireworks account-scoped paths; dots in upstream model
|
||||
# names become "p" (glm-5.2 -> glm-5p2). `GET /v1/models` only returns a
|
||||
# featured subset of serverless models, so validate api_ids against
|
||||
# /chat/completions, not the models list.
|
||||
#
|
||||
# Prompt caching is automatic prefix caching (no cache_control breakpoints);
|
||||
# serverless responses report prompt_tokens_details.cached_tokens in the
|
||||
# usage body. Costs below are from docs.fireworks.ai/serverless/pricing,
|
||||
# verified 2026-08-04. Rows use standard-tier prices unless noted otherwise.
|
||||
|
||||
[providers.fireworks.models."kimi-k3"]
|
||||
api_id = "accounts/fireworks/models/kimi-k3"
|
||||
display_name = "Kimi K3"
|
||||
family = "kimi-k3"
|
||||
agent_profile = "kimi"
|
||||
|
||||
[providers.fireworks.models."kimi-k3".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[providers.fireworks.models."kimi-k3".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "always_adaptive"
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.fireworks.models."kimi-k3".controls]
|
||||
reasoning_effort = ["low", "medium", "high"]
|
||||
|
||||
[providers.fireworks.models."kimi-k3".costs]
|
||||
input_cost_per_mtok = 3.0
|
||||
output_cost_per_mtok = 15.0
|
||||
cache_input_cost_per_mtok = 0.3
|
||||
|
||||
# Fireworks exposes the Fast tier through a separate router model ID. Its
|
||||
# published prices are 50% above the standard Kimi K3 rates.
|
||||
[providers.fireworks.models."kimi-k3-fast"]
|
||||
api_id = "accounts/fireworks/routers/kimi-k3-fast"
|
||||
display_name = "Kimi K3 Fast"
|
||||
family = "kimi-k3"
|
||||
agent_profile = "kimi"
|
||||
|
||||
[providers.fireworks.models."kimi-k3-fast".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[providers.fireworks.models."kimi-k3-fast".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "always_adaptive"
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.fireworks.models."kimi-k3-fast".controls]
|
||||
reasoning_effort = ["low", "medium", "high"]
|
||||
|
||||
[providers.fireworks.models."kimi-k3-fast".costs]
|
||||
input_cost_per_mtok = 4.5
|
||||
output_cost_per_mtok = 22.5
|
||||
cache_input_cost_per_mtok = 0.45
|
||||
|
||||
[providers.fireworks.models."kimi-k2.7-code"]
|
||||
api_id = "accounts/fireworks/models/kimi-k2p7-code"
|
||||
display_name = "Kimi K2.7 Code"
|
||||
family = "kimi-k2"
|
||||
default = true
|
||||
|
||||
[providers.fireworks.models."kimi-k2.7-code".limits]
|
||||
context_window = 262144
|
||||
max_output = 32768
|
||||
|
||||
[providers.fireworks.models."kimi-k2.7-code".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
|
||||
[providers.fireworks.models."kimi-k2.7-code".costs]
|
||||
input_cost_per_mtok = 0.95
|
||||
output_cost_per_mtok = 4.0
|
||||
cache_input_cost_per_mtok = 0.19
|
||||
|
||||
[providers.fireworks.models."kimi-k2.6"]
|
||||
api_id = "accounts/fireworks/models/kimi-k2p6"
|
||||
display_name = "Kimi K2.6"
|
||||
family = "kimi-k2"
|
||||
|
||||
[providers.fireworks.models."kimi-k2.6".limits]
|
||||
context_window = 262144
|
||||
max_output = 16384
|
||||
|
||||
[providers.fireworks.models."kimi-k2.6".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
prompt_cache = true
|
||||
|
||||
[providers.fireworks.models."kimi-k2.6".costs]
|
||||
input_cost_per_mtok = 0.95
|
||||
output_cost_per_mtok = 4.0
|
||||
cache_input_cost_per_mtok = 0.16
|
||||
|
||||
[providers.fireworks.models."deepseek-v4-pro"]
|
||||
api_id = "accounts/fireworks/models/deepseek-v4-pro"
|
||||
display_name = "DeepSeek V4 Pro"
|
||||
family = "deepseek-v4"
|
||||
agent_profile = "openai"
|
||||
|
||||
[providers.fireworks.models."deepseek-v4-pro".limits]
|
||||
context_window = 1048576
|
||||
max_output = 16384
|
||||
|
||||
[providers.fireworks.models."deepseek-v4-pro".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.fireworks.models."deepseek-v4-pro".controls]
|
||||
# Fireworks promotes low/medium to high and xhigh to max for DeepSeek V4.
|
||||
reasoning_effort = ["high", "max"]
|
||||
|
||||
[providers.fireworks.models."deepseek-v4-pro".costs]
|
||||
input_cost_per_mtok = 1.74
|
||||
output_cost_per_mtok = 3.48
|
||||
cache_input_cost_per_mtok = 0.145
|
||||
|
||||
[providers.fireworks.models."deepseek-v4-flash"]
|
||||
api_id = "accounts/fireworks/models/deepseek-v4-flash"
|
||||
display_name = "DeepSeek V4 Flash"
|
||||
family = "deepseek-v4"
|
||||
aliases = ["deepseek-v4", "deepseek", "deepseek-flash"]
|
||||
agent_profile = "openai"
|
||||
|
||||
[providers.fireworks.models."deepseek-v4-flash".limits]
|
||||
context_window = 1048576
|
||||
max_output = 384000
|
||||
|
||||
[providers.fireworks.models."deepseek-v4-flash".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.fireworks.models."deepseek-v4-flash".controls]
|
||||
reasoning_effort = ["high", "max"]
|
||||
|
||||
[providers.fireworks.models."deepseek-v4-flash".costs]
|
||||
input_cost_per_mtok = 0.14
|
||||
output_cost_per_mtok = 0.28
|
||||
cache_input_cost_per_mtok = 0.028
|
||||
|
||||
[providers.fireworks.models."glm-5.2"]
|
||||
api_id = "accounts/fireworks/models/glm-5p2"
|
||||
display_name = "GLM 5.2 (via Fireworks)"
|
||||
family = "glm-5"
|
||||
|
||||
[providers.fireworks.models."glm-5.2".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[providers.fireworks.models."glm-5.2".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
|
||||
[providers.fireworks.models."glm-5.2".costs]
|
||||
input_cost_per_mtok = 1.4
|
||||
output_cost_per_mtok = 4.4
|
||||
cache_input_cost_per_mtok = 0.14
|
||||
|
||||
[providers.fireworks.models."minimax-m2.7"]
|
||||
api_id = "accounts/fireworks/models/minimax-m2p7"
|
||||
display_name = "MiniMax M2.7"
|
||||
family = "minimax-m2"
|
||||
|
||||
[providers.fireworks.models."minimax-m2.7".limits]
|
||||
context_window = 196608
|
||||
max_output = 16384
|
||||
|
||||
[providers.fireworks.models."minimax-m2.7".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
prompt_cache = true
|
||||
|
||||
[providers.fireworks.models."minimax-m2.7".costs]
|
||||
input_cost_per_mtok = 0.3
|
||||
output_cost_per_mtok = 1.2
|
||||
cache_input_cost_per_mtok = 0.059
|
||||
|
||||
[providers.fireworks.models."qwen3.7-plus"]
|
||||
api_id = "accounts/fireworks/models/qwen3p7-plus"
|
||||
display_name = "Qwen3.7 Plus"
|
||||
family = "qwen3"
|
||||
|
||||
[providers.fireworks.models."qwen3.7-plus".limits]
|
||||
context_window = 262144
|
||||
max_output = 16384
|
||||
|
||||
[providers.fireworks.models."qwen3.7-plus".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
prompt_cache = true
|
||||
|
||||
[providers.fireworks.models."qwen3.7-plus".costs]
|
||||
input_cost_per_mtok = 0.4
|
||||
output_cost_per_mtok = 1.6
|
||||
cache_input_cost_per_mtok = 0.08
|
||||
|
||||
[providers.fireworks.models."gpt-oss-120b"]
|
||||
api_id = "accounts/fireworks/models/gpt-oss-120b"
|
||||
display_name = "GPT-OSS 120B"
|
||||
family = "gpt-oss"
|
||||
|
||||
[providers.fireworks.models."gpt-oss-120b".limits]
|
||||
context_window = 131072
|
||||
max_output = 32768
|
||||
|
||||
[providers.fireworks.models."gpt-oss-120b".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
|
||||
[providers.fireworks.models."gpt-oss-120b".costs]
|
||||
input_cost_per_mtok = 0.15
|
||||
output_cost_per_mtok = 0.6
|
||||
cache_input_cost_per_mtok = 0.015
|
||||
|
||||
[providers.fireworks.models."gpt-oss-20b"]
|
||||
api_id = "accounts/fireworks/models/gpt-oss-20b"
|
||||
display_name = "GPT-OSS 20B"
|
||||
family = "gpt-oss"
|
||||
small_default = true
|
||||
probe = true
|
||||
|
||||
[providers.fireworks.models."gpt-oss-20b".limits]
|
||||
context_window = 131072
|
||||
max_output = 32768
|
||||
|
||||
[providers.fireworks.models."gpt-oss-20b".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
|
||||
[providers.fireworks.models."gpt-oss-20b".costs]
|
||||
input_cost_per_mtok = 0.07
|
||||
output_cost_per_mtok = 0.3
|
||||
cache_input_cost_per_mtok = 0.035
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
[providers.gemini]
|
||||
display_name = "Gemini"
|
||||
adapter = "gemini"
|
||||
api_key_url = "https://aistudio.google.com/apikey"
|
||||
base_url = "https://generativelanguage.googleapis.com/v1beta"
|
||||
priority = 80
|
||||
|
||||
[providers.gemini.auth]
|
||||
credentials = ["env:GEMINI_API_KEY", "env:GOOGLE_API_KEY", "vault:GEMINI_API_KEY"]
|
||||
header = { custom = "x-goog-api-key" }
|
||||
|
||||
[providers.gemini.models."gemini-3.1-pro-preview"]
|
||||
display_name = "Gemini 3.1 Pro (Preview)"
|
||||
family = "gemini-3"
|
||||
training = "2025-01-01"
|
||||
knowledge_cutoff = "January 2025"
|
||||
estimated_output_tps = 85
|
||||
aliases = ["gemini-pro"]
|
||||
|
||||
[providers.gemini.models."gemini-3.1-pro-preview".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[providers.gemini.models."gemini-3.1-pro-preview".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[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
|
||||
|
||||
[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"
|
||||
knowledge_cutoff = "January 2025"
|
||||
estimated_output_tps = 85
|
||||
aliases = ["gemini-customtools"]
|
||||
|
||||
[providers.gemini.models."gemini-3.1-pro-preview-customtools".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[providers.gemini.models."gemini-3.1-pro-preview-customtools".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[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
|
||||
|
||||
[providers.gemini.models."gemini-3.5-flash"]
|
||||
display_name = "Gemini 3.5 Flash"
|
||||
family = "gemini-3"
|
||||
training = "2025-01-01"
|
||||
knowledge_cutoff = "January 2025"
|
||||
default = true
|
||||
estimated_output_tps = 150
|
||||
aliases = ["gemini-35-flash"]
|
||||
|
||||
[providers.gemini.models."gemini-3.5-flash".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[providers.gemini.models."gemini-3.5-flash".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[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
|
||||
|
||||
[providers.gemini.models."gemini-3-flash-preview"]
|
||||
display_name = "Gemini 3 Flash (Preview)"
|
||||
family = "gemini-3"
|
||||
training = "2025-01-01"
|
||||
knowledge_cutoff = "January 2025"
|
||||
estimated_output_tps = 150
|
||||
aliases = ["gemini-flash"]
|
||||
|
||||
[providers.gemini.models."gemini-3-flash-preview".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[providers.gemini.models."gemini-3-flash-preview".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[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
|
||||
|
||||
[providers.gemini.models."gemini-3.1-flash-lite"]
|
||||
display_name = "Gemini 3.1 Flash Lite"
|
||||
family = "gemini-3"
|
||||
training = "2025-01-01"
|
||||
knowledge_cutoff = "January 2025"
|
||||
estimated_output_tps = 200
|
||||
aliases = ["gemini-flash-lite", "gemini-3.1-flash-lite-preview"]
|
||||
small_default = true
|
||||
|
||||
[providers.gemini.models."gemini-3.1-flash-lite".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[providers.gemini.models."gemini-3.1-flash-lite".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[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
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
[providers.inception]
|
||||
display_name = "Inception"
|
||||
adapter = "openai_compatible"
|
||||
api_key_url = "https://console.inceptionlabs.ai/api-keys"
|
||||
base_url = "https://api.inceptionlabs.ai/v1"
|
||||
priority = 40
|
||||
|
||||
[providers.inception.auth]
|
||||
credentials = ["env:INCEPTION_API_KEY", "vault:INCEPTION_API_KEY"]
|
||||
|
||||
[providers.inception.models."mercury-2"]
|
||||
display_name = "Mercury 2"
|
||||
family = "mercury"
|
||||
default = true
|
||||
estimated_output_tps = 1000
|
||||
aliases = ["mercury"]
|
||||
|
||||
[providers.inception.models."mercury-2".limits]
|
||||
context_window = 131072
|
||||
max_output = 50000
|
||||
|
||||
[providers.inception.models."mercury-2".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[providers.inception.models."mercury-2".costs]
|
||||
input_cost_per_mtok = 0.25
|
||||
output_cost_per_mtok = 0.75
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
[providers.litellm]
|
||||
display_name = "LiteLLM"
|
||||
adapter = "openai_compatible"
|
||||
base_url = "http://localhost:4000/v1"
|
||||
priority = 50
|
||||
enabled = false
|
||||
|
||||
[providers.litellm.auth]
|
||||
credentials = ["env:LITELLM_API_KEY", "vault:LITELLM_API_KEY"]
|
||||
|
||||
# To enable LiteLLM, add entries like these to settings.toml:
|
||||
#
|
||||
# [llm.providers.litellm]
|
||||
# enabled = true
|
||||
# base_url = "http://localhost:4000/v1"
|
||||
#
|
||||
# [llm.providers.litellm.models."litellm-gpt-5"]
|
||||
# api_id = "gpt-5"
|
||||
# display_name = "LiteLLM GPT-5"
|
||||
# family = "litellm"
|
||||
# default = true
|
||||
#
|
||||
# [llm.providers.litellm.models."litellm-gpt-5".limits]
|
||||
# context_window = 128000
|
||||
# max_output = 8192
|
||||
#
|
||||
# [llm.providers.litellm.models."litellm-gpt-5".features]
|
||||
# tools = true
|
||||
# vision = false
|
||||
# reasoning = false
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
[providers.minimax]
|
||||
display_name = "MiniMax"
|
||||
adapter = "openai_compatible"
|
||||
api_key_url = "https://platform.minimaxi.com/user-center/basic-information/interface-key"
|
||||
base_url = "https://api.minimax.io/v1"
|
||||
priority = 50
|
||||
|
||||
[providers.minimax.auth]
|
||||
credentials = ["env:MINIMAX_API_KEY", "vault:MINIMAX_API_KEY"]
|
||||
|
||||
[providers.minimax.models."minimax-m2.5"]
|
||||
display_name = "Minimax M2.5"
|
||||
family = "minimax-m2"
|
||||
default = true
|
||||
estimated_output_tps = 45
|
||||
aliases = ["minimax"]
|
||||
|
||||
[providers.minimax.models."minimax-m2.5".limits]
|
||||
context_window = 196608
|
||||
max_output = 16384
|
||||
|
||||
[providers.minimax.models."minimax-m2.5".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[providers.minimax.models."minimax-m2.5".costs]
|
||||
input_cost_per_mtok = 0.3
|
||||
output_cost_per_mtok = 1.2
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
[providers.modal]
|
||||
display_name = "Modal"
|
||||
adapter = "openai_compatible"
|
||||
agent_profile = "kimi"
|
||||
api_key_url = "https://modal.com/docs/guide/endpoints#proxy-tokens"
|
||||
priority = 75
|
||||
enabled = false
|
||||
|
||||
[providers.modal.extra_headers]
|
||||
"Modal-Key" = "{{ secrets.MODAL_TOKEN_ID }}"
|
||||
"Modal-Secret" = "{{ secrets.MODAL_TOKEN_SECRET }}"
|
||||
|
||||
# Modal assigns an endpoint URL when the Shared API or an Auto Endpoint is
|
||||
# created. To enable Modal, add the endpoint URL to ~/.fabro/settings.toml:
|
||||
#
|
||||
# [llm.providers.modal]
|
||||
# enabled = true
|
||||
# base_url = "https://<your-modal-endpoint>.modal.run/v1"
|
||||
#
|
||||
# Then store both proxy-token values in the Fabro server vault:
|
||||
#
|
||||
# fabro secret set MODAL_TOKEN_ID wk-...
|
||||
# fabro secret set MODAL_TOKEN_SECRET ws-...
|
||||
|
||||
# Modal serves the Hugging Face repository id, so `api_id` keeps that
|
||||
# capitalization. OpenRouter routes the same model under its own lowercase
|
||||
# slug (`moonshotai/kimi-k3`).
|
||||
[providers.modal.models."kimi-k3"]
|
||||
api_id = "moonshotai/Kimi-K3"
|
||||
display_name = "Kimi K3 (via Modal)"
|
||||
family = "kimi-k3"
|
||||
default = true
|
||||
estimated_output_tps = 460
|
||||
|
||||
[providers.modal.models."kimi-k3".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[providers.modal.models."kimi-k3".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "always_adaptive"
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.modal.models."kimi-k3".controls]
|
||||
reasoning_effort = ["low", "high", "max"]
|
||||
|
||||
[providers.modal.models."kimi-k3".costs]
|
||||
input_cost_per_mtok = 3.0
|
||||
output_cost_per_mtok = 15.0
|
||||
cache_input_cost_per_mtok = 0.3
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
[providers.moonshot]
|
||||
display_name = "Moonshot AI"
|
||||
adapter = "openai_compatible"
|
||||
agent_profile = "kimi"
|
||||
api_key_url = "https://platform.kimi.ai/console/api-keys"
|
||||
base_url = "https://api.moonshot.ai/v1"
|
||||
priority = 70
|
||||
|
||||
[providers.moonshot.auth]
|
||||
credentials = [
|
||||
"env:MOONSHOT_API_KEY",
|
||||
"env:KIMI_API_KEY",
|
||||
"vault:MOONSHOT_API_KEY",
|
||||
"vault:KIMI_API_KEY",
|
||||
]
|
||||
|
||||
[providers.moonshot.models."kimi-k2.5"]
|
||||
display_name = "Kimi K2.5"
|
||||
family = "kimi-k2"
|
||||
training = "2025-10-01"
|
||||
knowledge_cutoff = "October 2025"
|
||||
estimated_output_tps = 50
|
||||
|
||||
[providers.moonshot.models."kimi-k2.5".limits]
|
||||
context_window = 262144
|
||||
max_output = 32768
|
||||
|
||||
[providers.moonshot.models."kimi-k2.5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.moonshot.models."kimi-k2.5".costs]
|
||||
input_cost_per_mtok = 0.6
|
||||
output_cost_per_mtok = 3.0
|
||||
cache_input_cost_per_mtok = 0.1
|
||||
|
||||
[providers.moonshot.models."kimi-k3"]
|
||||
display_name = "Kimi K3"
|
||||
family = "kimi-k3"
|
||||
default = true
|
||||
aliases = ["kimi"]
|
||||
|
||||
[providers.moonshot.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
|
||||
|
||||
[providers.moonshot.models."kimi-k3".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "always_adaptive"
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.moonshot.models."kimi-k3".controls]
|
||||
reasoning_effort = ["low", "high", "max"]
|
||||
|
||||
[providers.moonshot.models."kimi-k3".costs]
|
||||
input_cost_per_mtok = 3.0
|
||||
output_cost_per_mtok = 15.0
|
||||
cache_input_cost_per_mtok = 0.3
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
[providers.ollama]
|
||||
display_name = "Ollama"
|
||||
adapter = "openai_compatible"
|
||||
billing_policy = "none"
|
||||
base_url = "http://localhost:11434/v1"
|
||||
priority = 30
|
||||
enabled = false
|
||||
|
||||
# Example model. Uncomment after `ollama pull qwen3.5` (and `enabled = true`
|
||||
# above) to expose it through the OpenAI-compatible adapter.
|
||||
#
|
||||
# [providers.ollama.models."qwen3.5"]
|
||||
# api_id = "qwen3.5:latest"
|
||||
# display_name = "Qwen3.5"
|
||||
# family = "qwen3.5"
|
||||
# default = true
|
||||
# aliases = ["ollama-qwen3.5"]
|
||||
#
|
||||
# [providers.ollama.models."qwen3.5".limits]
|
||||
# context_window = 32768
|
||||
#
|
||||
# [providers.ollama.models."qwen3.5".features]
|
||||
# tools = true
|
||||
# vision = false
|
||||
# reasoning = false
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
[providers.openai]
|
||||
display_name = "OpenAI"
|
||||
adapter = "openai"
|
||||
api_key_url = "https://platform.openai.com/api-keys"
|
||||
base_url = "https://api.openai.com/v1"
|
||||
priority = 90
|
||||
|
||||
[providers.openai.auth]
|
||||
credentials = ["env:OPENAI_API_KEY", "vault:OPENAI_API_KEY", "vault:OPENAI_CODEX"]
|
||||
|
||||
[providers.openai.models."gpt-5.6-sol"]
|
||||
display_name = "GPT-5.6 Sol"
|
||||
family = "gpt-5"
|
||||
training = "2026-02-16"
|
||||
knowledge_cutoff = "February 16, 2026"
|
||||
default = true
|
||||
aliases = ["sol", "gpt-sol", "gpt56-sol", "gpt-56-sol", "gpt-5.6", "gpt56", "gpt-56"]
|
||||
# Codex drives the 5.6 models with a much narrower tool set than the other
|
||||
# OpenAI models. Set per model so the rest of this provider keeps "openai".
|
||||
agent_profile = "gpt56"
|
||||
|
||||
[providers.openai.models."gpt-5.6-sol".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openai.models."gpt-5.6-sol".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[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
|
||||
|
||||
[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 = ["terra", "gpt-terra", "gpt56-terra", "gpt-56-terra"]
|
||||
agent_profile = "gpt56"
|
||||
|
||||
[providers.openai.models."gpt-5.6-terra".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openai.models."gpt-5.6-terra".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[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
|
||||
|
||||
[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 = ["luna", "gpt-luna", "gpt56-luna", "gpt-56-luna"]
|
||||
agent_profile = "gpt56"
|
||||
|
||||
[providers.openai.models."gpt-5.6-luna".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openai.models."gpt-5.6-luna".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[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
|
||||
|
||||
[providers.openai.models."gpt-5.4"]
|
||||
display_name = "GPT-5.4"
|
||||
family = "gpt-5"
|
||||
training = "2025-08-31"
|
||||
knowledge_cutoff = "April 2025"
|
||||
estimated_output_tps = 70
|
||||
aliases = ["gpt54", "gpt-54", "gpt-5.2", "gpt5", "gpt-5.3-codex", "codex"]
|
||||
|
||||
[providers.openai.models."gpt-5.4".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openai.models."gpt-5.4".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[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
|
||||
|
||||
[providers.openai.models."gpt-5.5"]
|
||||
display_name = "GPT-5.5"
|
||||
family = "gpt-5"
|
||||
training = "2025-12-01"
|
||||
knowledge_cutoff = "December 2025"
|
||||
estimated_output_tps = 70
|
||||
aliases = ["gpt55", "gpt-55"]
|
||||
|
||||
[providers.openai.models."gpt-5.5".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openai.models."gpt-5.5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[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
|
||||
|
||||
[providers.openai.models."gpt-5.5-pro"]
|
||||
display_name = "GPT-5.5 Pro"
|
||||
family = "gpt-5"
|
||||
training = "2025-12-01"
|
||||
knowledge_cutoff = "December 2025"
|
||||
estimated_output_tps = 20
|
||||
aliases = ["gpt55-pro", "gpt-55-pro"]
|
||||
|
||||
[providers.openai.models."gpt-5.5-pro".limits]
|
||||
context_window = 1050000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openai.models."gpt-5.5-pro".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[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
|
||||
|
||||
[providers.openai.models."gpt-5.4-pro"]
|
||||
display_name = "GPT-5.4 Pro"
|
||||
family = "gpt-5"
|
||||
training = "2025-08-31"
|
||||
knowledge_cutoff = "April 2025"
|
||||
estimated_output_tps = 20
|
||||
aliases = ["gpt54-pro", "gpt-54-pro"]
|
||||
|
||||
[providers.openai.models."gpt-5.4-pro".limits]
|
||||
context_window = 1047576
|
||||
max_output = 128000
|
||||
|
||||
[providers.openai.models."gpt-5.4-pro".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[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
|
||||
|
||||
[providers.openai.models."gpt-5.4-mini"]
|
||||
display_name = "GPT-5.4 Mini"
|
||||
family = "gpt-5"
|
||||
training = "2025-08-31"
|
||||
knowledge_cutoff = "April 2025"
|
||||
estimated_output_tps = 140
|
||||
aliases = ["gpt54-mini", "gpt-54-mini", "gpt-5.3-codex-spark", "codex-spark"]
|
||||
probe = true
|
||||
small_default = true
|
||||
|
||||
[providers.openai.models."gpt-5.4-mini".limits]
|
||||
context_window = 272000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openai.models."gpt-5.4-mini".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
|
||||
[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
|
||||
|
|
@ -1,699 +0,0 @@
|
|||
[providers.openrouter]
|
||||
display_name = "OpenRouter"
|
||||
adapter = "openai_compatible"
|
||||
api_key_url = "https://openrouter.ai/keys"
|
||||
base_url = "https://openrouter.ai/api/v1"
|
||||
priority = 25
|
||||
enabled = false
|
||||
|
||||
[providers.openrouter.auth]
|
||||
credentials = ["env:OPENROUTER_API_KEY", "vault:OPENROUTER_API_KEY"]
|
||||
|
||||
# Attribution headers (HTTP-Referer, X-Title) are NOT sent by default.
|
||||
# Self-hosted Fabro installations stay anonymous on OpenRouter's public
|
||||
# leaderboard unless the operator opts in. To advertise, add to
|
||||
# settings.toml:
|
||||
#
|
||||
# [llm.providers.openrouter.extra_headers]
|
||||
# "HTTP-Referer" = "https://your-site.example"
|
||||
# "X-Title" = "Your App"
|
||||
#
|
||||
# To enable OpenRouter, add the following to ~/.fabro/settings.toml:
|
||||
#
|
||||
# [llm.providers.openrouter]
|
||||
# enabled = true
|
||||
#
|
||||
# Then run `fabro provider login openrouter` to store the API key,
|
||||
# or set the OPENROUTER_API_KEY environment variable.
|
||||
|
||||
# ---------- Anthropic via OpenRouter ----------
|
||||
#
|
||||
# Claude models bill Anthropic-style (cache read/write pricing), so these
|
||||
# rows override the provider's OpenAI-default billing_policy. Costs are
|
||||
# best-effort estimates; OpenRouter returns the authoritative usage.cost
|
||||
# in-band on every response.
|
||||
|
||||
[providers.openrouter.models."claude-fable-5"]
|
||||
api_id = "anthropic/claude-fable-5"
|
||||
display_name = "Claude Fable 5 (via OpenRouter)"
|
||||
family = "claude-5"
|
||||
billing_policy = "anthropic"
|
||||
aliases = ["fable", "claude-fable"]
|
||||
agent_profile = "claude-5"
|
||||
|
||||
[providers.openrouter.models."claude-fable-5".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openrouter.models."claude-fable-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "always_adaptive"
|
||||
prompt_cache = true
|
||||
cache_control_breakpoints = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.openrouter.models."claude-fable-5".costs]
|
||||
input_cost_per_mtok = 10.0
|
||||
output_cost_per_mtok = 50.0
|
||||
cache_input_cost_per_mtok = 1.0
|
||||
|
||||
[providers.openrouter.models."claude-opus-5"]
|
||||
api_id = "anthropic/claude-opus-5"
|
||||
display_name = "Claude Opus 5 (via OpenRouter)"
|
||||
family = "claude-5"
|
||||
billing_policy = "anthropic"
|
||||
training = "2026-05-01"
|
||||
knowledge_cutoff = "May 2026"
|
||||
aliases = ["opus", "claude-opus"]
|
||||
agent_profile = "claude-5"
|
||||
|
||||
[providers.openrouter.models."claude-opus-5".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openrouter.models."claude-opus-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
cache_control_breakpoints = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.openrouter.models."claude-opus-5".costs]
|
||||
input_cost_per_mtok = 5.0
|
||||
output_cost_per_mtok = 25.0
|
||||
cache_input_cost_per_mtok = 0.5
|
||||
|
||||
[providers.openrouter.models."claude-sonnet-5"]
|
||||
api_id = "anthropic/claude-sonnet-5"
|
||||
display_name = "Claude Sonnet 5 (via OpenRouter)"
|
||||
family = "claude-5"
|
||||
billing_policy = "anthropic"
|
||||
training = "2026-01-01"
|
||||
knowledge_cutoff = "Jan 2026"
|
||||
default = true
|
||||
aliases = ["sonnet", "claude-sonnet"]
|
||||
agent_profile = "claude-5"
|
||||
|
||||
[providers.openrouter.models."claude-sonnet-5".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openrouter.models."claude-sonnet-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
cache_control_breakpoints = true
|
||||
sampling_params = false
|
||||
|
||||
# Current introductory rate. OpenRouter's authoritative in-band usage.cost
|
||||
# supersedes this estimate on completed responses.
|
||||
[providers.openrouter.models."claude-sonnet-5".costs]
|
||||
input_cost_per_mtok = 2.0
|
||||
output_cost_per_mtok = 10.0
|
||||
cache_input_cost_per_mtok = 0.2
|
||||
|
||||
[providers.openrouter.models."claude-opus-4-8"]
|
||||
api_id = "anthropic/claude-opus-4.8"
|
||||
display_name = "Claude Opus 4.8 (via OpenRouter)"
|
||||
family = "claude-4"
|
||||
billing_policy = "anthropic"
|
||||
training = "2026-01-01"
|
||||
knowledge_cutoff = "Jan 2026"
|
||||
|
||||
[providers.openrouter.models."claude-opus-4-8".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openrouter.models."claude-opus-4-8".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
cache_control_breakpoints = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.openrouter.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
|
||||
|
||||
[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"
|
||||
|
||||
[providers.openrouter.models."claude-opus-4-7".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openrouter.models."claude-opus-4-7".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
cache_control_breakpoints = true
|
||||
|
||||
[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
|
||||
|
||||
[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"
|
||||
|
||||
[providers.openrouter.models."claude-sonnet-4-6".limits]
|
||||
context_window = 1000000
|
||||
max_output = 64000
|
||||
|
||||
[providers.openrouter.models."claude-sonnet-4-6".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
cache_control_breakpoints = true
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
aliases = ["haiku", "claude-haiku"]
|
||||
|
||||
[providers.openrouter.models."claude-haiku-4-5".limits]
|
||||
context_window = 200000
|
||||
max_output = 8192
|
||||
|
||||
[providers.openrouter.models."claude-haiku-4-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
prompt_cache = true
|
||||
cache_control_breakpoints = true
|
||||
|
||||
[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 ----------
|
||||
|
||||
# GPT-5.6 entries use OpenRouter's base rates below the 272k-token
|
||||
# long-context threshold. Authoritative in-band usage.cost covers
|
||||
# long-context and cache-write pricing.
|
||||
|
||||
[providers.openrouter.models."gpt-5.6-sol"]
|
||||
api_id = "openai/gpt-5.6-sol"
|
||||
display_name = "GPT-5.6 Sol (via OpenRouter)"
|
||||
family = "gpt-5"
|
||||
training = "2026-02-16"
|
||||
knowledge_cutoff = "February 16, 2026"
|
||||
aliases = ["sol", "gpt-sol", "gpt56-sol", "gpt-56-sol", "gpt-5.6", "gpt56", "gpt-56"]
|
||||
# Same narrow Codex tool set as on the openai provider; the gateway does not
|
||||
# change what the model was trained against.
|
||||
agent_profile = "gpt56"
|
||||
|
||||
[providers.openrouter.models."gpt-5.6-sol".limits]
|
||||
context_window = 1050000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openrouter.models."gpt-5.6-sol".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.openrouter.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
|
||||
|
||||
[providers.openrouter.models."gpt-5.6-terra"]
|
||||
api_id = "openai/gpt-5.6-terra"
|
||||
display_name = "GPT-5.6 Terra (via OpenRouter)"
|
||||
family = "gpt-5"
|
||||
training = "2026-02-16"
|
||||
knowledge_cutoff = "February 16, 2026"
|
||||
aliases = ["terra", "gpt-terra", "gpt56-terra", "gpt-56-terra"]
|
||||
agent_profile = "gpt56"
|
||||
|
||||
[providers.openrouter.models."gpt-5.6-terra".limits]
|
||||
context_window = 1050000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openrouter.models."gpt-5.6-terra".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.openrouter.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
|
||||
|
||||
[providers.openrouter.models."gpt-5.6-luna"]
|
||||
api_id = "openai/gpt-5.6-luna"
|
||||
display_name = "GPT-5.6 Luna (via OpenRouter)"
|
||||
family = "gpt-5"
|
||||
training = "2026-02-16"
|
||||
knowledge_cutoff = "February 16, 2026"
|
||||
aliases = ["luna", "gpt-luna", "gpt56-luna", "gpt-56-luna"]
|
||||
agent_profile = "gpt56"
|
||||
|
||||
[providers.openrouter.models."gpt-5.6-luna".limits]
|
||||
context_window = 1050000
|
||||
max_output = 128000
|
||||
|
||||
[providers.openrouter.models."gpt-5.6-luna".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.openrouter.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
|
||||
|
||||
[providers.openrouter.models."gpt-5.4"]
|
||||
api_id = "openai/gpt-5.4"
|
||||
display_name = "GPT-5.4 (via OpenRouter)"
|
||||
family = "gpt-5"
|
||||
aliases = ["gpt54", "gpt-54", "gpt-5.2", "gpt5", "gpt-5.3-codex", "codex"]
|
||||
|
||||
[providers.openrouter.models."gpt-5.4".limits]
|
||||
context_window = 1050000
|
||||
max_output = 32768
|
||||
|
||||
[providers.openrouter.models."gpt-5.4".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
|
||||
[providers.openrouter.models."gpt-5.4".costs]
|
||||
input_cost_per_mtok = 2.5
|
||||
output_cost_per_mtok = 15.0
|
||||
|
||||
[providers.openrouter.models."gpt-5.5"]
|
||||
api_id = "openai/gpt-5.5"
|
||||
display_name = "GPT-5.5 (via OpenRouter)"
|
||||
family = "gpt-5"
|
||||
aliases = ["gpt55", "gpt-55"]
|
||||
|
||||
[providers.openrouter.models."gpt-5.5".limits]
|
||||
context_window = 1050000
|
||||
max_output = 32768
|
||||
|
||||
[providers.openrouter.models."gpt-5.5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
|
||||
[providers.openrouter.models."gpt-5.5".costs]
|
||||
input_cost_per_mtok = 5.0
|
||||
output_cost_per_mtok = 30.0
|
||||
|
||||
# ---------- Google Gemini via 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"
|
||||
|
||||
[providers.openrouter.models."gemini-3.1-pro-preview".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[providers.openrouter.models."gemini-3.1-pro-preview".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
|
||||
[providers.openrouter.models."gemini-3.1-pro-preview".costs]
|
||||
input_cost_per_mtok = 2.0
|
||||
output_cost_per_mtok = 12.0
|
||||
|
||||
[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"
|
||||
|
||||
[providers.openrouter.models."gemini-3.5-flash".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[providers.openrouter.models."gemini-3.5-flash".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
|
||||
[providers.openrouter.models."gemini-3.5-flash".costs]
|
||||
input_cost_per_mtok = 1.5
|
||||
output_cost_per_mtok = 9.0
|
||||
|
||||
# ---------- Open-weights models ----------
|
||||
|
||||
[providers.openrouter.models."mimo-v2.5-pro"]
|
||||
api_id = "xiaomi/mimo-v2.5-pro"
|
||||
display_name = "Xiaomi MiMo v2.5 Pro"
|
||||
family = "mimo-v2"
|
||||
|
||||
[providers.openrouter.models."mimo-v2.5-pro".limits]
|
||||
context_window = 1050000
|
||||
max_output = 16384
|
||||
|
||||
[providers.openrouter.models."mimo-v2.5-pro".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[providers.openrouter.models."mimo-v2.5-pro".costs]
|
||||
input_cost_per_mtok = 0.435
|
||||
output_cost_per_mtok = 0.87
|
||||
|
||||
[providers.openrouter.models."minimax-m2.7"]
|
||||
api_id = "minimax/minimax-m2.7"
|
||||
display_name = "MiniMax M2.7"
|
||||
family = "minimax-m2"
|
||||
|
||||
[providers.openrouter.models."minimax-m2.7".limits]
|
||||
context_window = 200000
|
||||
max_output = 16384
|
||||
|
||||
[providers.openrouter.models."minimax-m2.7".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[providers.openrouter.models."minimax-m2.7".costs]
|
||||
input_cost_per_mtok = 0.28
|
||||
output_cost_per_mtok = 1.20
|
||||
|
||||
[providers.openrouter.models."deepseek-v4-pro"]
|
||||
api_id = "deepseek/deepseek-v4-pro"
|
||||
display_name = "DeepSeek V4 Pro"
|
||||
family = "deepseek-v4"
|
||||
agent_profile = "openai"
|
||||
|
||||
[providers.openrouter.models."deepseek-v4-pro".limits]
|
||||
context_window = 1048576
|
||||
max_output = 384000
|
||||
|
||||
[providers.openrouter.models."deepseek-v4-pro".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.openrouter.models."deepseek-v4-pro".controls]
|
||||
# OpenRouter names DeepSeek's max tier xhigh on this route.
|
||||
reasoning_effort = ["high", "xhigh"]
|
||||
|
||||
[providers.openrouter.models."deepseek-v4-pro".costs]
|
||||
input_cost_per_mtok = 0.435
|
||||
output_cost_per_mtok = 0.87
|
||||
cache_input_cost_per_mtok = 0.003625
|
||||
|
||||
[providers.openrouter.models."deepseek-v4-flash"]
|
||||
api_id = "deepseek/deepseek-v4-flash-0731"
|
||||
display_name = "DeepSeek V4 Flash"
|
||||
family = "deepseek-v4"
|
||||
aliases = ["deepseek-v4", "deepseek", "deepseek-flash"]
|
||||
agent_profile = "openai"
|
||||
|
||||
[providers.openrouter.models."deepseek-v4-flash".limits]
|
||||
context_window = 1048576
|
||||
max_output = 384000
|
||||
|
||||
[providers.openrouter.models."deepseek-v4-flash".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.openrouter.models."deepseek-v4-flash".controls]
|
||||
reasoning_effort = ["low", "high", "max"]
|
||||
|
||||
[providers.openrouter.models."deepseek-v4-flash".costs]
|
||||
input_cost_per_mtok = 0.14
|
||||
output_cost_per_mtok = 0.28
|
||||
cache_input_cost_per_mtok = 0.0028
|
||||
|
||||
[providers.openrouter.models."kimi-k2.6"]
|
||||
api_id = "moonshotai/kimi-k2.6"
|
||||
display_name = "Kimi K2.6"
|
||||
family = "kimi-k2"
|
||||
# Kimi models get the Kimi agent profile wherever they are served from, so a
|
||||
# gateway route behaves like the direct Moonshot one.
|
||||
agent_profile = "kimi"
|
||||
|
||||
[providers.openrouter.models."kimi-k2.6".limits]
|
||||
context_window = 262144
|
||||
max_output = 16384
|
||||
|
||||
[providers.openrouter.models."kimi-k2.6".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[providers.openrouter.models."kimi-k2.6".costs]
|
||||
input_cost_per_mtok = 0.73
|
||||
output_cost_per_mtok = 3.49
|
||||
|
||||
[providers.openrouter.models."kimi-k3"]
|
||||
api_id = "moonshotai/kimi-k3"
|
||||
display_name = "Kimi K3 (via OpenRouter)"
|
||||
family = "kimi-k3"
|
||||
agent_profile = "kimi"
|
||||
|
||||
[providers.openrouter.models."kimi-k3".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[providers.openrouter.models."kimi-k3".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "always_adaptive"
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.openrouter.models."kimi-k3".controls]
|
||||
reasoning_effort = ["low", "high", "max"]
|
||||
|
||||
[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
|
||||
|
||||
[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"
|
||||
|
||||
[providers.openrouter.models."laguna-s-2.1".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[providers.openrouter.models."laguna-s-2.1".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
sampling_params = true
|
||||
|
||||
[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
|
||||
|
||||
[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"
|
||||
|
||||
[providers.openrouter.models."laguna-xs-2.1".limits]
|
||||
context_window = 262144
|
||||
max_output = 32768
|
||||
|
||||
[providers.openrouter.models."laguna-xs-2.1".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
sampling_params = true
|
||||
|
||||
# Current promotional rate. OpenRouter's authoritative in-band usage.cost
|
||||
# supersedes this estimate on completed responses.
|
||||
[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
|
||||
|
||||
[providers.openrouter.models."qwen3-coder"]
|
||||
api_id = "qwen/qwen3-coder"
|
||||
display_name = "Qwen3 Coder"
|
||||
family = "qwen3"
|
||||
|
||||
[providers.openrouter.models."qwen3-coder".limits]
|
||||
context_window = 1050000
|
||||
max_output = 16384
|
||||
|
||||
[providers.openrouter.models."qwen3-coder".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[providers.openrouter.models."qwen3-coder".costs]
|
||||
input_cost_per_mtok = 0.22
|
||||
output_cost_per_mtok = 1.80
|
||||
|
||||
[providers.openrouter.models."qwen3.6-flash"]
|
||||
api_id = "qwen/qwen3.6-flash"
|
||||
display_name = "Qwen3.6 Flash"
|
||||
family = "qwen3"
|
||||
|
||||
[providers.openrouter.models."qwen3.6-flash".limits]
|
||||
context_window = 1000000
|
||||
max_output = 16384
|
||||
|
||||
[providers.openrouter.models."qwen3.6-flash".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[providers.openrouter.models."qwen3.6-flash".costs]
|
||||
input_cost_per_mtok = 0.1875
|
||||
output_cost_per_mtok = 1.125
|
||||
|
||||
[providers.openrouter.models."qwen3.8-max"]
|
||||
api_id = "qwen/qwen3.8-max"
|
||||
display_name = "Qwen3.8 Max"
|
||||
family = "qwen3"
|
||||
|
||||
[providers.openrouter.models."qwen3.8-max".limits]
|
||||
context_window = 1000000
|
||||
max_output = 131072
|
||||
|
||||
[providers.openrouter.models."qwen3.8-max".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[providers.openrouter.models."qwen3.8-max".controls]
|
||||
reasoning_effort = ["low", "medium", "high", "xhigh"]
|
||||
|
||||
[providers.openrouter.models."qwen3.8-max".costs]
|
||||
input_cost_per_mtok = 2.0
|
||||
output_cost_per_mtok = 6.0
|
||||
cache_input_cost_per_mtok = 0.25
|
||||
|
||||
[providers.openrouter.models."glm-5.2"]
|
||||
api_id = "z-ai/glm-5.2"
|
||||
display_name = "GLM 5.2 (via OpenRouter)"
|
||||
family = "glm-5"
|
||||
aliases = ["glm", "glm5", "glm52", "glm5.2"]
|
||||
|
||||
[providers.openrouter.models."glm-5.2".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[providers.openrouter.models."glm-5.2".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[providers.openrouter.models."glm-5.2".controls]
|
||||
reasoning_effort = ["high", "xhigh"]
|
||||
|
||||
[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
|
||||
|
||||
[providers.openrouter.models."glm-4.6"]
|
||||
api_id = "z-ai/glm-4.6"
|
||||
display_name = "GLM 4.6"
|
||||
family = "glm-4"
|
||||
|
||||
[providers.openrouter.models."glm-4.6".limits]
|
||||
context_window = 203000
|
||||
max_output = 16384
|
||||
|
||||
[providers.openrouter.models."glm-4.6".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[providers.openrouter.models."glm-4.6".costs]
|
||||
input_cost_per_mtok = 0.43
|
||||
output_cost_per_mtok = 1.74
|
||||
|
||||
[providers.openrouter.models."nemotron-3-super-120b-a12b"]
|
||||
api_id = "nvidia/nemotron-3-super-120b-a12b"
|
||||
display_name = "NVIDIA Nemotron 3 Super 120B"
|
||||
family = "nemotron-3"
|
||||
|
||||
[providers.openrouter.models."nemotron-3-super-120b-a12b".limits]
|
||||
context_window = 1000000
|
||||
max_output = 16384
|
||||
|
||||
[providers.openrouter.models."nemotron-3-super-120b-a12b".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[providers.openrouter.models."nemotron-3-super-120b-a12b".costs]
|
||||
input_cost_per_mtok = 0.09
|
||||
output_cost_per_mtok = 0.45
|
||||
|
||||
[providers.openrouter.models."devstral-2512"]
|
||||
api_id = "mistralai/devstral-2512"
|
||||
display_name = "Devstral 2512"
|
||||
family = "devstral"
|
||||
|
||||
[providers.openrouter.models."devstral-2512".limits]
|
||||
context_window = 262144
|
||||
max_output = 16384
|
||||
|
||||
[providers.openrouter.models."devstral-2512".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[providers.openrouter.models."devstral-2512".costs]
|
||||
input_cost_per_mtok = 0.40
|
||||
output_cost_per_mtok = 2.00
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
[providers.poolside]
|
||||
display_name = "Poolside"
|
||||
adapter = "openai_compatible"
|
||||
api_key_url = "https://platform.poolside.ai"
|
||||
base_url = "https://inference.poolside.ai/v1"
|
||||
priority = 65
|
||||
|
||||
[providers.poolside.auth]
|
||||
credentials = ["env:POOLSIDE_API_KEY", "vault:POOLSIDE_API_KEY"]
|
||||
|
||||
[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"]
|
||||
|
||||
[providers.poolside.models."laguna-s-2.1".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[providers.poolside.models."laguna-s-2.1".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
sampling_params = true
|
||||
|
||||
# Poolside Platform is free for a limited preview period. Keep the published
|
||||
# paid hosted rate as Fabro's durable estimate for paid access and post-preview
|
||||
# usage.
|
||||
[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
|
||||
|
||||
[providers.poolside.models."laguna-xs-2.1"]
|
||||
api_id = "poolside/laguna-xs-2.1"
|
||||
display_name = "Laguna XS 2.1"
|
||||
family = "laguna-2"
|
||||
small_default = true
|
||||
probe = true
|
||||
aliases = ["laguna-xs"]
|
||||
|
||||
[providers.poolside.models."laguna-xs-2.1".limits]
|
||||
context_window = 262144
|
||||
max_output = 32768
|
||||
|
||||
[providers.poolside.models."laguna-xs-2.1".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
sampling_params = true
|
||||
|
||||
# Poolside Platform is free for a limited preview period. These are Poolside's
|
||||
# published paid endpoint rates.
|
||||
[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
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
# Model IDs, capabilities, contexts, and prices are from Venice's published
|
||||
# model catalog, verified 2026-08-21:
|
||||
# https://github.com/veniceai/api-docs/blob/59a300b1d036c0c0acc0e5f75c0ab0dd07c40c1c/data/static-models.json
|
||||
|
||||
[providers.venice]
|
||||
display_name = "Venice"
|
||||
adapter = "openai_compatible"
|
||||
base_url = "https://api.venice.ai/api/v1"
|
||||
priority = 35
|
||||
aliases = ["venice-ai"]
|
||||
billing_policy = "openai"
|
||||
|
||||
[providers.venice.auth]
|
||||
credentials = ["env:VENICE_API_KEY", "vault:VENICE_API_KEY"]
|
||||
|
||||
[providers.venice.models."kimi-k3"]
|
||||
display_name = "Kimi K3"
|
||||
family = "kimi-k3"
|
||||
agent_profile = "kimi"
|
||||
aliases = ["kimi"]
|
||||
|
||||
[providers.venice.models."kimi-k3".limits]
|
||||
context_window = 1000000
|
||||
max_output = 131072
|
||||
|
||||
[providers.venice.models."kimi-k3".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.venice.models."kimi-k3".costs]
|
||||
input_cost_per_mtok = 3.75
|
||||
output_cost_per_mtok = 18.75
|
||||
cache_input_cost_per_mtok = 0.375
|
||||
|
||||
[providers.venice.models."kimi-k3-fast"]
|
||||
api_id = "kimi-k3-fast-api"
|
||||
display_name = "Kimi K3 Fast"
|
||||
family = "kimi-k3"
|
||||
agent_profile = "kimi"
|
||||
aliases = ["kimi-fast"]
|
||||
|
||||
[providers.venice.models."kimi-k3-fast".limits]
|
||||
context_window = 1000000
|
||||
max_output = 131072
|
||||
|
||||
[providers.venice.models."kimi-k3-fast".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.venice.models."kimi-k3-fast".costs]
|
||||
input_cost_per_mtok = 4.5
|
||||
output_cost_per_mtok = 22.5
|
||||
cache_input_cost_per_mtok = 0.45
|
||||
|
||||
[providers.venice.models."grok-4.6"]
|
||||
api_id = "grok-4-6"
|
||||
display_name = "Grok 4.6"
|
||||
family = "grok-4"
|
||||
aliases = ["grok", "grok46", "grok-46"]
|
||||
|
||||
[providers.venice.models."grok-4.6".limits]
|
||||
context_window = 500000
|
||||
max_output = 32000
|
||||
|
||||
[providers.venice.models."grok-4.6".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
|
||||
[providers.venice.models."grok-4.6".controls]
|
||||
reasoning_effort = ["low", "medium", "high", "xhigh"]
|
||||
|
||||
[providers.venice.models."grok-4.6".costs]
|
||||
input_cost_per_mtok = 2.27
|
||||
output_cost_per_mtok = 6.8
|
||||
cache_input_cost_per_mtok = 0.57
|
||||
|
||||
[providers.venice.models."glm-5.3"]
|
||||
api_id = "z-ai-glm-5-3"
|
||||
display_name = "GLM 5.3"
|
||||
family = "glm-5"
|
||||
aliases = ["glm", "glm5", "glm53", "glm5.3", "glm-5-3"]
|
||||
|
||||
[providers.venice.models."glm-5.3".limits]
|
||||
context_window = 1000000
|
||||
max_output = 131072
|
||||
|
||||
[providers.venice.models."glm-5.3".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
|
||||
[providers.venice.models."glm-5.3".controls]
|
||||
reasoning_effort = ["low", "high", "max"]
|
||||
|
||||
[providers.venice.models."glm-5.3".costs]
|
||||
input_cost_per_mtok = 1.75
|
||||
output_cost_per_mtok = 5.5
|
||||
cache_input_cost_per_mtok = 0.325
|
||||
|
||||
[providers.venice.models."deepseek-v4-flash"]
|
||||
api_id = "deepseek-v4-flash-0731"
|
||||
display_name = "DeepSeek V4 Flash"
|
||||
family = "deepseek-v4"
|
||||
agent_profile = "openai"
|
||||
default = true
|
||||
aliases = ["deepseek-v4", "deepseek", "deepseek-flash"]
|
||||
|
||||
[providers.venice.models."deepseek-v4-flash".limits]
|
||||
context_window = 1000000
|
||||
max_output = 32768
|
||||
|
||||
[providers.venice.models."deepseek-v4-flash".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.venice.models."deepseek-v4-flash".controls]
|
||||
reasoning_effort = ["low", "high", "max"]
|
||||
|
||||
[providers.venice.models."deepseek-v4-flash".costs]
|
||||
input_cost_per_mtok = 0.175
|
||||
output_cost_per_mtok = 0.35
|
||||
cache_input_cost_per_mtok = 0.035
|
||||
|
||||
[providers.venice.models."deepseek-v4-pro"]
|
||||
api_id = "deepseek-v4-pro-0813"
|
||||
display_name = "DeepSeek V4 Pro"
|
||||
family = "deepseek-v4"
|
||||
agent_profile = "openai"
|
||||
aliases = ["deepseek-pro"]
|
||||
|
||||
[providers.venice.models."deepseek-v4-pro".limits]
|
||||
context_window = 1000000
|
||||
max_output = 32768
|
||||
|
||||
[providers.venice.models."deepseek-v4-pro".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
sampling_params = false
|
||||
|
||||
[providers.venice.models."deepseek-v4-pro".costs]
|
||||
input_cost_per_mtok = 1.65
|
||||
output_cost_per_mtok = 4.95
|
||||
cache_input_cost_per_mtok = 0.165
|
||||
|
||||
[providers.venice.models."qwen3.8-max"]
|
||||
api_id = "qwen-3-8-max"
|
||||
display_name = "Qwen 3.8 Max"
|
||||
family = "qwen3"
|
||||
aliases = ["qwen", "qwen-max", "qwen3.8", "qwen-3.8", "qwen38", "qwen-3.8-max", "qwen38-max"]
|
||||
|
||||
[providers.venice.models."qwen3.8-max".limits]
|
||||
context_window = 1000000
|
||||
max_output = 131072
|
||||
|
||||
[providers.venice.models."qwen3.8-max".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_by_default = true
|
||||
prompt_cache = true
|
||||
|
||||
[providers.venice.models."qwen3.8-max".costs]
|
||||
input_cost_per_mtok = 2.5
|
||||
output_cost_per_mtok = 7.5
|
||||
cache_input_cost_per_mtok = 0.3125
|
||||
|
||||
[providers.venice.models."qwen3.8-27b"]
|
||||
api_id = "qwen-3-8-27b"
|
||||
display_name = "Qwen 3.8 27B"
|
||||
family = "qwen3.8"
|
||||
aliases = ["qwen-27b", "qwen-3.8-27b", "qwen38-27b"]
|
||||
|
||||
[providers.venice.models."qwen3.8-27b".limits]
|
||||
context_window = 262144
|
||||
max_output = 131072
|
||||
|
||||
[providers.venice.models."qwen3.8-27b".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
reasoning_by_default = true
|
||||
|
||||
[providers.venice.models."qwen3.8-27b".controls]
|
||||
reasoning_effort = ["low", "medium", "xhigh"]
|
||||
|
||||
[providers.venice.models."qwen3.8-27b".costs]
|
||||
input_cost_per_mtok = 0.45
|
||||
output_cost_per_mtok = 3.2
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
[providers.zai]
|
||||
display_name = "Z.ai"
|
||||
adapter = "openai_compatible"
|
||||
api_key_url = "https://open.bigmodel.cn/usercenter/apikeys"
|
||||
base_url = "https://api.z.ai/api/coding/paas/v4"
|
||||
priority = 60
|
||||
|
||||
[providers.zai.auth]
|
||||
credentials = ["env:ZAI_API_KEY", "vault:ZAI_API_KEY"]
|
||||
|
||||
[providers.zai.models."glm-5.2"]
|
||||
display_name = "GLM 5.2"
|
||||
family = "glm-5"
|
||||
default = true
|
||||
aliases = ["glm", "glm5", "glm52", "glm5.2"]
|
||||
|
||||
[providers.zai.models."glm-5.2".limits]
|
||||
context_window = 1048576
|
||||
max_output = 131072
|
||||
|
||||
[providers.zai.models."glm-5.2".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
reasoning_effort = "levels"
|
||||
prompt_cache = true
|
||||
|
||||
[providers.zai.models."glm-5.2".controls]
|
||||
reasoning_effort = ["high", "max"]
|
||||
|
||||
[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
|
||||
|
||||
[providers.zai.models."glm-4.7"]
|
||||
display_name = "GLM 4.7"
|
||||
family = "glm-4"
|
||||
estimated_output_tps = 100
|
||||
aliases = ["glm4"]
|
||||
|
||||
[providers.zai.models."glm-4.7".limits]
|
||||
context_window = 202752
|
||||
max_output = 16384
|
||||
|
||||
[providers.zai.models."glm-4.7".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[providers.zai.models."glm-4.7".costs]
|
||||
input_cost_per_mtok = 0.6
|
||||
output_cost_per_mtok = 2.2
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
//! Wire-dialect identity shared by the model catalog and LLM route assembly.
|
||||
//!
|
||||
//! A codec names *what the bytes say* — the wire dialect a route speaks —
|
||||
//! independently of the transport/auth scheme named by
|
||||
//! [`AdapterKind`](crate::AdapterKind). Catalog rows may select a codec
|
||||
//! explicitly; rows that omit it inherit the adapter's default, which
|
||||
//! reproduces the historical adapter→dialect fusion exactly.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumString, IntoStaticStr, VariantArray};
|
||||
|
||||
use crate::adapter::AdapterKind;
|
||||
|
||||
/// Stable wire-dialect identity for a route.
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
IntoStaticStr,
|
||||
VariantArray,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum CodecKind {
|
||||
AnthropicMessages,
|
||||
#[serde(rename = "openai_responses")]
|
||||
#[strum(to_string = "openai_responses")]
|
||||
OpenAiResponses,
|
||||
/// The conservative Chat Completions dialect. The name matches today's
|
||||
/// `openai_compatible` adapter string; `openai_chat` stays reserved for a
|
||||
/// possible future full-proprietary Chat Completions dialect.
|
||||
#[serde(rename = "openai_compatible")]
|
||||
#[strum(to_string = "openai_compatible")]
|
||||
OpenAiCompatible,
|
||||
GeminiGenerate,
|
||||
/// Amazon Bedrock's unified Converse/ConverseStream dialect: one
|
||||
/// model-agnostic envelope AWS translates to each hosted family's
|
||||
/// native format server-side.
|
||||
BedrockConverse,
|
||||
}
|
||||
|
||||
impl CodecKind {
|
||||
/// The codec each adapter kind drives when a catalog row does not
|
||||
/// configure `codec` explicitly. These defaults reproduce the historical
|
||||
/// behavior where the adapter implied the wire dialect.
|
||||
#[must_use]
|
||||
pub fn default_for(adapter: AdapterKind) -> Self {
|
||||
match adapter {
|
||||
AdapterKind::Anthropic => Self::AnthropicMessages,
|
||||
AdapterKind::OpenAi => Self::OpenAiResponses,
|
||||
AdapterKind::Gemini => Self::GeminiGenerate,
|
||||
AdapterKind::OpenAiCompatible => Self::OpenAiCompatible,
|
||||
AdapterKind::Bedrock => Self::BedrockConverse,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for CodecKind {
|
||||
fn as_ref(&self) -> &str {
|
||||
(*self).as_str()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn codec_kind_round_trips_as_snake_case() {
|
||||
for kind in CodecKind::VARIANTS {
|
||||
let json = serde_json::to_string(kind).unwrap();
|
||||
assert_eq!(json, format!("\"{}\"", kind.as_str()));
|
||||
let parsed: CodecKind = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed, *kind);
|
||||
assert_eq!(kind.as_str().parse::<CodecKind>().unwrap(), *kind);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_kind_strings_match_route_vocabulary() {
|
||||
for (kind, expected) in [
|
||||
(CodecKind::AnthropicMessages, "anthropic_messages"),
|
||||
(CodecKind::OpenAiResponses, "openai_responses"),
|
||||
(CodecKind::OpenAiCompatible, "openai_compatible"),
|
||||
(CodecKind::GeminiGenerate, "gemini_generate"),
|
||||
(CodecKind::BedrockConverse, "bedrock_converse"),
|
||||
] {
|
||||
assert_eq!(kind.as_str(), expected);
|
||||
assert_eq!(kind.to_string(), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapter_defaults_reproduce_the_historical_fusion() {
|
||||
for (adapter, expected) in [
|
||||
(AdapterKind::Anthropic, CodecKind::AnthropicMessages),
|
||||
(AdapterKind::OpenAi, CodecKind::OpenAiResponses),
|
||||
(AdapterKind::Gemini, CodecKind::GeminiGenerate),
|
||||
(AdapterKind::OpenAiCompatible, CodecKind::OpenAiCompatible),
|
||||
(AdapterKind::Bedrock, CodecKind::BedrockConverse),
|
||||
] {
|
||||
assert_eq!(CodecKind::default_for(adapter), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
//! String-backed provider and model identifiers.
|
||||
//!
|
||||
//! Provider and model identity are catalog data, not closed enums. These
|
||||
//! newtypes give catalog/auth/server seams a single, type-safe wrapper while
|
||||
//! keeping wire format compatible with plain strings.
|
||||
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Stable provider identifier referenced from settings, vault, and request
|
||||
/// routing.
|
||||
///
|
||||
/// Wraps a `String` because the set of providers is open-ended and supplied
|
||||
/// by `[llm.providers]` settings rather than compiled into a Rust enum.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ProviderId(String);
|
||||
|
||||
impl ProviderId {
|
||||
pub const ANTHROPIC: &'static str = "anthropic";
|
||||
pub const OPENAI: &'static str = "openai";
|
||||
pub const GEMINI: &'static str = "gemini";
|
||||
|
||||
/// Construct a provider ID from any string-like value without validation.
|
||||
/// Catalog construction is responsible for canonicalisation; consumers
|
||||
/// only need a wrapper for type clarity.
|
||||
pub fn new(id: impl Into<String>) -> Self {
|
||||
Self(id.into())
|
||||
}
|
||||
|
||||
/// Borrow the inner string.
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Consume the wrapper and return the inner `String`.
|
||||
#[must_use]
|
||||
pub fn into_inner(self) -> String {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn anthropic() -> Self {
|
||||
Self::new(Self::ANTHROPIC)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn openai() -> Self {
|
||||
Self::new(Self::OPENAI)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn gemini() -> Self {
|
||||
Self::new(Self::GEMINI)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn display_name(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ProviderId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ProviderId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ProviderId {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ProviderId {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ProviderId {
|
||||
type Err = std::convert::Infallible;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self::from(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ProviderId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable canonical, human-facing model identifier.
|
||||
///
|
||||
/// Aliases are alternate selectors for a model offering; they are not model
|
||||
/// IDs. The same `ModelId` may be offered by more than one provider, so a
|
||||
/// concrete catalog offering is identified by `(ProviderId, ModelId)`.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ModelId(String);
|
||||
|
||||
impl ModelId {
|
||||
pub fn new(id: impl Into<String>) -> Self {
|
||||
Self(id.into())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn into_inner(self) -> String {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ModelId {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ModelId {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ModelId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<str> for ModelId {
|
||||
fn eq(&self, other: &str) -> bool {
|
||||
self.0 == other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&str> for ModelId {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
self.0 == *other
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn provider_id_is_transparent_string_in_json() {
|
||||
let id = ProviderId::new("moonshot");
|
||||
let json = serde_json::to_string(&id).unwrap();
|
||||
assert_eq!(json, "\"moonshot\"");
|
||||
let back: ProviderId = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_id_is_transparent_string_in_json() {
|
||||
let id = ModelId::new("kimi-k2.5");
|
||||
let json = serde_json::to_string(&id).unwrap();
|
||||
assert_eq!(json, "\"kimi-k2.5\"");
|
||||
let back: ModelId = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_writes_inner_string() {
|
||||
assert_eq!(ProviderId::new("anthropic").to_string(), "anthropic");
|
||||
assert_eq!(
|
||||
ModelId::new("claude-opus-4-7").to_string(),
|
||||
"claude-opus-4-7"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ord_is_lexicographic() {
|
||||
let mut v = [ProviderId::new("zai"), ProviderId::new("anthropic")];
|
||||
v.sort();
|
||||
assert_eq!(v[0].as_str(), "anthropic");
|
||||
assert_eq!(v[1].as_str(), "zai");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
pub mod adapter;
|
||||
pub mod billing;
|
||||
pub mod bootstrap_catalog;
|
||||
pub mod catalog;
|
||||
pub mod codec;
|
||||
pub mod ids;
|
||||
pub mod model_ref;
|
||||
pub mod model_test;
|
||||
pub mod provider;
|
||||
pub mod reasoning;
|
||||
pub mod types;
|
||||
|
||||
pub use adapter::{AdapterKind, AgentProfileKind};
|
||||
pub use billing::{
|
||||
AnthropicBillingFacts, AnthropicModelPricing, BilledModelUsage, BilledTokenCounts, CostSource,
|
||||
GeminiBillingFacts, GeminiModelPricing, GeminiStoragePricing, GeminiStorageSegment,
|
||||
ModelBillingFacts, ModelBillingInput, ModelPricing, ModelPricingPolicy, ModelRef, ModelUsage,
|
||||
OpenAiBillingFacts, OpenAiModelPricing, PricePerMTok, Speed, TokenCounts, UsdMicros,
|
||||
};
|
||||
pub use catalog::{
|
||||
ApiKeyHeaderPolicy, BillingPolicy, Catalog, CredentialRef, CredentialRefParseError,
|
||||
FallbackTarget, ModelSelectionError, ProviderAuthConfig, SelectedModel,
|
||||
};
|
||||
pub use codec::CodecKind;
|
||||
pub use ids::{ModelId, ProviderId};
|
||||
pub use model_ref::ModelHandle;
|
||||
pub use model_test::ModelTestMode;
|
||||
pub use provider::Provider;
|
||||
pub use reasoning::ReasoningEffort;
|
||||
pub use types::{
|
||||
Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, ReasoningEffortFeature,
|
||||
};
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::ids::ProviderId;
|
||||
use crate::types::Model;
|
||||
|
||||
/// A reference to a model — either a fully resolved `Model` or a
|
||||
/// provider + model-name pair that hasn't been looked up yet.
|
||||
#[derive(Clone)]
|
||||
pub enum ModelHandle {
|
||||
/// A model whose metadata has been resolved from the catalog.
|
||||
Resolved(Arc<Model>),
|
||||
/// An unresolved provider:model pair (e.g. from CLI input or config).
|
||||
ByName {
|
||||
provider: ProviderId,
|
||||
model: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl ModelHandle {
|
||||
/// The model identifier string (e.g. `"claude-opus-4-6"`).
|
||||
#[must_use]
|
||||
pub fn model_id(&self) -> &str {
|
||||
match self {
|
||||
Self::Resolved(m) => m.id.as_str(),
|
||||
Self::ByName { model, .. } => model,
|
||||
}
|
||||
}
|
||||
|
||||
/// The provider for this model.
|
||||
#[must_use]
|
||||
pub fn provider(&self) -> &ProviderId {
|
||||
match self {
|
||||
Self::Resolved(m) => &m.provider,
|
||||
Self::ByName { provider, .. } => provider,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ModelHandle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}:{}", self.provider(), self.model_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ModelHandle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Resolved(m) => write!(f, "ModelRef::Resolved({:?})", m.id),
|
||||
Self::ByName { provider, model } => f
|
||||
.debug_struct("ModelRef::ByName")
|
||||
.field("provider", provider)
|
||||
.field("model", model)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ProviderId;
|
||||
use crate::catalog::Catalog;
|
||||
|
||||
#[test]
|
||||
fn by_name_display() {
|
||||
let r = ModelHandle::ByName {
|
||||
provider: ProviderId::anthropic(),
|
||||
model: "claude-opus-4-6".to_string(),
|
||||
};
|
||||
assert_eq!(r.to_string(), "anthropic:claude-opus-4-6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn by_name_accessors() {
|
||||
let r = ModelHandle::ByName {
|
||||
provider: ProviderId::openai(),
|
||||
model: "gpt-5.4".to_string(),
|
||||
};
|
||||
assert_eq!(r.model_id(), "gpt-5.4");
|
||||
assert_eq!(r.provider(), &ProviderId::openai());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_display() {
|
||||
let info = Catalog::builtin().get("claude-opus-4-6").unwrap().clone();
|
||||
let r = ModelHandle::Resolved(Arc::new(info));
|
||||
assert_eq!(r.to_string(), "anthropic:claude-opus-4-6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_accessors() {
|
||||
let info = Catalog::builtin().get("gpt-5.4").unwrap().clone();
|
||||
let r = ModelHandle::Resolved(Arc::new(info));
|
||||
assert_eq!(r.model_id(), "gpt-5.4");
|
||||
assert_eq!(r.provider(), &ProviderId::openai());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_format() {
|
||||
let r = ModelHandle::ByName {
|
||||
provider: ProviderId::gemini(),
|
||||
model: "gemini-3.1-pro-preview".to_string(),
|
||||
};
|
||||
let debug = format!("{r:?}");
|
||||
assert!(debug.contains("ByName"));
|
||||
assert!(debug.contains("gemini"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumString, IntoStaticStr};
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
IntoStaticStr,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum ModelTestMode {
|
||||
#[default]
|
||||
Basic,
|
||||
Deep,
|
||||
}
|
||||
|
||||
impl ModelTestMode {
|
||||
#[must_use]
|
||||
pub const fn timeout_secs(self) -> u64 {
|
||||
match self {
|
||||
Self::Basic => 30,
|
||||
Self::Deep => 90,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::adapter::AdapterKind;
|
||||
use crate::catalog::CatalogProvider;
|
||||
use crate::ids::ProviderId;
|
||||
|
||||
/// A user-facing LLM provider from the catalog.
|
||||
///
|
||||
/// The public projection of [`CatalogProvider`]. It deliberately omits
|
||||
/// internal-only fields (`auth`, `extra_headers`, `billing_policy`,
|
||||
/// `agent_profile`) so credential material never reaches the wire.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Provider {
|
||||
pub id: ProviderId,
|
||||
pub display_name: String,
|
||||
pub adapter: AdapterKind,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_url: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub api_key_url: Option<String>,
|
||||
pub priority: i32,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub aliases: Vec<String>,
|
||||
/// Number of catalog models for this provider. Stamped by the handler.
|
||||
pub model_count: u32,
|
||||
/// Catalog default model ID for this provider, if any. Stamped by the
|
||||
/// handler.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default_model: Option<String>,
|
||||
/// True if the server has credential material configured for this provider
|
||||
/// when the response is produced. Always `false` in static catalog data;
|
||||
/// stamped by `GET /providers` per request.
|
||||
#[serde(default)]
|
||||
pub configured: bool,
|
||||
/// Suggested vault secret name for configuring this provider, derived
|
||||
/// from the first vault credential in the catalog. `None` when the
|
||||
/// provider has no vault credential (e.g. Ollama, env-only providers).
|
||||
/// Used by the web UI to prefill the create-secret form.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub expected_secret_name: Option<String>,
|
||||
}
|
||||
|
||||
impl Provider {
|
||||
#[must_use]
|
||||
pub fn from_catalog(
|
||||
provider: &CatalogProvider,
|
||||
model_count: u32,
|
||||
default_model: Option<String>,
|
||||
configured: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: provider.id.clone(),
|
||||
display_name: provider.display_name.clone(),
|
||||
adapter: provider.adapter,
|
||||
base_url: provider.base_url.clone(),
|
||||
api_key_url: provider.api_key_url.clone(),
|
||||
priority: provider.priority,
|
||||
aliases: provider.aliases.clone(),
|
||||
model_count,
|
||||
default_model,
|
||||
configured,
|
||||
expected_secret_name: provider.vault_secret_name().map(str::to_owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Provider;
|
||||
use crate::catalog::Catalog;
|
||||
use crate::ids::ProviderId;
|
||||
|
||||
#[test]
|
||||
fn from_catalog_provider_copies_static_fields_and_supplied_runtime_fields() {
|
||||
let catalog = Catalog::builtin();
|
||||
let anthropic = catalog
|
||||
.provider(&ProviderId::anthropic())
|
||||
.expect("builtin catalog must define anthropic");
|
||||
|
||||
let provider =
|
||||
Provider::from_catalog(anthropic, 7, Some("claude-opus-4-7".to_string()), true);
|
||||
|
||||
assert_eq!(provider.id, ProviderId::anthropic());
|
||||
assert_eq!(provider.display_name, anthropic.display_name);
|
||||
assert_eq!(provider.adapter, anthropic.adapter);
|
||||
assert_eq!(provider.priority, anthropic.priority);
|
||||
assert_eq!(provider.model_count, 7);
|
||||
assert_eq!(provider.default_model.as_deref(), Some("claude-opus-4-7"));
|
||||
assert!(provider.configured);
|
||||
assert_eq!(
|
||||
provider.expected_secret_name.as_deref(),
|
||||
Some("ANTHROPIC_API_KEY"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
//! Shared reasoning-effort enum.
|
||||
//!
|
||||
//! `ReasoningEffort` is a Rust-owned vocabulary type. Catalog data, request
|
||||
//! validation, OpenAPI replacement types, and the LLM client all share one
|
||||
//! enum so that adding a new effort value remains a Rust change.
|
||||
|
||||
use std::cmp::Reverse;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
strum::Display,
|
||||
strum::EnumString,
|
||||
strum::IntoStaticStr,
|
||||
strum::VariantArray,
|
||||
)]
|
||||
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||
#[cfg_attr(feature = "clap", clap(rename_all = "lowercase"))]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum ReasoningEffort {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
XHigh,
|
||||
Max,
|
||||
}
|
||||
|
||||
impl ReasoningEffort {
|
||||
#[must_use]
|
||||
pub fn variants() -> &'static [Self] {
|
||||
<Self as strum::VariantArray>::VARIANTS
|
||||
}
|
||||
|
||||
/// Select the supported effort nearest to this value.
|
||||
///
|
||||
/// The enum declaration defines the ordered progression. When two values
|
||||
/// are equally distant, the higher effort wins.
|
||||
#[must_use]
|
||||
pub fn closest_supported(self, supported: &[Self]) -> Option<Self> {
|
||||
supported
|
||||
.iter()
|
||||
.copied()
|
||||
.min_by_key(|effort| ((self as u8).abs_diff(*effort as u8), Reverse(*effort)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use strum::VariantArray;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_canonical_lowercase_strings() {
|
||||
assert_eq!(
|
||||
ReasoningEffort::from_str("low").unwrap(),
|
||||
ReasoningEffort::Low
|
||||
);
|
||||
assert_eq!(
|
||||
ReasoningEffort::from_str("medium").unwrap(),
|
||||
ReasoningEffort::Medium
|
||||
);
|
||||
assert_eq!(
|
||||
ReasoningEffort::from_str("high").unwrap(),
|
||||
ReasoningEffort::High
|
||||
);
|
||||
assert_eq!(
|
||||
ReasoningEffort::from_str("xhigh").unwrap(),
|
||||
ReasoningEffort::XHigh
|
||||
);
|
||||
assert_eq!(
|
||||
ReasoningEffort::from_str("max").unwrap(),
|
||||
ReasoningEffort::Max
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_strings() {
|
||||
assert!(ReasoningEffort::from_str("none").is_err());
|
||||
assert!(ReasoningEffort::from_str("").is_err());
|
||||
assert!(ReasoningEffort::from_str("HIGH").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_matches_serde_lowercase() {
|
||||
assert_eq!(ReasoningEffort::XHigh.to_string(), "xhigh");
|
||||
assert_eq!(<&'static str>::from(ReasoningEffort::Max), "max");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variants_in_ordered_progression() {
|
||||
let v = ReasoningEffort::VARIANTS;
|
||||
assert_eq!(v[0], ReasoningEffort::Low);
|
||||
assert_eq!(v[v.len() - 1], ReasoningEffort::Max);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closest_supported_uses_exact_match() {
|
||||
assert_eq!(
|
||||
ReasoningEffort::High.closest_supported(&[
|
||||
ReasoningEffort::Low,
|
||||
ReasoningEffort::High,
|
||||
ReasoningEffort::Max,
|
||||
]),
|
||||
Some(ReasoningEffort::High)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closest_supported_rounds_equal_distance_up() {
|
||||
let kimi = [
|
||||
ReasoningEffort::Low,
|
||||
ReasoningEffort::High,
|
||||
ReasoningEffort::Max,
|
||||
];
|
||||
assert_eq!(
|
||||
ReasoningEffort::Medium.closest_supported(&kimi),
|
||||
Some(ReasoningEffort::High)
|
||||
);
|
||||
assert_eq!(
|
||||
ReasoningEffort::XHigh.closest_supported(&kimi),
|
||||
Some(ReasoningEffort::Max)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closest_supported_uses_nearest_lower_value_when_needed() {
|
||||
assert_eq!(
|
||||
ReasoningEffort::Max
|
||||
.closest_supported(&[ReasoningEffort::High, ReasoningEffort::XHigh]),
|
||||
Some(ReasoningEffort::XHigh)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closest_supported_returns_none_for_unsupported_control() {
|
||||
assert_eq!(ReasoningEffort::High.closest_supported(&[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_through_json() {
|
||||
let json = serde_json::to_string(&ReasoningEffort::High).unwrap();
|
||||
assert_eq!(json, "\"high\"");
|
||||
let parsed: ReasoningEffort = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed, ReasoningEffort::High);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ids::{ModelId, ProviderId};
|
||||
use crate::reasoning::ReasoningEffort;
|
||||
|
||||
// --- 2.9 Model ---
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
Default,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
strum::Display,
|
||||
strum::EnumString,
|
||||
strum::IntoStaticStr,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ReasoningEffortFeature {
|
||||
Levels,
|
||||
/// Effort levels are supported, and thinking is natively always-on
|
||||
/// adaptive at the endpoint; a manual thinking on/off toggle is not
|
||||
/// accepted.
|
||||
AlwaysAdaptive,
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ModelLimits {
|
||||
pub context_window: i64,
|
||||
pub max_output: Option<i64>,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ModelFeatures {
|
||||
pub tools: bool,
|
||||
pub vision: bool,
|
||||
pub reasoning: bool,
|
||||
/// Whether this model endpoint supports a native reasoning-effort
|
||||
/// parameter. User-facing allowed effort values live in catalog controls.
|
||||
#[serde(default)]
|
||||
pub reasoning_effort: ReasoningEffortFeature,
|
||||
/// Whether this model endpoint supports prompt caching annotations.
|
||||
#[serde(default)]
|
||||
pub prompt_cache: bool,
|
||||
/// Whether the endpoint only caches when the request marks the cacheable
|
||||
/// prefix with Anthropic-style `cache_control` breakpoints. Set on
|
||||
/// OpenAI-compatible routes fronting Anthropic models (e.g. Claude via
|
||||
/// OpenRouter); dialects whose caching mechanism is implied (native
|
||||
/// Anthropic, Bedrock) ignore it.
|
||||
#[serde(default)]
|
||||
pub cache_control_breakpoints: bool,
|
||||
/// Whether the model endpoint accepts classic sampling parameters
|
||||
/// (`temperature`, `top_p`). Models with always-on adaptive behavior
|
||||
/// reject them.
|
||||
#[serde(default = "default_true")]
|
||||
pub sampling_params: bool,
|
||||
}
|
||||
|
||||
impl ModelFeatures {
|
||||
/// Whether the model endpoint accepts a native reasoning-effort level.
|
||||
#[must_use]
|
||||
pub fn supports_reasoning_effort(&self) -> bool {
|
||||
matches!(
|
||||
self.reasoning_effort,
|
||||
ReasoningEffortFeature::Levels | ReasoningEffortFeature::AlwaysAdaptive
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ModelCosts {
|
||||
pub input_cost_per_mtok: Option<f64>,
|
||||
pub output_cost_per_mtok: Option<f64>,
|
||||
pub cache_input_cost_per_mtok: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ModelControls {
|
||||
/// Exact reasoning-effort values accepted by this provider/model offering.
|
||||
/// An empty list means the request control is unsupported.
|
||||
#[serde(default)]
|
||||
pub reasoning_effort: Vec<ReasoningEffort>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Model {
|
||||
pub id: ModelId,
|
||||
pub provider: ProviderId,
|
||||
pub family: String,
|
||||
pub display_name: String,
|
||||
pub limits: ModelLimits,
|
||||
pub training: Option<String>,
|
||||
pub knowledge_cutoff: Option<String>,
|
||||
pub features: ModelFeatures,
|
||||
/// Required in API responses; defaulted on deserialization so newer
|
||||
/// clients tolerate older servers that predate this field.
|
||||
#[serde(default)]
|
||||
pub controls: ModelControls,
|
||||
pub costs: ModelCosts,
|
||||
pub estimated_output_tps: Option<f64>,
|
||||
pub aliases: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub default: bool,
|
||||
#[serde(default)]
|
||||
pub small_default: bool,
|
||||
/// Whether the server has any credential configured for this model's
|
||||
/// provider at the time of the response. Always `false` in static catalog
|
||||
/// data; populated by `GET /models` per request.
|
||||
#[serde(default)]
|
||||
pub configured: bool,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn id(&self) -> &str {
|
||||
self.id.as_str()
|
||||
}
|
||||
|
||||
pub fn provider(&self) -> &ProviderId {
|
||||
&self.provider
|
||||
}
|
||||
|
||||
pub fn family(&self) -> &str {
|
||||
&self.family
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> &str {
|
||||
&self.display_name
|
||||
}
|
||||
|
||||
pub fn context_window(&self) -> i64 {
|
||||
self.limits.context_window
|
||||
}
|
||||
|
||||
pub fn max_output(&self) -> Option<i64> {
|
||||
self.limits.max_output
|
||||
}
|
||||
|
||||
pub fn supports_tools(&self) -> bool {
|
||||
self.features.tools
|
||||
}
|
||||
|
||||
pub fn supports_vision(&self) -> bool {
|
||||
self.features.vision
|
||||
}
|
||||
|
||||
pub fn supports_reasoning(&self) -> bool {
|
||||
self.features.reasoning
|
||||
}
|
||||
|
||||
pub fn supports_reasoning_effort(&self) -> bool {
|
||||
self.features.supports_reasoning_effort()
|
||||
}
|
||||
|
||||
pub fn supports_prompt_cache(&self) -> bool {
|
||||
self.features.prompt_cache
|
||||
}
|
||||
|
||||
pub fn supports_sampling_params(&self) -> bool {
|
||||
self.features.sampling_params
|
||||
}
|
||||
|
||||
pub fn training(&self) -> Option<&str> {
|
||||
self.training.as_deref()
|
||||
}
|
||||
|
||||
pub fn knowledge_cutoff(&self) -> Option<&str> {
|
||||
self.knowledge_cutoff.as_deref()
|
||||
}
|
||||
|
||||
pub fn input_cost_per_mtok(&self) -> Option<f64> {
|
||||
self.costs.input_cost_per_mtok
|
||||
}
|
||||
|
||||
pub fn output_cost_per_mtok(&self) -> Option<f64> {
|
||||
self.costs.output_cost_per_mtok
|
||||
}
|
||||
|
||||
pub fn cache_input_cost_per_mtok(&self) -> Option<f64> {
|
||||
self.costs.cache_input_cost_per_mtok
|
||||
}
|
||||
|
||||
pub fn estimated_output_tps(&self) -> Option<f64> {
|
||||
self.estimated_output_tps
|
||||
}
|
||||
|
||||
pub fn aliases(&self) -> &[String] {
|
||||
&self.aliases
|
||||
}
|
||||
|
||||
pub fn is_default(&self) -> bool {
|
||||
self.default
|
||||
}
|
||||
|
||||
pub fn is_small_default(&self) -> bool {
|
||||
self.small_default
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ids::ProviderId;
|
||||
|
||||
#[test]
|
||||
fn reasoning_effort_feature_always_adaptive_round_trips() {
|
||||
let parsed: ReasoningEffortFeature =
|
||||
serde_json::from_value(serde_json::json!("always_adaptive")).unwrap();
|
||||
assert_eq!(parsed, ReasoningEffortFeature::AlwaysAdaptive);
|
||||
assert_eq!(
|
||||
serde_json::to_value(parsed).unwrap(),
|
||||
serde_json::json!("always_adaptive")
|
||||
);
|
||||
assert_eq!(parsed.to_string(), "always_adaptive");
|
||||
assert_eq!(
|
||||
"always_adaptive".parse::<ReasoningEffortFeature>().unwrap(),
|
||||
parsed
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inherent_methods_return_correct_values() {
|
||||
let info = Model {
|
||||
id: ModelId::new("model-id"),
|
||||
provider: ProviderId::new("provider-id"),
|
||||
family: "family".to_string(),
|
||||
display_name: "Display Name".to_string(),
|
||||
limits: ModelLimits {
|
||||
context_window: 123_456,
|
||||
max_output: Some(7_890),
|
||||
},
|
||||
training: Some("training".to_string()),
|
||||
knowledge_cutoff: Some("knowledge-cutoff".to_string()),
|
||||
features: ModelFeatures {
|
||||
tools: true,
|
||||
vision: true,
|
||||
reasoning: true,
|
||||
reasoning_effort: ReasoningEffortFeature::Levels,
|
||||
prompt_cache: true,
|
||||
cache_control_breakpoints: false,
|
||||
sampling_params: true,
|
||||
},
|
||||
controls: ModelControls::default(),
|
||||
costs: ModelCosts {
|
||||
input_cost_per_mtok: Some(1.0),
|
||||
output_cost_per_mtok: Some(2.0),
|
||||
cache_input_cost_per_mtok: Some(0.1),
|
||||
},
|
||||
estimated_output_tps: Some(42.0),
|
||||
aliases: vec!["alias".to_string()],
|
||||
default: true,
|
||||
small_default: true,
|
||||
configured: false,
|
||||
};
|
||||
|
||||
assert_eq!(info.id(), "model-id");
|
||||
assert_eq!(info.provider(), &ProviderId::new("provider-id"));
|
||||
assert_eq!(info.family(), "family");
|
||||
assert_eq!(info.display_name(), "Display Name");
|
||||
assert_eq!(info.context_window(), 123_456);
|
||||
assert_eq!(info.max_output(), Some(7_890));
|
||||
assert!(info.supports_tools());
|
||||
assert!(info.supports_vision());
|
||||
assert!(info.supports_reasoning());
|
||||
assert!(info.supports_reasoning_effort());
|
||||
assert!(info.supports_prompt_cache());
|
||||
assert!(info.supports_sampling_params());
|
||||
assert_eq!(info.training(), Some("training"));
|
||||
assert_eq!(info.knowledge_cutoff(), Some("knowledge-cutoff"));
|
||||
assert_eq!(info.input_cost_per_mtok(), Some(1.0));
|
||||
assert_eq!(info.output_cost_per_mtok(), Some(2.0));
|
||||
assert_eq!(info.cache_input_cost_per_mtok(), Some(0.1));
|
||||
assert_eq!(info.estimated_output_tps(), Some(42.0));
|
||||
assert_eq!(info.aliases(), &["alias".to_string()]);
|
||||
assert!(info.is_default());
|
||||
assert!(info.is_small_default());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue