Resolve the model catalog table conflict in docs/public/core-concepts/models.mdx
by keeping both changes: this branch's `kimi` -> `moonshot` provider rename for
the Kimi rows, and main's new DeepSeek V4 rows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Consolidation pass over the fallback feature, no intended behavior
changes beyond noted validation and event-shape cleanups:
- Unify the two parallel notice types: FallbackPlanNotice is gone;
ModelFallbackNotice now owns the runtime NoNearbyReasoningLevel case
and the shared ChainEmpty wording. Notices emit through a new
Emitter::notice_scoped with their own level, and each distinct notice
is emitted once per run instead of on every LLM call.
- Move canonical_model_id onto Catalog so chain keys are written and
read through one function; reject provider-qualified fallback keys,
which could never match at dispatch and were silently dead config.
- Type FallbackTarget as ProviderId/ModelId, removing repeated
ProviderId::new re-wrapping at every use site.
- Derive FallbackPlan's current route from a position index instead of
storing current/requested_controls copies; advance() no longer has
unreachable None branches.
- Bundle the agent invocation's live state (session, bridge, lease,
forwarder, accounting) into LiveAgentInvocation; failover_agent_session
drops from 21 parameters to 7 and the six copies of the
abort/discard/classify teardown collapse into two methods.
- Share one route_request builder between one_shot and its failover
loop; complete_one_shot_request takes the request by value instead of
deep-cloning the message payload per call.
- Event::Failover carries FailoverProps directly; the props' original
route and attempt fields are now required, and reasoning efforts are
typed ReasoningEffort instead of strings.
- Reuse RunModelSettings/RunModelControls in fabro-api via
with_replacement, add the missing controls property to the OpenAPI
schema, regenerate the TS client, and add the type-identity/JSON
parity test.
- Smaller cleanups: ReasoningEffort::closest_supported uses enum
discriminants; ModelFallbackPolicy gains len(); resolve_model_fallbacks
takes a provider slice; duplicate-target filtering lives only in the
resolver.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses review feedback on the fallback notice work.
A provider-only fallback such as `openrouter` needs the primary model's
catalog entry to find the closest capability match. When the primary is
itself a passthrough selector there is no entry, so every provider-only
candidate was skipped with "provider `X` has no compatible model" even
when that provider had plenty. Adds a `PrimaryNotInCatalog` notice that
names the missing primary instead of blaming the provider.
Also from review:
- `code()` was a wildcard fallthrough, which docs/internal/events-strategy.md
forbids for new variants. Now exhaustive.
- `NoConfiguredOffering` discarded the `providers` list that
`NoEligibleOffering` hands it. The notice now names the providers that do
offer the model.
- `ModelFallbackNotice::reference` was a rendered `String`; it is now the
`ModelRef` it came from, which also drops the per-candidate double
allocation the previous refactor introduced.
- `ResolvedStartLlm` unpacked and repacked `ResolvedFallbackChain`
field-for-field; it now holds it directly.
- Added `FallbackTarget: Display` as `provider:model`, replacing two
hand-written `"{}:{}"` format strings.
- `Catalog::select` still inlined the `require_provider` body.
- Emission moved to `ModelFallbackNotice::emit_all`, covered by a new test
proving notices reach the event stream with the right level, code, and
message. Nothing tested that hand-off before.
Documented in `resolve_fallback_chain` why an unknown provider stays a hard
error while an unconfigured one is skipped, and that an unqualified unknown
selector pins to the primary's provider.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up cleanup on the portable fallback chain work.
- Add `Catalog::require_provider` and `Catalog::provider_id`, replacing the
`catalog_provider_id` free function in `start.rs` and two copies of the same
`provider(..).ok_or_else(UnknownProvider)` block inside the catalog.
- Add `FallbackTarget::new` and use it for the six struct literals that each
stringified a provider and model by hand.
- Extract per-candidate resolution into `resolve_fallback_candidate`, returning
a `FallbackCandidate` that is either a target or the skip reason. This flattens
`resolve_fallback_chain` from four levels of nesting to one loop and splits the
qualified/unqualified model arms into separate match patterns.
- Drop the `seen` HashSet and its per-candidate key clones in favor of a
`contains` check on the chain being built; fallback chains hold a handful of
entries.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reduce duplication and over-specification introduced with the Modal
provider, without changing shipped behavior.
- Extract enabled_provider_catalog and assert_deep_tool_round_trip in
the fabro-llm integration tests. The Poolside, Fireworks, OpenRouter,
and Modal deep round trips were four near-identical copies.
- Add ApiCredential::with_extra_headers for providers that authenticate
with request headers instead of an API key.
- Replace the unreachable require_env guards in the Modal e2e test with
the std::env::var form used by every sibling test, and register
MODAL_TOKEN_ID and MODAL_TOKEN_SECRET in EnvVars.
- Collapse modal_requires_both_vault_proxy_tokens to a single case. The
loop rebuilt the whole built-in catalog per iteration.
- Drop tautological and over-specified assertions: the api_key_url doc
URL, the forced default/probe lookups on a single-model provider, and
the get_on_provider loop that could not fail.
- Inline the single-use modal_env_catalog fixture and note why it
overrides the shipped secrets templates.
- Sort the MODAL_* keys in .env.example, and record in modal.toml why
api_id keeps the Hugging Face capitalization.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
catalog_from_settings_rejects_duplicate_provider_api_ids was a copy of
catalog_from_settings_rejects_duplicate_model_aliases with the alias
declaration swapped for an api_id. Fold them into one table-driven test
so the shared invariant is stated once: canonical IDs, aliases, and API
IDs occupy a single identifier namespace per provider.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up cleanup on the provider-qualified selector work.
- build_model_indexes now takes the paired (Model, CatalogModelSettings)
slice it is built from, instead of a separate settings map. This drops
a per-model map lookup with two cloned key components and removes the
expect() panic path for an invariant the caller already guarantees.
- get_on_provider expresses the exact-then-legacy lookup as one closure
applied twice, rather than a nested then/flatten chain.
- ModelRef::from_str selects the separator first and then checks both
sides once, so the empty-side check is no longer duplicated across two
branches and the slash split no longer allocates a Vec.
- Shorten the TooManySlashes message to the action the user should take.
- Merge the two near-identical fallback chain tests into one that runs
both qualified selector forms through the same assertion.
- The fallbacks splice test now asserts through the existing Serialize
impl instead of hand-rolling the ModelRefOrSplice rendering.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`small_default_for_provider` fell back to the provider's normal default
when no model was marked `small_default`. That turned "give me the small
utility model" into "give me the flagship" for any provider without one.
Run title generation asks for the small default, then gives it a 64-token
budget and a 10s timeout. On a server where kimi is the highest-priority
configured provider, that resolved to kimi-k3 — an always-reasoning model
that burned 161 reasoning tokens before emitting anything. The structured
output never completed, `generate_object` returned NoObjectGenerated, and
the caller silently kept the deterministic title.
Return `None` instead, and have `small_default_for_configured_ids` move on
to the next configured provider. The ordinary default is used only when no
configured provider marks a small model, and it now comes from the
configured set rather than the global catalog default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex drives the GPT-5.6 models with a much narrower tool set than the
other OpenAI models: a shell, `apply_patch`, and `update_plan`. It has no
file-read, file-write, grep, glob, or fetch tool at all -- reading and
searching go through the shell, and every write goes through
`apply_patch`. Offering 5.6 fabro's extra tools advertises affordances its
instructions never mention, so this adds a profile that registers only
what Codex does.
The profile is selected per model via `agent_profile = "gpt56"` on the six
5.6 rows (three each on `openai` and `openrouter`), following the existing
Kimi-over-a-gateway pattern. Every other model on those providers keeps
its provider default, with no code branch and no version sniffing.
- `ToolVocabulary::Codex` renames `shell` to `shell_command`; a strum
alias keeps `from_any_name` resolving it to `NativeTool::Shell`, so
permissions, categories, and telemetry still key on the canonical name.
- `shell_command` gains `workdir`, passed to the `cwd` argument
`execute_shell_command` already accepted, with Codex's "always set
`workdir`, do not `cd`" guidance.
- `prompts/gpt56.md.j2` is adapted from Codex's 5.6 `base_instructions`,
which are byte-identical across Sol, Terra, and Luna. A header comment
records provenance and the departures fabro's harness forces.
This is an alignment-only pass: it matches Codex's tool contract while
keeping direct tool calls. Codex actually drives 5.6 in code mode, with a
single `exec` tool taking JavaScript and every other tool reached through
a `tools` object inside a V8 isolate. That is deliberately out of scope.
Luna's `multi_agent_version: v1` (vs v2 on Sol and Terra) is also out of
scope. It only changes the sub-agent tool set, which fabro registers from
the caller rather than the profile, and fabro's current set matches
neither version exactly.
Two server cancel-timing tests are adjusted. `gpt-5.6-sol` is the
`openai` provider's default model, so runs that name no model now build a
3-tool profile instead of an 8-tool one and reach their first stage
sooner. `full_http_lifecycle_cancel` asserted `status.kind == "blocked"`
at the instant of cancel, which the worker is free to change the moment it
is signaled; it now accepts either live state, matching the tolerance its
own comment already documents for `pending_control`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Model default reasoning explicitly at the provider-route level so always-reasoning endpoints without effort controls receive summary headroom. Cap all summary requests at model output limits and bound retained visible summaries to the original allowance. Reuse builtin catalog fixtures and named budget constants in tests, and document the new model setting.
Kimi models ran on the OpenAI profile, which exists to look like Codex. Give
them their own profile derived from Kimi Code's system prompt.
Routing is per model, not per provider, because Kimi models are served both
directly by Moonshot and through gateways. `kimi` sets agent_profile at the
provider level; the Kimi model rows on `openrouter` set it individually, so a
gateway route behaves like the direct one while other OpenRouter models keep
the provider's OpenAI profile.
The profile targets a measured failure. Across two observed K3 implementation
stages, 32 of 35 tool failures were the same thing: writes to files the model
had not read, rejected by the workspace read-before-write guard, or
`old_string` values reconstructed from memory rather than taken from a read.
Kimi Code drills this rule in its own tool descriptions, so the profile does
too -- `edit_file` and `write_file` carry Kimi-specific descriptions naming the
guard and the failure text the model will see, alongside a "Reading Before
Writing" section in the system prompt. Profiles own their tool registries, so
this re-describes the tools for Kimi only; every other profile is untouched and
the executors and JSON schemas are shared unchanged.
Tool names stay fabro's existing snake_case. Whether Kimi Code's PascalCase
vocabulary measurably helps is untested, and renaming would also mean updating
the name-keyed categories in tool_permissions.rs, where an unknown tool falls
back to Shell. That is a separate change to make on evidence.
The prompt is a subtractive port: capabilities fabro does not have -- plan
mode, background tasks, cron, subagent swarms, the cwd tree listing -- are
dropped rather than promised. The shell timeout default matches Kimi Code's 60s
and memory discovery reads AGENTS.md, which is the only instruction file Kimi
Code looks for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The catalog comment showed `fabro provider login fireworks`, but
`--provider` is a required flag: `fabro provider login --provider fireworks`.
- The remote-server `fabro model test` example omitted `--provider fireworks`,
which could resolve the slug against a different provider.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot review flagged that dropping #[serde(default)] makes newer
clients hard-fail against servers that predate the controls field.
The late-added Model fields (default, small_default, configured) set
the precedent: required in the OpenAPI spec, defaulted on
deserialization. An empty controls list already means "unsupported",
so the degraded value is semantically correct.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the missing with_replacement for ModelControls so progenitor reuses
fabro_model::ModelControls instead of generating a dead parallel DTO,
re-export it from fabro_api::types, and assert type identity in the
round-trip test.
Drop #[serde(default)] from Model.controls and
ModelControls.reasoning_effort: the OpenAPI spec marks both required,
matching the strict deserialization of the sibling features/costs
fields. Update CLI stub payloads to include the now-required field.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Rewrite the Fireworks tool round-trip E2E test on the shared
run_model_test deep-test pattern used by the OpenRouter and Poolside
opt-in provider tests, instead of a fourth hand-rolled copy of the
multiply-tool scaffold.
- Drop the "(via Fireworks)" display-name suffix from slugs that have no
first-party provider (kimi-k2.6, deepseek-v4-*, minimax-m2.7),
matching the OpenRouter convention; rename "Qwen 3.7 Plus" to
"Qwen3.7 Plus" to match existing Qwen entries.
- Fix kimi-k2.6 vision flag to false, matching the OpenRouter entry for
the same slug (the portability test asserts they are the same model).
- Assert small_default_for_provider and per-model family/vision/
reasoning in the catalog tests, mirroring sibling provider tests.
- Add Troubleshooting and Further reading sections to the Fireworks
docs page, matching the other opt-in provider pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fallback provider set was always catalog.all_provider_ids(), computed
at every call site and threaded through five layers alongside the catalog
itself. Fold it into Catalog::resolve_selection_with_catalog_fallback and
carry only a catalog_fallback flag through the transform/validate/
materialize entry points.
- materialize_run delegates to resolve_run_model again instead of
re-inlining its provider normalization and selection
- run_preflight derives ready providers from llm_result instead of
taking both, so callers cannot pass inconsistent pairs; the legacy
tests now exercise the production ready-first routing path
- AppState::resolve_llm_client_with_ready_ids replaces three copies of
resolve-then-extract-provider-ids, and ready_llm_provider_ids
delegates to it
- the unreachable "model resolution failed" preflight check becomes an
invariant error where the materialized run is produced
- validate_prepared_manifest_with_vars/_for_preflight share the
ValidateInput construction
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a disabled-by-default `fireworks` provider to the built-in catalog,
served through the existing openai_compatible adapter/codec. The curated
roster covers Kimi K2.7 Code (default), Kimi K2.6, DeepSeek V4 Pro/Flash,
GLM 5.2, MiniMax M2.7, Qwen 3.7 Plus, and GPT-OSS 120B/20B (small
default + probe), with serverless pricing including cached-input rates.
All api_ids were verified live against /chat/completions (Fireworks'
GET /v1/models only returns a featured subset), and serverless responses
were confirmed to report prompt_tokens_details.cached_tokens, so cache
billing works through the existing codec path.
FIREWORKS_API_KEY is registered as an optional vault secret; provider
login, vault storage, and diagnostics probing are catalog-driven and
need no code changes. Includes catalog/install tests, two live e2e
tests, an integrations docs page, and a provider logo for the web UI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>