From 6997522171c478ebe36b25219e057eb9ec7e3128 Mon Sep 17 00:00:00 2001 From: Joshua Wootonn Date: Wed, 18 Mar 2026 14:55:32 -0500 Subject: [PATCH 1/5] Prefer version over environment in loaders --- js/src/logger.test.ts | 165 ++++++++++++++++++++++++++++++++++++++++++ js/src/logger.ts | 47 ++++++------ 2 files changed, 188 insertions(+), 24 deletions(-) diff --git a/js/src/logger.test.ts b/js/src/logger.test.ts index 4fda54eca..64980ced2 100644 --- a/js/src/logger.test.ts +++ b/js/src/logger.test.ts @@ -10,6 +10,8 @@ import { initLogger, Prompt, BraintrustState, + loadPrompt, + loadParameters, wrapTraced, currentSpan, withParent, @@ -464,6 +466,169 @@ test("init accepts dataset with id and version", () => { expect(datasetWithVersion.version).toBe("v2"); }); +describe("loader version precedence", () => { + let state: BraintrustState; + let getJson: ReturnType; + const promptRow = { + id: "11111111-1111-4111-8111-111111111111", + _xact_id: "v1", + project_id: "22222222-2222-4222-8222-222222222222", + log_id: "p", + org_id: "33333333-3333-4333-8333-333333333333", + name: "Saved prompt", + slug: "saved-prompt", + description: null, + tags: null, + prompt_data: { + prompt: { + type: "chat", + messages: [{ role: "user", content: "Hello" }], + }, + options: { model: "gpt-5-mini" }, + }, + } satisfies { + id: string; + _xact_id: string; + project_id: string; + log_id: "p"; + org_id: string; + name: string; + slug: string; + description: null; + tags: null; + prompt_data: { + prompt: { + type: "chat"; + messages: Array<{ role: "user"; content: string }>; + }; + options: { model: string }; + }; + }; + const parametersRow = { + id: "44444444-4444-4444-8444-444444444444", + _xact_id: "v1", + project_id: "55555555-5555-4555-8555-555555555555", + name: "Saved parameters", + slug: "saved-parameters", + description: null, + function_type: "parameters", + function_data: { + type: "parameters", + data: { prefix: "hello" }, + __schema: { + type: "object", + properties: { + prefix: { type: "string", default: "hello" }, + }, + additionalProperties: true, + }, + }, + } satisfies { + id: string; + _xact_id: string; + project_id: string; + name: string; + slug: string; + description: null; + function_type: "parameters"; + function_data: { + type: "parameters"; + data: { prefix: string }; + __schema: { + type: string; + properties: { prefix: { type: string; default: string } }; + additionalProperties: boolean; + }; + }; + }; + + beforeEach(async () => { + state = await _exportsForTestingOnly.simulateLoginForTests(); + vi.spyOn(state, "login").mockResolvedValue(state); + getJson = vi.spyOn(state.apiConn(), "get_json"); + }); + + afterEach(() => { + _exportsForTestingOnly.simulateLogoutForTests(); + vi.restoreAllMocks(); + }); + + test("loadPrompt prefers version over environment for project lookup", async () => { + getJson.mockResolvedValue({ objects: [promptRow] }); + + await loadPrompt({ + projectName: "test-project", + slug: "saved-prompt", + version: "v1", + environment: "production", + state, + }); + + expect(getJson).toHaveBeenCalledWith( + "v1/prompt", + expect.objectContaining({ + project_name: "test-project", + slug: "saved-prompt", + version: "v1", + }), + ); + expect(getJson.mock.calls[0][1]).not.toHaveProperty("environment"); + }); + + test("loadPrompt prefers version over environment for id lookup", async () => { + getJson.mockResolvedValue(promptRow); + + await loadPrompt({ + id: promptRow.id, + version: "v1", + environment: "production", + state, + }); + + expect(getJson).toHaveBeenCalledWith(`v1/prompt/${promptRow.id}`, { + version: "v1", + }); + }); + + test("loadParameters prefers version over environment for project lookup", async () => { + getJson.mockResolvedValue({ objects: [parametersRow] }); + + await loadParameters({ + projectName: "test-project", + slug: "saved-parameters", + version: "v1", + environment: "production", + state, + }); + + expect(getJson).toHaveBeenCalledWith( + "v1/function", + expect.objectContaining({ + project_name: "test-project", + slug: "saved-parameters", + version: "v1", + function_type: "parameters", + }), + ); + expect(getJson.mock.calls[0][1]).not.toHaveProperty("environment"); + }); + + test("loadParameters prefers version over environment for id lookup", async () => { + getJson.mockResolvedValue(parametersRow); + + await loadParameters({ + id: parametersRow.id, + version: "v1", + environment: "production", + state, + }); + + expect(getJson).toHaveBeenCalledWith(`v1/function/${parametersRow.id}`, { + version: "v1", + }); + }); +}); + describe("prompt.build structured output templating", () => { test("applies nunjucks templating inside schema", () => { const prompt = new Prompt( diff --git a/js/src/logger.ts b/js/src/logger.ts index 7b78fc9ec..4681b9158 100644 --- a/js/src/logger.ts +++ b/js/src/logger.ts @@ -4074,6 +4074,7 @@ export type LoadParametersByIdOptions = LoadParametersBaseOptions & { export type LoadParametersByIdWithEnvOptions = LoadParametersBaseOptions & { id: string; environment: string; + version?: string; }; export type LoadParametersByProjectNameOptions = LoadParametersBaseOptions & { @@ -4087,6 +4088,7 @@ export type LoadParametersByProjectNameWithEnvOptions = projectName: string; slug: string; environment: string; + version?: string; }; export type LoadParametersByProjectIdOptions = LoadParametersBaseOptions & { @@ -4100,6 +4102,7 @@ export type LoadParametersByProjectIdWithEnvOptions = projectId: string; slug: string; environment: string; + version?: string; }; export type LoadParametersOptions = @@ -4127,7 +4130,7 @@ type LoadParametersImplementationOptions = LoadParametersBaseOptions & { * @param options.projectId The id of the project to load the prompt from. This takes precedence over `projectName` if specified. * @param options.slug The slug of the prompt to load. * @param options.version An optional version of the prompt (to read). If not specified, the latest version will be used. - * @param options.environment Fetch the version of the prompt assigned to the specified environment (e.g. "production", "staging"). Cannot be specified at the same time as `version`. + * @param options.environment Fetch the version of the prompt assigned to the specified environment (e.g. "production", "staging"). If both `version` and `environment` are provided, `version` takes precedence. * @param options.id The id of a specific prompt to load. If specified, this takes precedence over all other parameters (project and slug). * @param options.defaults (Optional) A dictionary of default values to use when rendering the prompt. Prompt values will override these defaults. * @param options.noTrace If true, do not include logging metadata for this prompt when build() is called. @@ -4163,11 +4166,11 @@ export async function loadPrompt({ forceLogin, state: stateArg, }: LoadPromptOptions) { - if (version && environment) { - throw new Error( - "Cannot specify both 'version' and 'environment' parameters. Please use only one (remove the other).", - ); - } + const versionOrEnvironment = version + ? { version } + : environment + ? { environment } + : {}; if (id) { // When loading by ID, we don't need project or slug } else if (isEmpty(projectName) && isEmpty(projectId)) { @@ -4188,10 +4191,9 @@ export async function loadPrompt({ }); if (id) { // Load prompt by ID using the /v1/prompt/{id} endpoint - response = await state.apiConn().get_json(`v1/prompt/${id}`, { - ...(version && { version }), - ...(environment && { environment }), - }); + response = await state + .apiConn() + .get_json(`v1/prompt/${id}`, versionOrEnvironment); // Wrap single prompt response in objects array to match list API format if (response) { response = { objects: [response] }; @@ -4201,13 +4203,12 @@ export async function loadPrompt({ project_name: projectName, project_id: projectId, slug, - version, - ...(environment && { environment }), + ...versionOrEnvironment, }); } } catch (e) { // If environment or version was specified, don't fall back to cache - if (environment || version) { + if (versionOrEnvironment) { throw new Error(`Prompt not found with specified parameters: ${e}`); } @@ -4287,7 +4288,7 @@ export async function loadPrompt({ * @param options.projectId The id of the project to load the parameters from. This takes precedence over `projectName` if specified. * @param options.slug The slug of the parameters to load. * @param options.version An optional version of the parameters (to read). If not specified, the latest version will be used. - * @param options.environment Fetch the version of the parameters assigned to the specified environment (e.g. "production", "staging"). Cannot be specified at the same time as `version`. + * @param options.environment Fetch the version of the parameters assigned to the specified environment (e.g. "production", "staging"). If both `version` and `environment` are provided, `version` takes precedence. * @param options.id The id of specific parameters to load. If specified, this takes precedence over all other parameters (project and slug). * @param options.appUrl The URL of the Braintrust App. Defaults to https://www.braintrust.dev. * @param options.apiKey The API key to use. If the parameter is not specified, will try to use the `BRAINTRUST_API_KEY` environment variable. @@ -4340,11 +4341,11 @@ export async function loadParameters< }: LoadParametersImplementationOptions): Promise< RemoteEvalParameters> > { - if (version && environment) { - throw new Error( - "Cannot specify both 'version' and 'environment' parameters. Please use only one (remove the other).", - ); - } + const versionOrEnvironment = version + ? { version } + : environment + ? { environment } + : {}; if (id) { // When loading by ID, we don't need project or slug } else if (isEmpty(projectName) && isEmpty(projectId)) { @@ -4365,8 +4366,7 @@ export async function loadParameters< }); if (id) { response = await state.apiConn().get_json(`v1/function/${id}`, { - ...(version && { version }), - ...(environment && { environment }), + ...versionOrEnvironment, }); if (response) { response = { objects: [response] }; @@ -4376,13 +4376,12 @@ export async function loadParameters< project_name: projectName, project_id: projectId, slug, - version, function_type: "parameters", - ...(environment && { environment }), + ...versionOrEnvironment, }); } } catch (e) { - if (environment || version) { + if (versionOrEnvironment) { throw new Error(`Parameters not found with specified parameters: ${e}`); } From c9cebe8e5193f24fea43ba985d18b49acc03ba84 Mon Sep 17 00:00:00 2001 From: Joshua Wootonn Date: Wed, 18 Mar 2026 15:05:51 -0500 Subject: [PATCH 2/5] Format unformatted touched files from this PR --- js/src/logger.test.ts | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/js/src/logger.test.ts b/js/src/logger.test.ts index 64980ced2..fc4f14f7b 100644 --- a/js/src/logger.test.ts +++ b/js/src/logger.test.ts @@ -1,8 +1,5 @@ /* eslint-disable @typescript-eslint/consistent-type-assertions */ -import type { - ChatCompletionContentPartText, - ChatCompletionContentPartImage, -} from "openai/resources"; + import { vi, expect, test, describe, beforeEach, afterEach } from "vitest"; import { _exportsForTestingOnly, @@ -19,14 +16,9 @@ import { updateSpan, Attachment, deepCopyEvent, - renderMessage, renderMessageImpl, } from "./logger"; -import { - parseTemplateFormat, - isTemplateFormat, - renderTemplateContent, -} from "./template/renderer"; + import { configureNode } from "./node/config"; import { writeFile, unlink } from "node:fs/promises"; import { join } from "node:path"; @@ -35,11 +27,6 @@ import { SpanComponentsV3 } from "../util/span_identifier_v3"; configureNode(); -function getExportVersion(exportedSpan: string): number { - const exportedBytes = base64ToUint8Array(exportedSpan); - return exportedBytes[0]; -} - test("renderMessage with file content parts", () => { const message = { role: "user" as const, From 6ef1062296d6e1e0978245e1fc67fed0a9ba7395 Mon Sep 17 00:00:00 2001 From: Joshua Wootonn Date: Thu, 19 Mar 2026 11:42:25 -0500 Subject: [PATCH 3/5] Fix incorrect thruthy assertion in the catch of the loadPrompt and loadParameters --- js/src/logger.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/logger.ts b/js/src/logger.ts index 4681b9158..0560c52e7 100644 --- a/js/src/logger.ts +++ b/js/src/logger.ts @@ -4208,7 +4208,7 @@ export async function loadPrompt({ } } catch (e) { // If environment or version was specified, don't fall back to cache - if (versionOrEnvironment) { + if (version || environment) { throw new Error(`Prompt not found with specified parameters: ${e}`); } @@ -4381,7 +4381,7 @@ export async function loadParameters< }); } } catch (e) { - if (versionOrEnvironment) { + if (version || environment) { throw new Error(`Parameters not found with specified parameters: ${e}`); } From 5b15125e3dd2dff50815e29b2563edf2b7eaf5ca Mon Sep 17 00:00:00 2001 From: Joshua Wootonn Date: Thu, 19 Mar 2026 12:19:41 -0500 Subject: [PATCH 4/5] Normalize Gemini service tier header in e2e snapshots --- e2e/helpers/normalize.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/helpers/normalize.ts b/e2e/helpers/normalize.ts index 11d7dc8be..9743b436a 100644 --- a/e2e/helpers/normalize.ts +++ b/e2e/helpers/normalize.ts @@ -39,6 +39,7 @@ const DYNAMIC_HEADER_KEYS = new Set([ "openai-project", "server-timing", "set-cookie", + "x-gemini-service-tier", "x-ratelimit-remaining-requests", "x-ratelimit-remaining-tokens", "x-ratelimit-reset-requests", From acc6f5acf76d844fd78cab7b082229077df87d4d Mon Sep 17 00:00:00 2001 From: Joshua Wootonn Date: Thu, 19 Mar 2026 12:26:53 -0500 Subject: [PATCH 5/5] Update Google GenAI e2e snapshots --- .../__snapshots__/log-payloads.json | 3 +++ .../__snapshots__/log-payloads.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/e2e/scenarios/google-genai-auto-instrumentation-node-hook/__snapshots__/log-payloads.json b/e2e/scenarios/google-genai-auto-instrumentation-node-hook/__snapshots__/log-payloads.json index 57f4edd7b..5e7813b51 100644 --- a/e2e/scenarios/google-genai-auto-instrumentation-node-hook/__snapshots__/log-payloads.json +++ b/e2e/scenarios/google-genai-auto-instrumentation-node-hook/__snapshots__/log-payloads.json @@ -168,6 +168,7 @@ "vary": "Origin, X-Origin, Referer", "x-content-type-options": "nosniff", "x-frame-options": "SAMEORIGIN", + "x-gemini-service-tier": "", "x-xss-protection": "0" } }, @@ -359,6 +360,7 @@ "vary": "Origin, X-Origin, Referer", "x-content-type-options": "nosniff", "x-frame-options": "SAMEORIGIN", + "x-gemini-service-tier": "", "x-xss-protection": "0" } }, @@ -887,6 +889,7 @@ "vary": "Origin, X-Origin, Referer", "x-content-type-options": "nosniff", "x-frame-options": "SAMEORIGIN", + "x-gemini-service-tier": "", "x-xss-protection": "0" } }, diff --git a/e2e/scenarios/wrap-google-genai-content-traces/__snapshots__/log-payloads.json b/e2e/scenarios/wrap-google-genai-content-traces/__snapshots__/log-payloads.json index b263c73cf..e712905b6 100644 --- a/e2e/scenarios/wrap-google-genai-content-traces/__snapshots__/log-payloads.json +++ b/e2e/scenarios/wrap-google-genai-content-traces/__snapshots__/log-payloads.json @@ -169,6 +169,7 @@ "vary": "Origin, X-Origin, Referer", "x-content-type-options": "nosniff", "x-frame-options": "SAMEORIGIN", + "x-gemini-service-tier": "", "x-xss-protection": "0" } }, @@ -372,6 +373,7 @@ "vary": "Origin, X-Origin, Referer", "x-content-type-options": "nosniff", "x-frame-options": "SAMEORIGIN", + "x-gemini-service-tier": "", "x-xss-protection": "0" } }, @@ -914,6 +916,7 @@ "vary": "Origin, X-Origin, Referer", "x-content-type-options": "nosniff", "x-frame-options": "SAMEORIGIN", + "x-gemini-service-tier": "", "x-xss-protection": "0" } },