From f29b067a0221d167cfe14e3e6328e5970d1ee035 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Wed, 16 Sep 2026 16:19:50 -0400 Subject: [PATCH 1/7] Fix Answer Question navigation on the runs board --- apps/fabro-web/app/routes/run-detail.test.ts | 78 ++++++++++++++++++-- apps/fabro-web/app/routes/runs.tsx | 4 +- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/apps/fabro-web/app/routes/run-detail.test.ts b/apps/fabro-web/app/routes/run-detail.test.ts index e0462d724..7df596d92 100644 --- a/apps/fabro-web/app/routes/run-detail.test.ts +++ b/apps/fabro-web/app/routes/run-detail.test.ts @@ -19,6 +19,7 @@ import { TEST_PRINCIPAL, makeUsage } from "../lib/test-fixtures"; let currentRunSummary: any = null; let currentRunState: any = null; let currentQuestions: any[] = []; +let currentBoardRuns: any[] = []; let deleteRunApiResult: Promise | null = null; const mountedRenderers: TestRenderer.ReactTestRenderer[] = []; @@ -27,6 +28,12 @@ const deleteRunApiMock = mock((_id: string) => ); const mutateRunListCachesMock = mock((_mutate: unknown) => undefined); const swrMutateMock = mock((_key: unknown) => Promise.resolve(undefined)); +const questionQueryMock = mock((_runId: string, _enabled: boolean) => ({ data: currentQuestions })); +const submitAnswerMock = mock((_answer: unknown) => Promise.resolve(undefined)); +const submitAnswerHookMock = mock((_runId: string) => ({ + isMutating: false, + trigger: submitAnswerMock, +})); mock.module("@headlessui/react", () => ({ Dialog: ({ open, children }: any) => @@ -48,6 +55,10 @@ mock.module("@headlessui/react", () => ({ })); mock.module("../lib/queries", () => ({ + useAllRuns: () => ({ data: { data: currentBoardRuns }, isLoading: false }), + useRunsPage: () => ({ data: null, isLoading: false }), + useAuthConfig: () => ({ data: { methods: [] } }), + useSystemInfo: () => ({ data: null }), useRun: () => ({ data: currentRunSummary, isLoading: false, @@ -56,9 +67,7 @@ mock.module("../lib/queries", () => ({ data: null, isLoading: false, }), - useRunQuestions: () => ({ - data: currentQuestions, - }), + useRunQuestions: questionQueryMock, useRunPullRequest: () => ({ data: null, isLoading: false, @@ -79,6 +88,11 @@ mock.module("../lib/run-events", () => ({ useRunEvents: () => undefined, })); +mock.module("../lib/board-events", () => ({ + shouldRefreshBoardForEvent: () => false, + useBoardEvents: () => undefined, +})); + mock.module("../hooks/use-run-toasts", () => ({ useRunToasts: () => undefined, })); @@ -169,7 +183,7 @@ mock.module("../lib/mutations", () => ({ usePreviewRun: mutationState, useRetryRun: mutationState, useSteerRun: mutationState, - useSubmitInterviewAnswer: mutationState, + useSubmitInterviewAnswer: submitAnswerHookMock, useUpdateRunTitle: mutationState, useUnarchiveRun: mutationState, })); @@ -187,6 +201,7 @@ const { default: RunDetail, resolveDockClearance, } = await import("./run-detail"); +const { default: Runs } = await import("./runs"); mock.restore(); type LifecycleToastState = import("./run-detail/lifecycle-toasts").LifecycleToastState; type RunDetailActionResult = import("./run-detail/lifecycle-toasts").RunDetailActionResult; @@ -334,7 +349,7 @@ async function renderRunDetailHarness({ [ { path: "/runs", - element: h("div", { "data-route": "runs-index" }, "Runs"), + element: h(Runs), }, { path: "/runs/:id", @@ -637,6 +652,10 @@ describe("RunDetail full-height child routes", () => { currentRunSummary = null; currentRunState = null; currentQuestions = []; + currentBoardRuns = []; + questionQueryMock.mockClear(); + submitAnswerMock.mockClear(); + submitAnswerHookMock.mockClear(); deleteRunApiResult = null; deleteRunApiMock.mockClear(); mutateRunListCachesMock.mockClear(); @@ -644,6 +663,55 @@ describe("RunDetail full-height child routes", () => { delete (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT; }); + test("Answer Question on a blocked board card opens that run's pending interview", async () => { + currentBoardRuns = [ + { ...makeRunSummary(), id: "other-run" }, + makeRunSummary({ status: "blocked" }), + ]; + const { renderer, router } = await renderRunDetailHarness({ + initialEntry: "/runs?view=columns", + status: "blocked", + questions: [makeQuestion()], + }); + + await act(async () => { + findButtonByText(renderer, "Answer Question")!.props.onClick(); + }); + + expect(router.state.location.pathname).toBe("/runs/run_1"); + expect(questionQueryMock).toHaveBeenCalledWith("run_1", true); + const interview = renderer.root.findByProps({ "aria-label": "Interview question" }); + expect(textFromTestNode(interview)).toContain("Approve?"); + const answer = interview.findByProps({ "aria-label": "Answer yes" }); + expect(answer.props.disabled).toBe(false); + await act(async () => { + answer.props.onClick(); + }); + expect(submitAnswerHookMock).toHaveBeenCalledWith("run_1"); + expect(submitAnswerMock).toHaveBeenCalledWith({ questionId: "q_1", answer: { kind: "yes" } }); + }); + + for (const status of ["blocked", "running"]) { + test(`a stale blocked card opens the ${status} run without an already answered question`, async () => { + currentBoardRuns = [makeRunSummary({ status: "blocked" })]; + const { renderer, router } = await renderRunDetailHarness({ + initialEntry: "/runs?view=columns", + status, + questions: [], + }); + + await act(async () => { + findButtonByText(renderer, "Answer Question")!.props.onClick(); + }); + + expect(router.state.location.pathname).toBe("/runs/run_1"); + expect(questionQueryMock).toHaveBeenCalledWith("run_1", status === "blocked"); + expect(textFromNode(renderer.toJSON())).toContain("Overview"); + expect(renderer.root.findAllByProps({ "aria-label": "Interview question" })).toHaveLength(0); + expect(submitAnswerMock).not.toHaveBeenCalled(); + }); + } + test("uses a full-height flex wrapper for fullHeight child routes", async () => { const renderer = await renderRunDetail({ initialEntry: "/runs/run_1/files", diff --git a/apps/fabro-web/app/routes/runs.tsx b/apps/fabro-web/app/routes/runs.tsx index 9aeceb78c..69ae6ff04 100644 --- a/apps/fabro-web/app/routes/runs.tsx +++ b/apps/fabro-web/app/routes/runs.tsx @@ -1,5 +1,5 @@ import { useState, useCallback, useMemo, useRef } from "react"; -import { Link, Navigate } from "react-router"; +import { Link, Navigate, useNavigate } from "react-router"; import { CheckIcon, ChevronDownIcon, CommandLineIcon } from "@heroicons/react/24/outline"; import { EllipsisVerticalIcon } from "@heroicons/react/20/solid"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; @@ -360,6 +360,7 @@ function PrCard({ // piece as a sibling `
` below the card body recreates a recurring bug // where stats stack onto separate lines instead of sitting next to size/actions. function PrCardFooter({ pr, actions }: { pr: RunItem; actions?: string[] }) { + const navigate = useNavigate(); const hasActions = actions != null && actions.length > 0; const hasStats = pr.resources != null || @@ -399,6 +400,7 @@ function PrCardFooter({ pr, actions }: { pr: RunItem; actions?: string[] }) { key={label} type="button" disabled={pr.actionDisabled} + onClick={label === "Answer Question" ? () => navigate(`/runs/${pr.id}`) : undefined} className={`inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:text-fg-muted disabled:border-line ${ label === "Merge" ? "border-mint/20 text-mint hover:border-mint/50 hover:text-fg" From 2866cc6bc83318e5a4e2e6300c34d28c534f62fc Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Wed, 16 Sep 2026 16:28:13 -0400 Subject: [PATCH 2/7] Document run approval versus human interview answers --- docs/public/workflows/human-in-the-loop.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/public/workflows/human-in-the-loop.mdx b/docs/public/workflows/human-in-the-loop.mdx index f1cb0bfa7..934e50b77 100644 --- a/docs/public/workflows/human-in-the-loop.mdx +++ b/docs/public/workflows/human-in-the-loop.mdx @@ -19,6 +19,14 @@ approve -> skip [label="[S] Skip"] When execution reaches the gate, the user sees the previous stage's output followed by the node's label ("Approve Plan") and the available options. In the CLI, this appears as an interactive menu. In the web UI, blocked runs show an interview dock on the run page where the user can answer the pending question. + +`fabro approve` and `fabro deny` grant or deny permission to **start a run** waiting +for execution approval. To answer a human gate during execution, use the +interactive CLI prompt or the run page's interview dock. API clients can list +pending questions with `GET /api/v1/runs/{id}/questions` and answer a specific +question with `POST /api/v1/runs/{id}/questions/{qid}/answer`. + + ### Keyboard accelerators The prefixes `[A]`, `[R]`, `[S]` in edge labels serve as keyboard accelerators. Fabro supports three formats: From 1630d263548457712cdbe8fe3dfdcfa7ff432655 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 17 Sep 2026 14:54:00 -0400 Subject: [PATCH 3/7] Clarify interview guidance in approval command help --- docs/public/reference/cli.mdx | 8 ++-- lib/apps/fabro-cli/src/args.rs | 6 +++ lib/apps/fabro-cli/tests/it/cmd/approve.rs | 46 ++++++++++++++++---- lib/apps/fabro-cli/tests/it/cmd/deny.rs | 50 +++++++++++++++++----- 4 files changed, 87 insertions(+), 23 deletions(-) diff --git a/docs/public/reference/cli.mdx b/docs/public/reference/cli.mdx index 659b31bf2..39a72d299 100644 --- a/docs/public/reference/cli.mdx +++ b/docs/public/reference/cli.mdx @@ -63,7 +63,7 @@ fabro [OPTIONS] [COMMAND] | Command | Description | | --- | --- | -| `fabro approve` | Approve pending workflow runs | +| `fabro approve` | Approve pending workflow runs.

Controls permission to start a run. For human interview gates during execution, use the questions API. | | `fabro archive` | Mark terminal runs as archived (reviewed, no further action needed). Archived runs are hidden from default listings | | `fabro artifact` | Inspect and copy run artifacts (screenshots, reports, traces) | | `fabro ask` | Ask Fabro a read-only question about a run | @@ -71,7 +71,7 @@ fabro [OPTIONS] [COMMAND] | `fabro auth` | Manage CLI authentication state | | `fabro completion` | Generate shell completions | | `fabro create` | Register a workflow version and create a submitted run | -| `fabro deny` | Deny pending workflow runs | +| `fabro deny` | Deny pending workflow runs.

Controls permission to start a run. For human interview gates during execution, use the questions API. | | `fabro discord` | Open the Discord community in the browser | | `fabro docs` | Open the docs website in the browser | | `fabro doctor` | Check environment and integration health | @@ -111,7 +111,7 @@ fabro [OPTIONS] [COMMAND] ### `fabro approve` -Approve pending workflow runs +Approve pending workflow runs.

Controls permission to start a run. For human interview gates during execution, use the questions API. ```bash fabro approve [OPTIONS] ... @@ -371,7 +371,7 @@ fabro create [OPTIONS] ### `fabro deny` -Deny pending workflow runs +Deny pending workflow runs.

Controls permission to start a run. For human interview gates during execution, use the questions API. ```bash fabro deny [OPTIONS] ... diff --git a/lib/apps/fabro-cli/src/args.rs b/lib/apps/fabro-cli/src/args.rs index 09d7a01dc..1c09c47a2 100644 --- a/lib/apps/fabro-cli/src/args.rs +++ b/lib/apps/fabro-cli/src/args.rs @@ -1358,8 +1358,14 @@ pub(crate) enum RunsCommands { /// Show detailed information about a workflow run Inspect(InspectArgs), /// Approve pending workflow runs. + /// + /// Controls permission to start a run. For human interview gates during + /// execution, use the questions API. Approve(RunsApproveArgs), /// Deny pending workflow runs. + /// + /// Controls permission to start a run. For human interview gates during + /// execution, use the questions API. Deny(RunsDenyArgs), /// Mark terminal runs as archived (reviewed, no further action needed). /// Archived runs are hidden from default listings. diff --git a/lib/apps/fabro-cli/tests/it/cmd/approve.rs b/lib/apps/fabro-cli/tests/it/cmd/approve.rs index 2f4d23ad9..9af2123bb 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/approve.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/approve.rs @@ -14,21 +14,49 @@ fn help() { success: true exit_code: 0 ----- stdout ----- - Approve pending workflow runs + Approve pending workflow runs. + + Controls permission to start a run. For human interview gates during execution, use the questions API. Usage: fabro approve [OPTIONS] ... Arguments: - ... Run IDs or workflow names to approve + ... + Run IDs or workflow names to approve Options: - --json Output as JSON [env: FABRO_JSON=] - --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=] - --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] - --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] - --quiet Suppress non-essential output [env: FABRO_QUIET=] - --verbose Enable verbose output [env: FABRO_VERBOSE=] - -h, --help Print help + --json + Output as JSON + + [env: FABRO_JSON=] + + --server + Fabro server target: http(s) URL or absolute Unix socket path + + [env: FABRO_SERVER=] + + --debug + Enable DEBUG-level logging (default is INFO) + + [env: FABRO_DEBUG=] + + --no-upgrade-check + Disable automatic upgrade check + + [env: FABRO_NO_UPGRADE_CHECK=true] + + --quiet + Suppress non-essential output + + [env: FABRO_QUIET=] + + --verbose + Enable verbose output + + [env: FABRO_VERBOSE=] + + -h, --help + Print help (see a summary with '-h') ----- stderr ----- "); } diff --git a/lib/apps/fabro-cli/tests/it/cmd/deny.rs b/lib/apps/fabro-cli/tests/it/cmd/deny.rs index 867b65907..2638138c3 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/deny.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/deny.rs @@ -14,22 +14,52 @@ fn help() { success: true exit_code: 0 ----- stdout ----- - Deny pending workflow runs + Deny pending workflow runs. + + Controls permission to start a run. For human interview gates during execution, use the questions API. Usage: fabro deny [OPTIONS] ... Arguments: - ... Run IDs or workflow names to deny + ... + Run IDs or workflow names to deny Options: - --json Output as JSON [env: FABRO_JSON=] - --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=] - --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] - --reason Reason for denying execution - --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] - --quiet Suppress non-essential output [env: FABRO_QUIET=] - --verbose Enable verbose output [env: FABRO_VERBOSE=] - -h, --help Print help + --json + Output as JSON + + [env: FABRO_JSON=] + + --server + Fabro server target: http(s) URL or absolute Unix socket path + + [env: FABRO_SERVER=] + + --debug + Enable DEBUG-level logging (default is INFO) + + [env: FABRO_DEBUG=] + + --reason + Reason for denying execution + + --no-upgrade-check + Disable automatic upgrade check + + [env: FABRO_NO_UPGRADE_CHECK=true] + + --quiet + Suppress non-essential output + + [env: FABRO_QUIET=] + + --verbose + Enable verbose output + + [env: FABRO_VERBOSE=] + + -h, --help + Print help (see a summary with '-h') ----- stderr ----- "); } From e5ba7e3a0f3f378d49abcacade736fcc3c9afdce Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 17 Sep 2026 15:15:42 -0400 Subject: [PATCH 4/7] Keep approval help concise and document attaching to interviews --- docs/public/reference/cli.mdx | 8 ++-- docs/public/workflows/human-in-the-loop.mdx | 8 ++-- lib/apps/fabro-cli/src/args.rs | 10 +---- lib/apps/fabro-cli/tests/it/cmd/approve.rs | 46 ++++--------------- lib/apps/fabro-cli/tests/it/cmd/deny.rs | 50 +++++---------------- lib/apps/fabro-cli/tests/it/cmd/fabro.rs | 4 +- 6 files changed, 31 insertions(+), 95 deletions(-) diff --git a/docs/public/reference/cli.mdx b/docs/public/reference/cli.mdx index 39a72d299..f18744a08 100644 --- a/docs/public/reference/cli.mdx +++ b/docs/public/reference/cli.mdx @@ -63,7 +63,7 @@ fabro [OPTIONS] [COMMAND] | Command | Description | | --- | --- | -| `fabro approve` | Approve pending workflow runs.

Controls permission to start a run. For human interview gates during execution, use the questions API. | +| `fabro approve` | Approve a pending run for execution | | `fabro archive` | Mark terminal runs as archived (reviewed, no further action needed). Archived runs are hidden from default listings | | `fabro artifact` | Inspect and copy run artifacts (screenshots, reports, traces) | | `fabro ask` | Ask Fabro a read-only question about a run | @@ -71,7 +71,7 @@ fabro [OPTIONS] [COMMAND] | `fabro auth` | Manage CLI authentication state | | `fabro completion` | Generate shell completions | | `fabro create` | Register a workflow version and create a submitted run | -| `fabro deny` | Deny pending workflow runs.

Controls permission to start a run. For human interview gates during execution, use the questions API. | +| `fabro deny` | Deny execution of a pending run | | `fabro discord` | Open the Discord community in the browser | | `fabro docs` | Open the docs website in the browser | | `fabro doctor` | Check environment and integration health | @@ -111,7 +111,7 @@ fabro [OPTIONS] [COMMAND] ### `fabro approve` -Approve pending workflow runs.

Controls permission to start a run. For human interview gates during execution, use the questions API. +Approve a pending run for execution ```bash fabro approve [OPTIONS] ... @@ -371,7 +371,7 @@ fabro create [OPTIONS] ### `fabro deny` -Deny pending workflow runs.

Controls permission to start a run. For human interview gates during execution, use the questions API. +Deny execution of a pending run ```bash fabro deny [OPTIONS] ... diff --git a/docs/public/workflows/human-in-the-loop.mdx b/docs/public/workflows/human-in-the-loop.mdx index 934e50b77..b64e953e2 100644 --- a/docs/public/workflows/human-in-the-loop.mdx +++ b/docs/public/workflows/human-in-the-loop.mdx @@ -21,10 +21,10 @@ When execution reaches the gate, the user sees the previous stage's output follo `fabro approve` and `fabro deny` grant or deny permission to **start a run** waiting -for execution approval. To answer a human gate during execution, use the -interactive CLI prompt or the run page's interview dock. API clients can list -pending questions with `GET /api/v1/runs/{id}/questions` and answer a specific -question with `POST /api/v1/runs/{id}/questions/{qid}/answer`. +for execution approval. To answer a human gate during execution, run +`fabro attach ` for an interactive prompt or use the run page's interview dock. +API clients can list pending questions with `GET /api/v1/runs/{id}/questions` and +answer a specific question with `POST /api/v1/runs/{id}/questions/{qid}/answer`. ### Keyboard accelerators diff --git a/lib/apps/fabro-cli/src/args.rs b/lib/apps/fabro-cli/src/args.rs index 1c09c47a2..b85da48c1 100644 --- a/lib/apps/fabro-cli/src/args.rs +++ b/lib/apps/fabro-cli/src/args.rs @@ -1357,15 +1357,9 @@ pub(crate) enum RunsCommands { Rm(RunsRemoveArgs), /// Show detailed information about a workflow run Inspect(InspectArgs), - /// Approve pending workflow runs. - /// - /// Controls permission to start a run. For human interview gates during - /// execution, use the questions API. + /// Approve a pending run for execution. Approve(RunsApproveArgs), - /// Deny pending workflow runs. - /// - /// Controls permission to start a run. For human interview gates during - /// execution, use the questions API. + /// Deny execution of a pending run. Deny(RunsDenyArgs), /// Mark terminal runs as archived (reviewed, no further action needed). /// Archived runs are hidden from default listings. diff --git a/lib/apps/fabro-cli/tests/it/cmd/approve.rs b/lib/apps/fabro-cli/tests/it/cmd/approve.rs index 9af2123bb..919d21341 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/approve.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/approve.rs @@ -14,49 +14,21 @@ fn help() { success: true exit_code: 0 ----- stdout ----- - Approve pending workflow runs. - - Controls permission to start a run. For human interview gates during execution, use the questions API. + Approve a pending run for execution Usage: fabro approve [OPTIONS] ... Arguments: - ... - Run IDs or workflow names to approve + ... Run IDs or workflow names to approve Options: - --json - Output as JSON - - [env: FABRO_JSON=] - - --server - Fabro server target: http(s) URL or absolute Unix socket path - - [env: FABRO_SERVER=] - - --debug - Enable DEBUG-level logging (default is INFO) - - [env: FABRO_DEBUG=] - - --no-upgrade-check - Disable automatic upgrade check - - [env: FABRO_NO_UPGRADE_CHECK=true] - - --quiet - Suppress non-essential output - - [env: FABRO_QUIET=] - - --verbose - Enable verbose output - - [env: FABRO_VERBOSE=] - - -h, --help - Print help (see a summary with '-h') + --json Output as JSON [env: FABRO_JSON=] + --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=] + --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] + --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] + --quiet Suppress non-essential output [env: FABRO_QUIET=] + --verbose Enable verbose output [env: FABRO_VERBOSE=] + -h, --help Print help ----- stderr ----- "); } diff --git a/lib/apps/fabro-cli/tests/it/cmd/deny.rs b/lib/apps/fabro-cli/tests/it/cmd/deny.rs index 2638138c3..29e760006 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/deny.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/deny.rs @@ -14,52 +14,22 @@ fn help() { success: true exit_code: 0 ----- stdout ----- - Deny pending workflow runs. - - Controls permission to start a run. For human interview gates during execution, use the questions API. + Deny execution of a pending run Usage: fabro deny [OPTIONS] ... Arguments: - ... - Run IDs or workflow names to deny + ... Run IDs or workflow names to deny Options: - --json - Output as JSON - - [env: FABRO_JSON=] - - --server - Fabro server target: http(s) URL or absolute Unix socket path - - [env: FABRO_SERVER=] - - --debug - Enable DEBUG-level logging (default is INFO) - - [env: FABRO_DEBUG=] - - --reason - Reason for denying execution - - --no-upgrade-check - Disable automatic upgrade check - - [env: FABRO_NO_UPGRADE_CHECK=true] - - --quiet - Suppress non-essential output - - [env: FABRO_QUIET=] - - --verbose - Enable verbose output - - [env: FABRO_VERBOSE=] - - -h, --help - Print help (see a summary with '-h') + --json Output as JSON [env: FABRO_JSON=] + --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=] + --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] + --reason Reason for denying execution + --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] + --quiet Suppress non-essential output [env: FABRO_QUIET=] + --verbose Enable verbose output [env: FABRO_VERBOSE=] + -h, --help Print help ----- stderr ----- "); } diff --git a/lib/apps/fabro-cli/tests/it/cmd/fabro.rs b/lib/apps/fabro-cli/tests/it/cmd/fabro.rs index 2972f10b8..5687c37a6 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/fabro.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/fabro.rs @@ -31,8 +31,8 @@ fn help() { dump Export a run's durable state to a directory rm Remove one or more workflow runs inspect Show detailed information about a workflow run - approve Approve pending workflow runs - deny Deny pending workflow runs + approve Approve a pending run for execution + deny Deny execution of a pending run archive Mark terminal runs as archived (reviewed, no further action needed). Archived runs are hidden from default listings unarchive Restore archived runs to their prior terminal status model List and test LLM models From 22f46031f561480c7f93a75f69f7cd01a1aab6ae Mon Sep 17 00:00:00 2001 From: "fabro-releases[bot]" Date: Fri, 18 Sep 2026 09:31:57 +0000 Subject: [PATCH 5/7] Bump version to 0.360.0-nightly.0 --- Cargo.lock | 98 +++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1ce43f02..9c161b570 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2246,7 +2246,7 @@ dependencies = [ [[package]] name = "fabro-acp" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -2266,7 +2266,7 @@ dependencies = [ [[package]] name = "fabro-api" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "chrono", "fabro-automation", @@ -2292,7 +2292,7 @@ dependencies = [ [[package]] name = "fabro-auth" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2317,7 +2317,7 @@ dependencies = [ [[package]] name = "fabro-automation" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2338,11 +2338,11 @@ dependencies = [ [[package]] name = "fabro-build-support" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" [[package]] name = "fabro-checkpoint" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "fabro-config", "fabro-types", @@ -2350,7 +2350,7 @@ dependencies = [ [[package]] name = "fabro-cli" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2454,7 +2454,7 @@ dependencies = [ [[package]] name = "fabro-client" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2483,7 +2483,7 @@ dependencies = [ [[package]] name = "fabro-config" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2512,7 +2512,7 @@ dependencies = [ [[package]] name = "fabro-core" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "async-trait", "fabro-types", @@ -2528,7 +2528,7 @@ dependencies = [ [[package]] name = "fabro-db" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2541,7 +2541,7 @@ dependencies = [ [[package]] name = "fabro-dev" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2560,7 +2560,7 @@ dependencies = [ [[package]] name = "fabro-dump" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2574,7 +2574,7 @@ dependencies = [ [[package]] name = "fabro-environment" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2596,7 +2596,7 @@ dependencies = [ [[package]] name = "fabro-github" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2621,7 +2621,7 @@ dependencies = [ [[package]] name = "fabro-graphviz" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -2636,7 +2636,7 @@ dependencies = [ [[package]] name = "fabro-hooks" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "async-trait", "fabro-auth", @@ -2661,7 +2661,7 @@ dependencies = [ [[package]] name = "fabro-http" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2671,7 +2671,7 @@ dependencies = [ [[package]] name = "fabro-install" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2690,7 +2690,7 @@ dependencies = [ [[package]] name = "fabro-interview" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "async-trait", "dialoguer", @@ -2705,7 +2705,7 @@ dependencies = [ [[package]] name = "fabro-llm" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2734,7 +2734,7 @@ dependencies = [ [[package]] name = "fabro-macros" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "clap", "fabro-options-metadata", @@ -2745,7 +2745,7 @@ dependencies = [ [[package]] name = "fabro-manifest" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2774,7 +2774,7 @@ dependencies = [ [[package]] name = "fabro-mcp" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -2787,7 +2787,7 @@ dependencies = [ [[package]] name = "fabro-mcp-server" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2814,7 +2814,7 @@ dependencies = [ [[package]] name = "fabro-mcp-store" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "chrono", "fabro-db", @@ -2832,7 +2832,7 @@ dependencies = [ [[package]] name = "fabro-oauth" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2854,7 +2854,7 @@ dependencies = [ [[package]] name = "fabro-options-metadata" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "serde", "serde_json", @@ -2862,7 +2862,7 @@ dependencies = [ [[package]] name = "fabro-proc" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "cc", "libc", @@ -2875,7 +2875,7 @@ dependencies = [ [[package]] name = "fabro-redact" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "aho-corasick", "ref-cast", @@ -2891,7 +2891,7 @@ dependencies = [ [[package]] name = "fabro-sandbox" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2929,7 +2929,7 @@ dependencies = [ [[package]] name = "fabro-server" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3027,7 +3027,7 @@ dependencies = [ [[package]] name = "fabro-slack" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "fabro-http", "fabro-interview", @@ -3049,18 +3049,18 @@ dependencies = [ [[package]] name = "fabro-spa" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "rust-embed", ] [[package]] name = "fabro-static" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" [[package]] name = "fabro-store" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "async-trait", "bytes", @@ -3093,7 +3093,7 @@ dependencies = [ [[package]] name = "fabro-telemetry" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -3119,7 +3119,7 @@ dependencies = [ [[package]] name = "fabro-template" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -3133,7 +3133,7 @@ dependencies = [ [[package]] name = "fabro-test" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3158,7 +3158,7 @@ dependencies = [ [[package]] name = "fabro-tool" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3182,7 +3182,7 @@ dependencies = [ [[package]] name = "fabro-tracker" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3196,7 +3196,7 @@ dependencies = [ [[package]] name = "fabro-types" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "chrono", "clap", @@ -3223,7 +3223,7 @@ dependencies = [ [[package]] name = "fabro-util" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "console 0.15.11", @@ -3246,7 +3246,7 @@ dependencies = [ [[package]] name = "fabro-validate" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "fabro-acp", "fabro-graphviz", @@ -3259,7 +3259,7 @@ dependencies = [ [[package]] name = "fabro-variable" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3276,7 +3276,7 @@ dependencies = [ [[package]] name = "fabro-vault" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3295,7 +3295,7 @@ dependencies = [ [[package]] name = "fabro-workflow" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3369,7 +3369,7 @@ dependencies = [ [[package]] name = "fabro-workflow-version" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "fabro-config", "fabro-graphviz", @@ -8560,7 +8560,7 @@ dependencies = [ [[package]] name = "twin-github" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" dependencies = [ "axum", "base64", diff --git a/Cargo.toml b/Cargo.toml index 2a57149a5..6ffc31f36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.357.0-nightly.0" +version = "0.360.0-nightly.0" license = "MIT" [workspace.dependencies] From 3e065b806bb3be7a1f38c3ac6c3e895bc70958a6 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 18 Sep 2026 22:07:00 -0400 Subject: [PATCH 6/7] Bump lithos-llm to 43a42ac and migrate catalogs to the codecs schema Move the lithos-llm pin from 55add459 to 43a42ac28e9d9bcf40a91abc02be4f12ca274ebb, and the three Pebble pins from a39f43e to 67c9f48, Pebble `main`, which pins that same lithos-llm revision so Cargo holds one lithos-llm crate. lithos-llm `main` (ca19fac) is one commit further; that commit touches only its nightly workflow, so this pin stays on the revision Pebble unifies with. The `openai`, `anthropic`, `gemini`, and `openai-compatible` features are gone upstream; each expanded to `runtime`, which `bedrock` implies, so the four names leave the fabro-llm feature list. Every other manifest already names `runtime`. The catalog schema now names one adapter and many codecs per provider. `adapter` defaults to `http`, `codecs = [...]` replaces `codec` and defaults to `["openai-chat"]`, and the loader rejects the old `codec` key and the four protocol-named adapter ids. Every inline catalog in tests and docs moves to the new shape: the `openai-compatible` + `openai-chat` pair is dropped as the default, `adapter = "openai"` + `codec = "openai-responses"` becomes `codecs = ["openai-responses"]`, and the one test that swaps in a custom adapter id now adds the line instead of replacing one. The settings reference, the API schema's `Provider.adapter` description, and the SDK page describe the new fields; the three `docs/superpowers/plans/` files that show the old shape are dated, unchecked historical plans and are left as they are. The implied agent profile for an operator provider that declares none used to read the removed protocol adapter ids; it now reads the provider's first codec (Anthropic Messages and Gemini map to their harnesses, the `bedrock` adapter to Anthropic, everything else to OpenAI), with a test for the codec path. Absorbing the rest of the range: OpenRouter and Fireworks now ship enabled, so the two fabro-llm tests that used OpenRouter as the disabled fixture use `bedrock-openai`, and the docs and comments that said the two ship disabled are corrected. The built-in catalog grew past 100 enabled model rows (Vercel, TypeSafe, and the enabled OpenRouter and Fireworks rosters), so the pagination shape test walks `page[offset]` to the last page instead of assuming one page fits. `cargo update -p` on the four crates also re-resolved a few already-locked edges to match the lithos-llm lockfile: `windows-sys` 0.61.2/0.60.2 -> 0.59.0 under dirs-sys, errno, nu-ansi-term, quinn-udp, rustix, rustls-platform-verifier, tempfile, terminal_size, and winapi-util; `windows-core` 0.61.2 -> 0.62.2 under iana-time-zone; `errno` 0.2.8 -> 0.3.14 under signal-hook-registry; and `indexmap` 2.13.0 as a new public dependency of lithos-llm. No package version was added or removed. Co-Authored-By: Claude Fable 5.1 --- Cargo.lock | 31 ++++++------- Cargo.toml | 8 ++-- docs/public/api-reference/fabro-api.yaml | 4 +- docs/public/core-concepts/models.mdx | 4 +- docs/public/integrations/fireworks.mdx | 2 +- docs/public/integrations/openrouter.mdx | 2 +- docs/public/reference/sdk.mdx | 2 +- docs/public/reference/user-configuration.mdx | 12 ++---- lib/apps/fabro-cli/tests/it/cmd/exec.rs | 4 +- lib/apps/fabro-server/src/run_manifest.rs | 2 - .../src/server/handler/sessions.rs | 4 +- lib/apps/fabro-server/src/server/tests.rs | 18 ++------ lib/apps/fabro-server/tests/it/pagination.rs | 35 ++++++++++----- lib/components/fabro-llm/Cargo.toml | 2 +- lib/components/fabro-llm/src/catalog.rs | 43 +++++++++++++++---- lib/components/fabro-llm/src/selection.rs | 8 ++-- lib/components/fabro-validate/src/lib.rs | 2 - .../src/handler/llm/fallback.rs | 5 ++- .../fabro-workflow/src/handler/prompt.rs | 4 -- .../fabro-workflow/src/operations/start.rs | 2 - .../src/pipeline/pull_request.rs | 2 - .../src/transforms/model_resolution.rs | 2 - .../fabro-workflow/tests/it/integration.rs | 2 - .../fabro-workflow/tests/it/pebble_agent.rs | 2 - lib/foundation/fabro-auth/src/vault_source.rs | 2 - lib/foundation/fabro-config/src/builders.rs | 2 - .../src/commands/docs_options_reference.rs | 10 ++--- lib/foundation/fabro-types/src/catalog_api.rs | 2 +- .../fabro-api-client/src/models/provider.ts | 2 +- 29 files changed, 112 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c161b570..e55d59432 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2063,7 +2063,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2190,7 +2190,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4358,7 +4358,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -4888,7 +4888,7 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lithos-llm" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/lithos-llm?rev=55add4596b861a0623d00c3a54aa5c147c8d504b#55add4596b861a0623d00c3a54aa5c147c8d504b" +source = "git+https://github.com/lithoscomputer/lithos-llm?rev=43a42ac28e9d9bcf40a91abc02be4f12ca274ebb#43a42ac28e9d9bcf40a91abc02be4f12ca274ebb" dependencies = [ "async-trait", "aws-config", @@ -4900,6 +4900,7 @@ dependencies = [ "crc32fast", "futures-core", "futures-util", + "indexmap 2.13.0", "mime_guess", "reqwest 0.13.4", "serde", @@ -5304,7 +5305,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5869,7 +5870,7 @@ dependencies = [ [[package]] name = "pebble-agent" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=a39f43e26effdf99635eaf343f095c17157c9c93#a39f43e26effdf99635eaf343f095c17157c9c93" +source = "git+https://github.com/lithoscomputer/pebble?rev=67c9f486dd28f15c04e8d590a91e6f5563f7605d#67c9f486dd28f15c04e8d590a91e6f5563f7605d" dependencies = [ "async-trait", "futures-util", @@ -5886,7 +5887,7 @@ dependencies = [ [[package]] name = "pebble-cli-core" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=a39f43e26effdf99635eaf343f095c17157c9c93#a39f43e26effdf99635eaf343f095c17157c9c93" +source = "git+https://github.com/lithoscomputer/pebble?rev=67c9f486dd28f15c04e8d590a91e6f5563f7605d#67c9f486dd28f15c04e8d590a91e6f5563f7605d" dependencies = [ "anyhow", "async-trait", @@ -5915,7 +5916,7 @@ dependencies = [ [[package]] name = "pebble-coding-agent" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/pebble?rev=a39f43e26effdf99635eaf343f095c17157c9c93#a39f43e26effdf99635eaf343f095c17157c9c93" +source = "git+https://github.com/lithoscomputer/pebble?rev=67c9f486dd28f15c04e8d590a91e6f5563f7605d#67c9f486dd28f15c04e8d590a91e6f5563f7605d" dependencies = [ "async-trait", "futures-util", @@ -6284,7 +6285,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -6761,7 +6762,7 @@ dependencies = [ "errno 0.3.14", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6820,7 +6821,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7467,7 +7468,7 @@ version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ - "errno 0.2.8", + "errno 0.3.14", "libc", ] @@ -8032,7 +8033,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -8067,7 +8068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -9134,7 +9135,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6ffc31f36..a3683fbb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,7 @@ insta = "1" fabro-test = { path = "lib/foundation/fabro-test" } # Provider-neutral LLM catalog and client. Pinned to a revision until 0.x is # published to crates.io. -lithos-llm = { git = "https://github.com/lithoscomputer/lithos-llm", rev = "55add4596b861a0623d00c3a54aa5c147c8d504b", default-features = false } +lithos-llm = { git = "https://github.com/lithoscomputer/lithos-llm", rev = "43a42ac28e9d9bcf40a91abc02be4f12ca274ebb", default-features = false } # Deterministic OpenAI twin used by twin-mode E2E tests; the same revision # lithos-llm verifies its codecs against. twin-openai = { git = "https://github.com/lithoscomputer/twins", rev = "ca45f0e50a6716d716aa2f638ca3cf767e88f613" } @@ -124,9 +124,9 @@ sandbox-driver-testing = { git = "https://github.com/lithoscomputer/sandbox-driv # sandbox, so the pebble and sandbox-driver pins move independently. Pebble # pins the same lithos-llm rev as fabro, and its lockfile policy is that # every shared crate resolves to the version lithos-llm locks. -pebble-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "a39f43e26effdf99635eaf343f095c17157c9c93" } -pebble-coding-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "a39f43e26effdf99635eaf343f095c17157c9c93", features = ["mcp", "search-providers"] } -pebble-cli-core = { git = "https://github.com/lithoscomputer/pebble", rev = "a39f43e26effdf99635eaf343f095c17157c9c93" } +pebble-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "67c9f486dd28f15c04e8d590a91e6f5563f7605d" } +pebble-coding-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "67c9f486dd28f15c04e8d590a91e6f5563f7605d", features = ["mcp", "search-providers"] } +pebble-cli-core = { git = "https://github.com/lithoscomputer/pebble", rev = "67c9f486dd28f15c04e8d590a91e6f5563f7605d" } sentry = { version = "0.35", default-features = false, features = ["backtrace", "contexts", "ureq", "rustls"] } fork = "0.2" exec = "0.3" diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index e620dbd55..af816268f 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -8417,8 +8417,8 @@ components: example: "Anthropic" adapter: type: string - description: "lithos adapter id the provider speaks, such as `anthropic`, `openai`, `gemini`, or `openai-compatible`." - example: "anthropic" + description: "lithos adapter id the provider uses: `http` or `bedrock`, or a custom adapter's id." + example: "http" base_url: type: string description: Effective API base URL, including any operator override. diff --git a/docs/public/core-concepts/models.mdx b/docs/public/core-concepts/models.mdx index 3bbf7524c..ae0c60fcf 100644 --- a/docs/public/core-concepts/models.mdx +++ b/docs/public/core-concepts/models.mdx @@ -80,13 +80,11 @@ Claude Fable 5 is available as an explicit model but is not the default Anthropi Fabro's catalog is the [lithos-llm](https://docs.rs/lithos-llm) built-in catalog. The `[llm]` table in settings is a second layer over it: 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`, `enabled`, `limits`, `capabilities`, `pricing`, `small_default`, `probe`, `family`, and the cutoffs. The coding harness a model expects lives under `metadata.agent`, a namespace lithos ships and other agents such as Pebble read too. See [Settings Configuration](/reference/user-configuration#llm) for every key. +Provider and model facts use lithos field names: `adapter`, `codecs`, `base_url`, `auth`, `enabled`, `limits`, `capabilities`, `pricing`, `small_default`, `probe`, `family`, and the cutoffs. The coding harness a model expects lives under `metadata.agent`, a namespace lithos ships and other agents such as Pebble read too. See [Settings Configuration](/reference/user-configuration#llm) for every key. ```toml title="settings.toml" [llm.providers.proxy] display_name = "Acme Gateway" -adapter = "openai-compatible" -codec = "openai-chat" base_url = "https://llm-gateway.example.com/v1" auth = { type = "bearer" } aliases = ["gateway"] diff --git a/docs/public/integrations/fireworks.mdx b/docs/public/integrations/fireworks.mdx index 17c70bf2d..742ec1716 100644 --- a/docs/public/integrations/fireworks.mdx +++ b/docs/public/integrations/fireworks.mdx @@ -3,7 +3,7 @@ title: "Fireworks AI" description: "Run open-weights models on Fireworks AI's serverless inference platform" --- -[Fireworks AI](https://fireworks.ai/) serves open-weights models (Kimi, DeepSeek, GLM, Qwen, GPT-OSS, and more) behind an OpenAI-compatible API. Fabro ships a disabled `fireworks` provider entry with a curated model catalog, so you can opt in from `settings.toml` without changing Fabro code. +[Fireworks AI](https://fireworks.ai/) serves open-weights models (Kimi, DeepSeek, GLM, Qwen, GPT-OSS, and more) behind an OpenAI-compatible API. Fabro ships an enabled `fireworks` provider entry with a curated model catalog; it needs only an API key, and `settings.toml` can adjust it without changing Fabro code. ## Prerequisites diff --git a/docs/public/integrations/openrouter.mdx b/docs/public/integrations/openrouter.mdx index ec61ec565..1fb8c6f1c 100644 --- a/docs/public/integrations/openrouter.mdx +++ b/docs/public/integrations/openrouter.mdx @@ -3,7 +3,7 @@ title: "OpenRouter" description: "Route Fabro models through OpenRouter's multi-provider gateway" --- -[OpenRouter](https://openrouter.ai/) is an aggregator that fronts hundreds of models behind one OpenAI-compatible API. Fabro ships a disabled `openrouter` provider entry with a curated model catalog, so you can opt in from `settings.toml` without changing Fabro code. +[OpenRouter](https://openrouter.ai/) is an aggregator that fronts hundreds of models behind one OpenAI-compatible API. Fabro ships an enabled `openrouter` provider entry with a curated model catalog; it needs only an API key, and `settings.toml` can adjust it without changing Fabro code. ## Prerequisites diff --git a/docs/public/reference/sdk.mdx b/docs/public/reference/sdk.mdx index 7e0cca8dc..dba9d5056 100644 --- a/docs/public/reference/sdk.mdx +++ b/docs/public/reference/sdk.mdx @@ -449,7 +449,7 @@ let result = client.complete_with_context(request, context).await; ### Provider adapters -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. +A provider names one lithos adapter in `adapter` (`http`, the default, or `bedrock`) and lists the wire codecs its host speaks in `codecs` (`openai-chat`, the default, `openai-responses`, `anthropic-messages`, `gemini-generate`, or `bedrock-converse`). A new OpenAI-compatible endpoint needs a catalog entry, not code. 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. diff --git a/docs/public/reference/user-configuration.mdx b/docs/public/reference/user-configuration.mdx index 8154900b5..6e24b6cff 100644 --- a/docs/public/reference/user-configuration.mdx +++ b/docs/public/reference/user-configuration.mdx @@ -89,8 +89,6 @@ level = "info" [llm.providers.proxy] display_name = "Acme Gateway" -adapter = "openai-compatible" -codec = "openai-chat" base_url = "https://llm-gateway.example.com/v1" auth = { type = "bearer" } aliases = ["gateway"] @@ -154,8 +152,6 @@ Several built-in providers ship with `enabled = false`. Turn one on by setting ```toml title="settings.toml" [llm.providers.proxy] display_name = "Acme Gateway" -adapter = "openai-compatible" -codec = "openai-chat" base_url = "https://llm-gateway.example.com/v1" auth = { type = "bearer" } priority = 50 @@ -195,11 +191,11 @@ Define or override an LLM provider. The keys are the lithos provider record. | Key | Type / values | Default | Description | |---|---|---|---| | `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. | +| `adapter` | string | `"http"` | lithos adapter id: `http` or `bedrock`. Any other id names a custom adapter the application registered. | +| `codecs` | array | `["openai-chat"]` | Wire codecs the host speaks, in preference order: `openai-chat`, `openai-responses`, `anthropic-messages`, `gemini-generate`, or `bedrock-converse`. A Chat Completions host needs no line. | +| `base_url` | string | required for new providers | Provider API base URL. The `openai-chat` codec 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" }`. | -| `enabled` | boolean | `true` | Set `false` to hide a provider from Fabro. `bedrock`, `bedrock-openai`, `fireworks`, `litellm`, `modal`, `ollama`, and `openrouter` ship disabled. | +| `enabled` | boolean | `true` | Set `false` to hide a provider from Fabro. `bedrock`, `bedrock-openai`, `litellm`, `modal`, and `ollama` ship disabled. | | `priority` | integer | `0` | Higher-priority ready providers win unqualified model and default selection. | | `aliases` | array | `[]` | Additional provider names accepted by model routing and fallback config. | | `default_model` | string | None | The provider's default model id. | diff --git a/lib/apps/fabro-cli/tests/it/cmd/exec.rs b/lib/apps/fabro-cli/tests/it/cmd/exec.rs index d1acba902..6502f5598 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/exec.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/exec.rs @@ -333,7 +333,7 @@ fn exec_accepts_configured_custom_provider_from_settings() { let context = test_context!(); context.write_home( ".fabro/settings.toml", - "_version = 1\n\n[llm.providers.acme-aws]\ndisplay_name = \"Acme AWS\"\nadapter = \"openai-compatible\"\ncodec = \"openai-chat\"\nbase_url = \"https://bedrock.example.invalid/v1\"\nauth = { type = \"bearer\" }\nallow_passthrough = true\n\n[llm.providers.acme-aws.metadata.agent]\nprofile = \"openai\"\n\n[cli.exec.model]\nprovider = \"acme-aws\"\nname = \"acme-claude-sonnet-4-6\"\n", + "_version = 1\n\n[llm.providers.acme-aws]\ndisplay_name = \"Acme AWS\"\nbase_url = \"https://bedrock.example.invalid/v1\"\nauth = { type = \"bearer\" }\nallow_passthrough = true\n\n[llm.providers.acme-aws.metadata.agent]\nprofile = \"openai\"\n\n[cli.exec.model]\nprovider = \"acme-aws\"\nname = \"acme-claude-sonnet-4-6\"\n", ); let mut cmd = context.exec_cmd(); @@ -398,7 +398,7 @@ fn exec_server_target_accepts_configured_custom_provider_from_settings() { let context = test_context!(); context.write_home( ".fabro/settings.toml", - "_version = 1\n\n[llm.providers.acme-aws]\ndisplay_name = \"Acme AWS\"\nadapter = \"openai-compatible\"\ncodec = \"openai-chat\"\nbase_url = \"https://bedrock.example.invalid/v1\"\nauth = { type = \"bearer\" }\nallow_passthrough = true\n\n[llm.providers.acme-aws.metadata.agent]\nprofile = \"openai\"\n\n[cli.exec.model]\nprovider = \"acme-aws\"\nname = \"acme-claude-sonnet-4-6\"\n", + "_version = 1\n\n[llm.providers.acme-aws]\ndisplay_name = \"Acme AWS\"\nbase_url = \"https://bedrock.example.invalid/v1\"\nauth = { type = \"bearer\" }\nallow_passthrough = true\n\n[llm.providers.acme-aws.metadata.agent]\nprofile = \"openai\"\n\n[cli.exec.model]\nprovider = \"acme-aws\"\nname = \"acme-claude-sonnet-4-6\"\n", ); let server = MockServer::start(); server.mock(|when, then| { diff --git a/lib/apps/fabro-server/src/run_manifest.rs b/lib/apps/fabro-server/src/run_manifest.rs index f671d11e4..45a072779 100644 --- a/lib/apps/fabro-server/src/run_manifest.rs +++ b/lib/apps/fabro-server/src/run_manifest.rs @@ -3039,8 +3039,6 @@ digraph Demo { r#" [providers.acme] display_name = "Acme" -adapter = "openai-compatible" -codec = "openai-chat" base_url = "https://api.acme.test/v1" auth = { type = "bearer" } default_model = "acme-large" diff --git a/lib/apps/fabro-server/src/server/handler/sessions.rs b/lib/apps/fabro-server/src/server/handler/sessions.rs index 04ce2c082..03fd56651 100644 --- a/lib/apps/fabro-server/src/server/handler/sessions.rs +++ b/lib/apps/fabro-server/src/server/handler/sessions.rs @@ -1530,8 +1530,8 @@ mod tests { } /// OpenAI and OpenRouter both offer `gpt-5.6-sol` under the `gpt-56-sol` - /// alias; OpenRouter ships disabled, so enable it the way an operator - /// would. + /// alias; both ship enabled, and the overlay makes it the default on + /// each. fn portable_session_catalog() -> Catalog { fabro_llm::test_support::test_catalog_with_overlay( r#" diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index 678ba7c61..7755da168 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -335,8 +335,6 @@ fn acme_overlay(base_url: &str) -> String { r#" [providers.acme] display_name = "Acme" -adapter = "openai-compatible" -codec = "openai-chat" base_url = {base_url} auth = {{ type = "bearer" }} priority = 120 @@ -8417,8 +8415,6 @@ async fn model_api_keeps_duplicate_ids_provider_scoped_and_selects_ready_priorit r#" [providers.direct] display_name = "Direct" -adapter = "openai-compatible" -codec = "openai-chat" base_url = {direct} auth = {{ type = "bearer" }} priority = 120 @@ -8436,8 +8432,6 @@ capabilities = {{ text = true }} [providers.aggregator] display_name = "Aggregator" -adapter = "openai-compatible" -codec = "openai-chat" base_url = {aggregator} auth = {{ type = "bearer" }} priority = 110 @@ -8600,8 +8594,6 @@ async fn test_model_forwards_and_validates_reasoning_effort() { r#" [providers.acme] display_name = "Acme" -adapter = "openai-compatible" -codec = "openai-chat" base_url = {base_url} auth = {{ type = "bearer" }} priority = 120 @@ -9152,8 +9144,8 @@ async fn test_providers_registration_issue_returns_error_without_probe() { // An adapter lithos does not ship cannot be built, so the provider is // configured (it has a vault key) yet unavailable. let overlay = acme_overlay("https://api.acme.test/v1").replace( - "adapter = \"openai-compatible\"", - "adapter = \"not-an-adapter\"", + "display_name = \"Acme\"", + "display_name = \"Acme\"\nadapter = \"not-an-adapter\"", ); let state = TestAppStateBuilder::new() .runtime_settings(default_test_server_settings(), RunLayer::default()) @@ -9226,8 +9218,7 @@ async fn test_providers_mixed_results_preserve_catalog_order_and_counts() { r#" [providers.zeta] display_name = "Zeta" -adapter = "openai" -codec = "openai-responses" +codecs = ["openai-responses"] base_url = {base_url} auth = {{ type = "bearer" }} priority = 50 @@ -9242,8 +9233,7 @@ probe = true [providers.alpha] display_name = "Alpha" -adapter = "openai" -codec = "openai-responses" +codecs = ["openai-responses"] base_url = {base_url} auth = {{ type = "bearer" }} priority = 40 diff --git a/lib/apps/fabro-server/tests/it/pagination.rs b/lib/apps/fabro-server/tests/it/pagination.rs index f773ac207..41a58587f 100644 --- a/lib/apps/fabro-server/tests/it/pagination.rs +++ b/lib/apps/fabro-server/tests/it/pagination.rs @@ -72,16 +72,31 @@ async fn paginated_endpoints_return_correct_shape() { let app = fabro_server::test_support::build_test_router(state); for ep in ENDPOINTS { - // Large limit: paginated shape, has_more = false (all fixture items fit). - // Using an explicit large limit instead of the server default so the test - // stays robust when datasets (e.g. the built-in model catalog) grow. - let json = get_json(app.clone(), &format!("{}?page[limit]=100", ep.path)).await; - assert_paginated_shape(&json, ep.name); - assert_eq!( - json["meta"]["has_more"], false, - "{}: large limit should have has_more=false", - ep.name - ); + // Walk the collection at the largest page the API allows: every page + // has the paginated shape, and the last one reports has_more = false. + // The built-in model catalog is larger than one page, so the walk + // follows `page[offset]` rather than assuming one page fits. + let mut offset = 0; + loop { + let json = get_json( + app.clone(), + &format!("{}?page[limit]=100&page[offset]={offset}", ep.path), + ) + .await; + assert_paginated_shape(&json, &format!("{} offset={offset}", ep.name)); + let page_len = json["data"].as_array().unwrap().len(); + assert!(page_len <= 100, "{}: page exceeded the limit", ep.name); + if json["meta"]["has_more"] == false { + break; + } + assert!( + page_len == 100, + "{}: has_more=true on a page shorter than the limit", + ep.name + ); + offset += page_len; + assert!(offset < 10_000, "{}: has_more never turned false", ep.name); + } // limit=1: at most 1 item, has_more = true (all fixtures have >1 item) let json = get_json(app.clone(), &format!("{}?page[limit]=1", ep.path)).await; diff --git a/lib/components/fabro-llm/Cargo.toml b/lib/components/fabro-llm/Cargo.toml index cccc5fee2..21a993bed 100644 --- a/lib/components/fabro-llm/Cargo.toml +++ b/lib/components/fabro-llm/Cargo.toml @@ -27,7 +27,7 @@ fabro-redact.workspace = true fabro-static.workspace = true fabro-types = { path = "../../foundation/fabro-types" } futures.workspace = true -lithos-llm = { workspace = true, features = ["builtin-catalog", "openai", "anthropic", "gemini", "openai-compatible", "bedrock", "bedrock-aws", "local-files"] } +lithos-llm = { workspace = true, features = ["builtin-catalog", "bedrock", "bedrock-aws", "local-files"] } serde.workspace = true serde_json.workspace = true strum.workspace = true diff --git a/lib/components/fabro-llm/src/catalog.rs b/lib/components/fabro-llm/src/catalog.rs index 0f76d5d34..731f8fe44 100644 --- a/lib/components/fabro-llm/src/catalog.rs +++ b/lib/components/fabro-llm/src/catalog.rs @@ -13,7 +13,8 @@ use fabro_static::EnvVars; use fabro_types::AgentProfileKind; pub use lithos_llm::catalog::Offering; use lithos_llm::catalog::{ - Catalog, CatalogBuilder, CatalogError, CatalogModel, CatalogProvider, Metadata, + Catalog, CatalogBuilder, CatalogError, CatalogModel, CatalogProvider, CodecId, Metadata, + adapter_ids, codec_ids, }; use serde::Deserialize; @@ -101,11 +102,16 @@ fn implied_agent_profiles(catalog: &Catalog) -> String { } /// The profile a provider's wire protocol implies, for a provider whose -/// catalog entry does not name one. +/// catalog entry does not name one. The protocol is the provider's first +/// codec, the one the client sends a generation call on; the `bedrock` +/// adapter speaks Converse to Anthropic-shaped models. fn adapter_agent_profile(provider: &CatalogProvider) -> AgentProfileKind { - match provider.adapter().as_str() { - "anthropic" | "bedrock" => AgentProfileKind::Anthropic, - "gemini" => AgentProfileKind::Gemini, + if provider.adapter().as_str() == adapter_ids::BEDROCK { + return AgentProfileKind::Anthropic; + } + match provider.codecs().first().map(CodecId::as_str) { + Some(codec_ids::ANTHROPIC_MESSAGES) => AgentProfileKind::Anthropic, + Some(codec_ids::GEMINI_GENERATE) => AgentProfileKind::Gemini, _ => AgentProfileKind::OpenAi, } } @@ -241,7 +247,7 @@ enabled = false Some(AgentProfileKind::OpenAi) ); assert_eq!( - agent_profile(&catalog, "openrouter", None), + agent_profile(&catalog, "bedrock-openai", None), None, "disabled providers have no profile to offer" ); @@ -282,8 +288,6 @@ enabled = false r#" [providers.acme] display_name = "Acme" -adapter = "openai-compatible" -codec = "openai-chat" base_url = "https://api.acme.test/v1" auth = { type = "bearer" } default_model = "acme-llama" @@ -308,4 +312,27 @@ capabilities = { text = true, tools = true } Some(AgentProfileKind::OpenAi) ); } + + /// The implied profile follows the provider's first codec, so a host that + /// speaks Anthropic Messages gets the Anthropic harness. + #[test] + fn the_implied_profile_follows_the_first_codec() { + let overlay = LlmLayer( + toml::from_str( + r#" +[providers.acme] +display_name = "Acme" +codecs = ["anthropic-messages"] +base_url = "https://api.acme.test" +auth = { type = "header", name = "x-api-key" } +"#, + ) + .unwrap(), + ); + let catalog = build_catalog(&overlay, &|_| None).unwrap(); + assert_eq!( + agent_profile(&catalog, "acme", None), + Some(AgentProfileKind::Anthropic) + ); + } } diff --git a/lib/components/fabro-llm/src/selection.rs b/lib/components/fabro-llm/src/selection.rs index 4372afddd..63ab7aee4 100644 --- a/lib/components/fabro-llm/src/selection.rs +++ b/lib/components/fabro-llm/src/selection.rs @@ -407,12 +407,12 @@ mod tests { fn disabled_providers_are_not_selectable() { let catalog = test_catalog(); assert!(matches!( - select(&catalog, "gpt-5.4", None, &eligible(&["openrouter"])), + select(&catalog, "gpt-5.4", None, &eligible(&["bedrock-openai"])), Err(ModelSelectionError::NoEligibleOffering { .. }) )); - let enabled = test_catalog_with_overlay("[providers.openrouter]\nenabled = true\n"); - let entry = select(&enabled, "gpt-5.4", None, &eligible(&["openrouter"])).unwrap(); - assert_eq!(entry.provider.id(), &ProviderId::new("openrouter")); + let enabled = test_catalog_with_overlay("[providers.bedrock-openai]\nenabled = true\n"); + let entry = select(&enabled, "gpt-5.4", None, &eligible(&["bedrock-openai"])).unwrap(); + assert_eq!(entry.provider.id(), &ProviderId::new("bedrock-openai")); } #[test] diff --git a/lib/components/fabro-validate/src/lib.rs b/lib/components/fabro-validate/src/lib.rs index 1605dcc3a..bbbc8e711 100644 --- a/lib/components/fabro-validate/src/lib.rs +++ b/lib/components/fabro-validate/src/lib.rs @@ -215,8 +215,6 @@ mod tests { r#" [providers.acme-venice] display_name = "Acme Venice" -adapter = "openai-compatible" -codec = "openai-chat" base_url = "https://api.venice.ai/api/v1" auth = { type = "bearer" } default_model = "venice-large" diff --git a/lib/components/fabro-workflow/src/handler/llm/fallback.rs b/lib/components/fabro-workflow/src/handler/llm/fallback.rs index 447a9cc03..bcdecf5b2 100644 --- a/lib/components/fabro-workflow/src/handler/llm/fallback.rs +++ b/lib/components/fabro-workflow/src/handler/llm/fallback.rs @@ -286,8 +286,9 @@ mod tests { use super::*; - /// Modal and OpenRouter ship disabled; enable them the way an operator - /// would so their models become fallback targets. + /// Modal ships disabled; enable it the way an operator would so its + /// models become fallback targets. OpenRouter ships enabled; the line + /// is kept so the overlay reads the same for both. fn enabled_fallback_catalog() -> Catalog { test_catalog_with_overlay( "[providers.modal]\nenabled = true\n\n[providers.openrouter]\nenabled = true\n", diff --git a/lib/components/fabro-workflow/src/handler/prompt.rs b/lib/components/fabro-workflow/src/handler/prompt.rs index 12e9d9393..1c53a66c6 100644 --- a/lib/components/fabro-workflow/src/handler/prompt.rs +++ b/lib/components/fabro-workflow/src/handler/prompt.rs @@ -680,8 +680,6 @@ mod tests { r#" [providers.acme] display_name = "Acme" - adapter = "openai-compatible" - codec = "openai-chat" base_url = "https://api.acme.test/v1" auth = { type = "bearer" } default_model = "acme-claude" @@ -756,8 +754,6 @@ mod tests { r#" [providers.acme] display_name = "Acme" - adapter = "openai-compatible" - codec = "openai-chat" base_url = "https://api.acme.test/v1" auth = { type = "bearer" } default_model = "acme-claude" diff --git a/lib/components/fabro-workflow/src/operations/start.rs b/lib/components/fabro-workflow/src/operations/start.rs index 27e915f93..d59fb0a3a 100644 --- a/lib/components/fabro-workflow/src/operations/start.rs +++ b/lib/components/fabro-workflow/src/operations/start.rs @@ -1481,8 +1481,6 @@ mod tests { r#" [providers.acme] display_name = "Acme" - adapter = "openai-compatible" - codec = "openai-chat" base_url = "https://api.acme.test/v1" auth = { type = "bearer" } default_model = "acme-claude" diff --git a/lib/components/fabro-workflow/src/pipeline/pull_request.rs b/lib/components/fabro-workflow/src/pipeline/pull_request.rs index db6915877..d37b33866 100644 --- a/lib/components/fabro-workflow/src/pipeline/pull_request.rs +++ b/lib/components/fabro-workflow/src/pipeline/pull_request.rs @@ -779,8 +779,6 @@ mod tests { r#" [providers.mock] display_name = "Mock" -adapter = "openai-compatible" -codec = "openai-chat" base_url = "http://mock.invalid/v1" auth = { type = "bearer" } allow_passthrough = true diff --git a/lib/components/fabro-workflow/src/transforms/model_resolution.rs b/lib/components/fabro-workflow/src/transforms/model_resolution.rs index 450fb65b6..2b0765fe0 100644 --- a/lib/components/fabro-workflow/src/transforms/model_resolution.rs +++ b/lib/components/fabro-workflow/src/transforms/model_resolution.rs @@ -167,8 +167,6 @@ mod tests { r#" [providers.acme-venice] display_name = "Acme Venice" -adapter = "openai-compatible" -codec = "openai-chat" base_url = "https://api.venice.ai/api/v1" auth = { type = "bearer" } priority = 200 diff --git a/lib/components/fabro-workflow/tests/it/integration.rs b/lib/components/fabro-workflow/tests/it/integration.rs index 6937a551f..d6a555627 100644 --- a/lib/components/fabro-workflow/tests/it/integration.rs +++ b/lib/components/fabro-workflow/tests/it/integration.rs @@ -2682,8 +2682,6 @@ async fn shared_thread_compaction_before_routing_audit_succeeds() { r#" [providers.compact] display_name = "Compact" -adapter = "openai-compatible" -codec = "openai-chat" base_url = {base_url} auth = {{ type = "bearer" }} default_model = "compact-model" diff --git a/lib/components/fabro-workflow/tests/it/pebble_agent.rs b/lib/components/fabro-workflow/tests/it/pebble_agent.rs index 8f54f5dbf..6f52794ec 100644 --- a/lib/components/fabro-workflow/tests/it/pebble_agent.rs +++ b/lib/components/fabro-workflow/tests/it/pebble_agent.rs @@ -133,8 +133,6 @@ fn provider_toml(name: &str, model: &str, base_url: &str, profile: &str) -> Stri r#" [providers.{name}] display_name = "{name}" -adapter = "openai-compatible" -codec = "openai-chat" base_url = {base_url} auth = {{ type = "bearer" }} default_model = "{model}" diff --git a/lib/foundation/fabro-auth/src/vault_source.rs b/lib/foundation/fabro-auth/src/vault_source.rs index 7f211d7d3..74a7932b0 100644 --- a/lib/foundation/fabro-auth/src/vault_source.rs +++ b/lib/foundation/fabro-auth/src/vault_source.rs @@ -383,8 +383,6 @@ mod tests { r#" [providers.gateway] display_name = "Gateway" -adapter = "openai-compatible" -codec = "openai-chat" base_url = "https://gateway.test/v1" auth = { type = "bearer" } default_headers = { "x-portkey-api-key" = "{{ secrets.PORTKEY_API_KEY }}", "x-portkey-config" = "@prod" } diff --git a/lib/foundation/fabro-config/src/builders.rs b/lib/foundation/fabro-config/src/builders.rs index 125183e99..d6bdf4608 100644 --- a/lib/foundation/fabro-config/src/builders.rs +++ b/lib/foundation/fabro-config/src/builders.rs @@ -690,8 +690,6 @@ methods = ["dev-token"] [llm.providers.acme] display_name = "Acme" -adapter = "openai-compatible" -codec = "openai-chat" base_url = "https://api.acme.test/v1" auth = { type = "bearer" } enabled = true diff --git a/lib/foundation/fabro-dev/src/commands/docs_options_reference.rs b/lib/foundation/fabro-dev/src/commands/docs_options_reference.rs index b4909121d..9136a261d 100644 --- a/lib/foundation/fabro-dev/src/commands/docs_options_reference.rs +++ b/lib/foundation/fabro-dev/src/commands/docs_options_reference.rs @@ -229,8 +229,6 @@ Several built-in providers ship with `enabled = false`. Turn one on by setting ```toml title="settings.toml" [llm.providers.proxy] display_name = "Acme Gateway" -adapter = "openai-compatible" -codec = "openai-chat" base_url = "https://llm-gateway.example.com/v1" auth = { type = "bearer" } priority = 50 @@ -270,11 +268,11 @@ Define or override an LLM provider. The keys are the lithos provider record. | Key | Type / values | Default | Description | |---|---|---|---| | `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. | +| `adapter` | string | `"http"` | lithos adapter id: `http` or `bedrock`. Any other id names a custom adapter the application registered. | +| `codecs` | array | `["openai-chat"]` | Wire codecs the host speaks, in preference order: `openai-chat`, `openai-responses`, `anthropic-messages`, `gemini-generate`, or `bedrock-converse`. A Chat Completions host needs no line. | +| `base_url` | string | required for new providers | Provider API base URL. The `openai-chat` codec 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" }`. | -| `enabled` | boolean | `true` | Set `false` to hide a provider from Fabro. `bedrock`, `bedrock-openai`, `fireworks`, `litellm`, `modal`, `ollama`, and `openrouter` ship disabled. | +| `enabled` | boolean | `true` | Set `false` to hide a provider from Fabro. `bedrock`, `bedrock-openai`, `litellm`, `modal`, and `ollama` ship disabled. | | `priority` | integer | `0` | Higher-priority ready providers win unqualified model and default selection. | | `aliases` | array | `[]` | Additional provider names accepted by model routing and fallback config. | | `default_model` | string | None | The provider's default model id. | diff --git a/lib/foundation/fabro-types/src/catalog_api.rs b/lib/foundation/fabro-types/src/catalog_api.rs index 35076048c..04622393b 100644 --- a/lib/foundation/fabro-types/src/catalog_api.rs +++ b/lib/foundation/fabro-types/src/catalog_api.rs @@ -75,7 +75,7 @@ pub struct Model { pub struct Provider { pub id: ProviderId, pub display_name: String, - /// lithos adapter id, such as `openai` or `openai-compatible`. + /// lithos adapter id: `http` or `bedrock`, or a custom adapter's id. pub adapter: String, pub base_url: String, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/lib/packages/fabro-api-client/src/models/provider.ts b/lib/packages/fabro-api-client/src/models/provider.ts index 9edd7e550..79a284fcd 100644 --- a/lib/packages/fabro-api-client/src/models/provider.ts +++ b/lib/packages/fabro-api-client/src/models/provider.ts @@ -27,7 +27,7 @@ export interface Provider { */ 'display_name': string; /** - * lithos adapter id the provider speaks, such as `anthropic`, `openai`, `gemini`, or `openai-compatible`. + * lithos adapter id the provider uses: `http` or `bedrock`, or a custom adapter's id. */ 'adapter': string; /** From 6e9be17e134ae14b385ce451b1942722dc8a0e26 Mon Sep 17 00:00:00 2001 From: "fabro-releases[bot]" Date: Sat, 19 Sep 2026 09:31:42 +0000 Subject: [PATCH 7/7] Bump version to 0.361.0-nightly.0 --- Cargo.lock | 98 +++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e55d59432..7c7722461 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2246,7 +2246,7 @@ dependencies = [ [[package]] name = "fabro-acp" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -2266,7 +2266,7 @@ dependencies = [ [[package]] name = "fabro-api" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "chrono", "fabro-automation", @@ -2292,7 +2292,7 @@ dependencies = [ [[package]] name = "fabro-auth" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2317,7 +2317,7 @@ dependencies = [ [[package]] name = "fabro-automation" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2338,11 +2338,11 @@ dependencies = [ [[package]] name = "fabro-build-support" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" [[package]] name = "fabro-checkpoint" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "fabro-config", "fabro-types", @@ -2350,7 +2350,7 @@ dependencies = [ [[package]] name = "fabro-cli" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2454,7 +2454,7 @@ dependencies = [ [[package]] name = "fabro-client" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2483,7 +2483,7 @@ dependencies = [ [[package]] name = "fabro-config" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2512,7 +2512,7 @@ dependencies = [ [[package]] name = "fabro-core" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "async-trait", "fabro-types", @@ -2528,7 +2528,7 @@ dependencies = [ [[package]] name = "fabro-db" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2541,7 +2541,7 @@ dependencies = [ [[package]] name = "fabro-dev" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -2560,7 +2560,7 @@ dependencies = [ [[package]] name = "fabro-dump" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "bytes", @@ -2574,7 +2574,7 @@ dependencies = [ [[package]] name = "fabro-environment" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2596,7 +2596,7 @@ dependencies = [ [[package]] name = "fabro-github" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2621,7 +2621,7 @@ dependencies = [ [[package]] name = "fabro-graphviz" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -2636,7 +2636,7 @@ dependencies = [ [[package]] name = "fabro-hooks" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "async-trait", "fabro-auth", @@ -2661,7 +2661,7 @@ dependencies = [ [[package]] name = "fabro-http" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "fabro-static", "http 1.4.0", @@ -2671,7 +2671,7 @@ dependencies = [ [[package]] name = "fabro-install" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -2690,7 +2690,7 @@ dependencies = [ [[package]] name = "fabro-interview" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "async-trait", "dialoguer", @@ -2705,7 +2705,7 @@ dependencies = [ [[package]] name = "fabro-llm" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2734,7 +2734,7 @@ dependencies = [ [[package]] name = "fabro-macros" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "clap", "fabro-options-metadata", @@ -2745,7 +2745,7 @@ dependencies = [ [[package]] name = "fabro-manifest" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2774,7 +2774,7 @@ dependencies = [ [[package]] name = "fabro-mcp" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -2787,7 +2787,7 @@ dependencies = [ [[package]] name = "fabro-mcp-server" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -2814,7 +2814,7 @@ dependencies = [ [[package]] name = "fabro-mcp-store" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "chrono", "fabro-db", @@ -2832,7 +2832,7 @@ dependencies = [ [[package]] name = "fabro-oauth" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "axum", @@ -2854,7 +2854,7 @@ dependencies = [ [[package]] name = "fabro-options-metadata" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "serde", "serde_json", @@ -2862,7 +2862,7 @@ dependencies = [ [[package]] name = "fabro-proc" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "cc", "libc", @@ -2875,7 +2875,7 @@ dependencies = [ [[package]] name = "fabro-redact" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "aho-corasick", "ref-cast", @@ -2891,7 +2891,7 @@ dependencies = [ [[package]] name = "fabro-sandbox" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -2929,7 +2929,7 @@ dependencies = [ [[package]] name = "fabro-server" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3027,7 +3027,7 @@ dependencies = [ [[package]] name = "fabro-slack" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "fabro-http", "fabro-interview", @@ -3049,18 +3049,18 @@ dependencies = [ [[package]] name = "fabro-spa" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "rust-embed", ] [[package]] name = "fabro-static" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" [[package]] name = "fabro-store" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "async-trait", "bytes", @@ -3093,7 +3093,7 @@ dependencies = [ [[package]] name = "fabro-telemetry" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "base64", @@ -3119,7 +3119,7 @@ dependencies = [ [[package]] name = "fabro-template" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "fabro-types", @@ -3133,7 +3133,7 @@ dependencies = [ [[package]] name = "fabro-test" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3158,7 +3158,7 @@ dependencies = [ [[package]] name = "fabro-tool" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3182,7 +3182,7 @@ dependencies = [ [[package]] name = "fabro-tracker" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "async-trait", @@ -3196,7 +3196,7 @@ dependencies = [ [[package]] name = "fabro-types" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "chrono", "clap", @@ -3223,7 +3223,7 @@ dependencies = [ [[package]] name = "fabro-util" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "console 0.15.11", @@ -3246,7 +3246,7 @@ dependencies = [ [[package]] name = "fabro-validate" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "fabro-acp", "fabro-graphviz", @@ -3259,7 +3259,7 @@ dependencies = [ [[package]] name = "fabro-variable" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3276,7 +3276,7 @@ dependencies = [ [[package]] name = "fabro-vault" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "chrono", @@ -3295,7 +3295,7 @@ dependencies = [ [[package]] name = "fabro-workflow" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "anyhow", "assert_cmd", @@ -3369,7 +3369,7 @@ dependencies = [ [[package]] name = "fabro-workflow-version" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "fabro-config", "fabro-graphviz", @@ -8561,7 +8561,7 @@ dependencies = [ [[package]] name = "twin-github" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" dependencies = [ "axum", "base64", diff --git a/Cargo.toml b/Cargo.toml index a3683fbb4..37d1a1144 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.360.0-nightly.0" +version = "0.361.0-nightly.0" license = "MIT" [workspace.dependencies]