Skip to content

feat: add project_group_name to projects.create() - #835

Merged
Luca Forstner (lforst) merged 2 commits into
mainfrom
feat/project-group-name
Oct 1, 2026
Merged

Luca Forstner (lforst) merged 2 commits into
mainfrom
feat/project-group-name

Conversation

@elijah0528

@elijah0528 Elijah Kurien (elijah0528) commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Ref: braintrustdata/braintrust-sdk-javascript#2550

Python port of the JS change.
projects.create() now takes an optional project_group_name.
When braintrust push or project.publish() registers a project that doesn't exist yet, it gets created inside that group.
This lets people who only have project-creation permission on a group (not the whole org) register projects.

project = braintrust.projects.create("my-project", project_group_name="my-group")

We deliberately don't add this to init_logger, init, init_dataset or Eval.
Loggers resolve their project on every startup, and passing a group turns that cheap lookup into an uncached "create or verify group membership" call that locks the group's row on the backend.
Every logger cold start in that group would wait on the same lock, and logging would start failing if the project ever gets moved out of the group.
Creating a project is a one-time setup step, so it belongs on the push/publish path.
(An earlier version of this PR added the option to those entrypoints, mirroring JS #2541. The JS SDK has since moved it in #2550, and this PR now follows that.)

ProjectIdCache.get() passes project.project_group_name to get_by_name(), which only adds project_group_name to the POST /v1/project body when it's set, so requests without a group are unchanged.
Evaluators pushed via braintrust push still register their project without a group, same as in JS.

One difference to JS: the JS SDK registers projects through api/project/register, while the Python SDK uses the generated /v1/project resource since #705.
Both are backed by createProjectSchema, so I think the field should work on both.
If the API change turns out to only cover api/project/register, this needs to switch endpoints.
The generated CreateProject type doesn't have the field yet, hence the cast.

Added tests in test_framework2.py for the field being sent and being omitted when unset.
test_framework2.py, test_logger.py, test_framework.py, the cli/ tests and nox -s pylint pass.

🤖 Generated with Claude Code

Co-authored by StarfolkAI (@starfolkai)[bot]

Python port of braintrustdata/braintrust-sdk-javascript#2541.

`init_logger`, `init`, `init_dataset`, and `Eval`/`EvalAsync` now accept
`project_group_name`. When the named project does not exist yet, it is created
inside that project group, which lets callers who only hold project-creation
permission on a group (rather than on the whole organization) register projects.

The option is only honored when the project is looked up by name -- it is
ignored when `project_id` is supplied -- and it is omitted from the wire payload
entirely when unset, so existing callers see no change in behavior or requests.

Notes on the port:

- The JS SDK registers projects through `api/project/register`; the Python SDK
  was migrated to the generated `POST /v1/project` resource in #705. Both are
  backed by the same `createProjectSchema`, which the API half of this change
  extends, so the new field is threaded into that body via a shared
  `_register_project` helper.
- `init`/`init_dataset` need no extra round trip here. Unlike the JS versions,
  they already resolve the project via project registration and then register
  the experiment/dataset by `project_id`.
- The JS change also keys its project-metadata LRU cache on the group name.
  Python's `_compute_logger_metadata` has no such cache, so there is nothing to
  key.
- New parameters are appended to each signature rather than inserted next to
  `project_id`, so existing positional callers are unaffected.

`test_project_group.py` drives the real entrypoints against a local HTTP server
(`api/_test_server.scripted_server`) and asserts on the request bodies the SDK
actually sends, including that the field is absent when unset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T00:40:54.353100Z f11743a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f11743afda

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment thread py/src/braintrust/logger.py Outdated
if project_group_name is not None:
# Only present when set, so exported span components (and therefore anything that resolves
# them with an older SDK) are byte-for-byte unchanged for callers not using project groups.
compute_metadata_args["project_group_name"] = project_group_name

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the project group during W3C trace propagation

When a logger initialized with project_group_name is still unresolved, its spans retain this dictionary, but Span.inject() routes through _get_parent_info() / _get_otel_parent(), which serializes only project_name:<name>. A receiving service then reconstructs metadata containing only project_name and may register the project outside the requested group—or fail for the group-scoped users this feature is intended to support. Include the group in the propagation representation or resolve the project ID before emitting the baggage value.

Useful? React with 👍 / 👎.

Comment thread py/src/braintrust/logger.py Outdated
project_info = state.api_client().projects.post_project(
body={"name": project or GLOBAL_PROJECT, "org_name": state.org_name}
)
project_info = _register_project(state, project or GLOBAL_PROJECT, project_group_name=project_group_name)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would probably inline this - cruft otherwise

@lforst Luca Forstner (lforst) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reservations about project groups being in the SDK but unblocking.

@lforst
Luca Forstner (lforst) dismissed their stale review October 1, 2026 15:49

questionable api

@lforst Luca Forstner (lforst) changed the title feat: add project_group_name to create projects inside a project group feat: add project_group_name to projects.create() Oct 1, 2026
@lforst
Luca Forstner (lforst) merged commit 4033ca8 into main Oct 1, 2026
154 of 165 checks passed
@lforst
Luca Forstner (lforst) deleted the feat/project-group-name branch October 1, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants