Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 1 addition & 40 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ concurrency:
# to anonymous, which is what the cache is there to make rare.
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
RUSTUP_TOOLCHAIN: stable

jobs:
fmt:
Expand All @@ -33,8 +32,6 @@ jobs:

clippy:
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: stable
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -83,8 +80,6 @@ jobs:

test:
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: stable
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -133,7 +128,6 @@ jobs:
docs:
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: stable
RUSTDOCFLAGS: -D warnings
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -162,8 +156,6 @@ jobs:

check-workshop:
runs-on: windows-latest
env:
RUSTUP_TOOLCHAIN: stable
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -234,8 +226,6 @@ jobs:
# missing-system-package class of failure the Windows-only job cannot see.
check-workshop-linux:
runs-on: ubuntu-22.04
env:
RUSTUP_TOOLCHAIN: stable
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -341,35 +331,6 @@ jobs:
working-directory: crates/gateway-config-ui/ui
run: npm test

msrv:
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.89.0
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@1.89.0

- name: Cache cargo
uses: Swatinem/rust-cache@v2

- name: Cache the embedding model
uses: ./.github/actions/hf-model-cache

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: crates/*/ui/package-lock.json

- name: Install UI dependencies
run: |
npm ci --prefix crates/workshop-server/ui
npm ci --prefix crates/gateway-config-ui/ui

- name: Check on MSRV
run: cargo check --locked --workspace --exclude workshop --exclude workshop-server --all-targets --all-features

supply-chain:
runs-on: ubuntu-latest
steps:
Expand All @@ -391,7 +352,7 @@ jobs:
# or that job bypasses this gate.
ci-green:
runs-on: ubuntu-latest
needs: [fmt, clippy, test, docs, check-workshop, check-workshop-linux, ui, msrv, supply-chain]
needs: [fmt, clippy, test, docs, check-workshop, check-workshop-linux, ui, supply-chain]
if: always()
steps:
- name: Verify every job succeeded
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/release-workshop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ concurrency:
group: release-workshop-${{ github.ref_name }}
cancel-in-progress: false

env:
# The repo's rust-toolchain.toml pins the MSRV for local builds; CI tests
# the current stable.
RUSTUP_TOOLCHAIN: stable

jobs:
prepare:
runs-on: ubuntu-latest
Expand Down
7 changes: 0 additions & 7 deletions .github/workflows/stt-miri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,9 @@ jobs:
native-whisper:
runs-on: [self-hosted, windows, cuda]
timeout-minutes: 90
env:
RUSTUP_TOOLCHAIN: 1.89
RUSTUP_AUTO_INSTALL: "0"
steps:
- uses: actions/checkout@v4

- name: Verify preinstalled MSRV Rust
shell: powershell
run: tools/validate-rust-1.89.0.ps1

- name: Cache Cargo
uses: Swatinem/rust-cache@v2

Expand Down
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,3 @@ Multi-crate Rust workspace for the PromptForge pipeline runtime, inference gatew
- Long-running work reports through `shared-progress`. Producers report operation state, hosts forward it, and renderers format it.
- Unsafe code stays in its explicitly owned boundary. Every unsafe block documents its safety invariants immediately before the block.
- Comments explain a non-obvious constraint, ordering requirement, or workaround. Every platform or external-bug workaround cites its upstream issue URL in the explanatory comment.
- CI lints with a newer stable toolchain than the pinned local MSRV (`rust-toolchain.toml`). Lints introduced or strengthened after the pin, such as unused glob imports, may fail only on CI. Check with the newer toolchain before pushing when a change touches imports or doc comments; `cargo doc` runs with warnings denied on CI.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ default-members = ["crates/gateway"]
[workspace.package]
version = "0.3.0"
edition = "2024"
rust-version = "1.89"
license = "BSL-1.0"
repository = "https://github.com/cppalliance/promptforge"

[workspace.dependencies]
base64 = "0.22"
bytes = "1"
promptforge = { path = "crates/promptforge", version = "0.3.0" }
promptforge-core = { path = "crates/promptforge-core", version = "0.3.0" }
promptforge-core-support = { path = "crates/promptforge-core-support", version = "0.3.0" }
Expand Down
7 changes: 1 addition & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
[![CI](https://github.com/cppalliance/promptforge/actions/workflows/ci.yml/badge.svg)](https://github.com/cppalliance/promptforge/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/license-BSL--1.0-blue.svg)](LICENSE)
[![Rust](https://img.shields.io/badge/rust-1.89%2B-orange.svg)](https://www.rust-lang.org/)

# PromptForge

Expand Down Expand Up @@ -65,7 +64,7 @@ Lua sets up the turn. The prose before a Lua block is that block's lazy `prose`

## Build from source

Every build needs Rust 1.89 or later and Node.js 22. The two web UIs are bundled with esbuild during the Cargo build, so run `npm ci` once in each `ui/` folder after cloning:
Every build needs Rust and Node.js 22. The two web UIs are bundled with esbuild during the Cargo build, so run `npm ci` once in each `ui/` folder after cloning:

```bash
git clone git@github.com:cppalliance/promptforge.git
Expand Down Expand Up @@ -109,10 +108,6 @@ Build the guide locally with `mdbook build guide`.

![Filing cabinets](images/banner-06.png)

## Minimum Rust Version

Rust 1.89 or later.

## Contributing

Build, format, and test before you open a PR. CI runs `cargo fmt --check`, `clippy -D warnings`, and `cargo test --workspace`.
Expand Down
1 change: 0 additions & 1 deletion crates/build-llama-cuda/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
name = "build-llama-cuda"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish = false
Expand Down
1 change: 0 additions & 1 deletion crates/build-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
name = "build-ui"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish = false
Expand Down
1 change: 0 additions & 1 deletion crates/build-workshop/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
name = "build-workshop"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish = false
Expand Down
1 change: 0 additions & 1 deletion crates/gateway-config-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
name = "gateway-config-ui"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@ function capabilities(): SettingDef[] {
default: false,
visibleWhen: (ctx) => ctx.value("thinking") !== "never" && ctx.value("thinking") != null,
},
{
key: "voices",
label: "Voices",
help: "The voices the model offers for speech synthesis.",
section: "capabilities",
type: "chips",
default: [],
visibleWhen: (ctx) => ctx.value("kind") === "speech",
},
];
}

Expand Down
17 changes: 16 additions & 1 deletion crates/gateway-config-ui/ui/src/services/config-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ function sameValue(a: unknown, b: unknown): boolean {
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
}

/**
* Drops fields the server accepts on one model kind only. A kind switch
* hides the voices control without clearing its value, and the server
* rejects a non-speech model carrying `voices`, so the payload must not
* keep the stale list.
*/
function stripKindBoundFields(data: EntryData): void {
if (data["kind"] !== "speech") {
delete data["voices"];
}
}

