Skip to content

Commit 7d73bf3

Browse files
committed
feat(functions push): add --manifest path for non-SDK function types
The runner-based push path can only ship function types the Braintrust SDK exposes builders for: tools, scorers, prompts, parameters. Topics- pipeline types (facet, classifier) and inline-quickjs preprocessors have no SDK builder, so AI-agent workflows that author lenses cannot push them through `bt`. `--manifest <path>` accepts a JSON object or array of objects matching the wire shape `/insert-functions` already accepts (the same endpoint `bt topics config enable` uses to create classifier functions). The manifest path bypasses the runner entirely; auth, project resolution, and `--if-exists` flow through the existing helpers. Mutually exclusive with `--file`, positional file paths, `--runner`, `--language`, `--requirements`, `--tsconfig`, and `--external-packages`, since none of those apply when the SDK runner is skipped. Closes #149.
1 parent fd5aed7 commit 7d73bf3

6 files changed

Lines changed: 540 additions & 2 deletions

File tree

src/functions/mod.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,18 @@ pub(crate) struct PushArgs {
270270
)]
271271
pub file_flag: Vec<PathBuf>,
272272

273+
/// JSON manifest file with one or more raw function definitions.
274+
/// Skips the SDK runner; the manifest is posted directly to /insert-functions.
275+
/// Use this for function types the SDK doesn't expose builders for
276+
/// (preprocessor with inline-quickjs runtime, facet, classifier).
277+
#[arg(
278+
long = "manifest",
279+
env = "BT_FUNCTIONS_PUSH_MANIFEST",
280+
value_name = "PATH",
281+
conflicts_with_all = ["files", "file_flag", "runner", "language", "requirements", "tsconfig", "external_packages"]
282+
)]
283+
pub manifest: Option<PathBuf>,
284+
273285
/// Behavior when a function with the same slug already exists.
274286
#[arg(
275287
long = "if-exists",
@@ -687,6 +699,35 @@ mod tests {
687699
assert!(msg.contains("--type"));
688700
}
689701

702+
#[test]
703+
fn push_manifest_flag_parses() {
704+
let _guard = test_lock();
705+
let parsed =
706+
parse(&["functions", "push", "--manifest", "fn.json"]).expect("parse push manifest");
707+
let FunctionsCommands::Push(push) = parsed.command.expect("subcommand") else {
708+
panic!("expected push command");
709+
};
710+
assert_eq!(
711+
push.manifest,
712+
Some(std::path::PathBuf::from("fn.json")),
713+
"manifest path should be parsed"
714+
);
715+
assert!(push.files.is_empty());
716+
assert!(push.file_flag.is_empty());
717+
}
718+
719+
#[test]
720+
fn push_manifest_conflicts_with_file_path() {
721+
let _guard = test_lock();
722+
let err = parse(&["functions", "push", "--manifest", "fn.json", "extra.ts"])
723+
.expect_err("manifest+file path should conflict");
724+
let msg = err.to_string();
725+
assert!(
726+
msg.contains("manifest") || msg.contains("--manifest") || msg.contains("conflict"),
727+
"expected conflict-related error, got: {msg}"
728+
);
729+
}
730+
690731
#[test]
691732
fn top_level_type_flag_still_parses_for_functions_namespace() {
692733
let _guard = test_lock();

0 commit comments

Comments
 (0)