/** A config document's `active_profile` pointer, when it carries one. */
function profilePointer(config: EntryData): string | null {
const pointer = config["active_profile"];
Expand Down Expand Up @@ -700,7 +712,9 @@ export class ConfigStore {
const array = modelArray(entry.kind);
let items = this.entriesOf(payload, array);
if (entry.draft) {
items.push(structuredClone(entry.data));
const data = structuredClone(entry.data);
stripKindBoundFields(data);
items.push(data);
payload[array] = items;
return payload;
}
Expand All @@ -718,6 +732,7 @@ export class ConfigStore {
for (const [key, value] of edits ?? []) {
writePath(target, key, value);
}
stripKindBoundFields(target);
const renamed = String(target["name"] ?? entry.name);
if (renamed !== entry.name) {
for (const profile of this.entriesOf(payload, "profile")) {
Expand Down
73 changes: 71 additions & 2 deletions crates/gateway-config-ui/ui/src/views/model-detail.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ test("the local detail pane renders the registry sections with the model's value
const kindSelect = root.querySelector(".field-row[data-key='kind'] .select");
assert.deepEqual(
dropdownValues(root, "kind"),
["chat", "embedding", "classifier"],
"the header kind dropdown offers the three model kinds",
["chat", "embedding", "classifier", "speech"],
"the header kind dropdown offers the four model kinds",
);
assert.equal(kindSelect.value, "chat");

Expand Down Expand Up @@ -442,6 +442,75 @@ test("Save PUTs the edited payload with untouched secrets redacted, then the pen
);
});

test("picking the speech kind reveals the voices chips and Save PUTs them", async () => {
const stub = fixtureStub();
const { dom, root } = await bootApp({ key: "k", stub });
navigate(dom, "#/remote/gpt-remote");
await settle();

assert.equal(
root.querySelector(".field-row[data-key='voices']"),
null,
"a chat model hides the voices field",
);

dropdownValues(root, "kind");
root.querySelector(".field-row[data-key='kind'] [data-value='speech']").click();
await settle();

const voicesRow = root.querySelector(".field-row[data-key='voices']");
assert.ok(voicesRow, "the speech kind reveals the voices field");
const chips = voicesRow.querySelector(".chip-input input");
chips.value = "nova";
chips.dispatchEvent(new dom.window.KeyboardEvent("keydown", { key: "Enter" }));
await settle();

root.querySelector(".detail-save").click();
await settle();

const put = stub.calls.find(
(call) => call.url.endsWith("/admin/config") && call.init.method === "PUT",
);
assert.ok(put, "Save PUTs /admin/config");
const model = JSON.parse(put.init.body).model.find((entry) => entry.name === "gpt-remote");
assert.equal(model.kind, "speech", "the picked kind carries into the payload");
assert.deepEqual(model.voices, ["nova"], "the added chip carries into the payload");
});

test("switching back from speech hides the voices chips and Save drops them", async () => {
const stub = fixtureStub();
const { dom, root } = await bootApp({ key: "k", stub });
navigate(dom, "#/remote/gpt-remote");
await settle();

dropdownValues(root, "kind");
root.querySelector(".field-row[data-key='kind'] [data-value='speech']").click();
await settle();
const chips = root.querySelector(".field-row[data-key='voices'] .chip-input input");
chips.value = "nova";
chips.dispatchEvent(new dom.window.KeyboardEvent("keydown", { key: "Enter" }));
await settle();

root.querySelector(".field-row[data-key='kind'] [data-value='chat']").click();
await settle();
assert.equal(
root.querySelector(".field-row[data-key='voices']"),
null,
"switching back to chat hides the voices field",
);

root.querySelector(".detail-save").click();
await settle();

const put = stub.calls.find(
(call) => call.url.endsWith("/admin/config") && call.init.method === "PUT",
);
assert.ok(put, "Save PUTs /admin/config");
const model = JSON.parse(put.init.body).model.find((entry) => entry.name === "gpt-remote");
assert.equal(model.kind, "chat", "the reverted kind carries into the payload");
assert.equal("voices" in model, false, "the hidden voices stay out of the payload");
});

test("deleting a model confirms, PUTs the config without it, and returns to the list", async () => {
const stub = fixtureStub();
const { dom, root } = await bootApp({ key: "k", stub });
Expand Down
2 changes: 1 addition & 1 deletion crates/gateway-config-ui/ui/src/views/models-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ export function createModelsView(deps: ModelsViewDeps): ModelsView {
help: "The workload this model serves.",
section: "header",
type: "dropdown",
options: ["chat", "embedding", "classifier"],
options: ["chat", "embedding", "classifier", "speech"],
default: "chat",
}),
);
Expand Down
1 change: 0 additions & 1 deletion crates/gateway-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
name = "gateway-config"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish = false
Expand Down
Loading
Loading