diff --git a/core/bifrost.go b/core/bifrost.go index 259b0cabea9..6dbac1b508a 100644 --- a/core/bifrost.go +++ b/core/bifrost.go @@ -6176,6 +6176,10 @@ func executeRequestWithRetries[T any]( } resp.PopulateExtraFields(requestType, providerKey, model, resolvedModelUsed) tracer.PopulateLLMResponseAttributes(ctx, handle, resp, bifrostError) + } else if bifrostError != nil { + // Failed stream requests carry a chan result and miss the cast above; + // stamp error attributes explicitly so spans don't report unknown. + tracer.PopulateLLMResponseAttributes(ctx, handle, nil, bifrostError) } // End span with appropriate status diff --git a/core/providers/utils/utils.go b/core/providers/utils/utils.go index c999cdf61dc..1ada81440f8 100644 --- a/core/providers/utils/utils.go +++ b/core/providers/utils/utils.go @@ -3398,6 +3398,9 @@ func completeDeferredSpan(ctx *schemas.BifrostContext, result *schemas.BifrostRe } else if result != nil { // Fall back to final chunk if no accumulated data (shouldn't happen normally) tracer.PopulateLLMResponseAttributes(ctx, handle, result, err) + } else if err != nil { + // Stream failed before the first chunk — still stamp error attributes. + tracer.PopulateLLMResponseAttributes(ctx, handle, nil, err) } // Finalize aggregated post-hook spans before ending the LLM span diff --git a/ui/lib/utils/secretVarForm.ts b/ui/lib/utils/secretVarForm.ts index 296bf535969..abab8616ced 100644 --- a/ui/lib/utils/secretVarForm.ts +++ b/ui/lib/utils/secretVarForm.ts @@ -9,6 +9,12 @@ function inferType(ref: string | undefined): SecretVar["type"] | undefined { export const emptySecretVar = (): SecretVar => ({ value: "", ref: "" }); +// trimSecretVar strips stray whitespace from a SecretVar form value's literal value and reference. +export const trimSecretVar = (field: T): T => { + if (!field) return field; + return { ...field, value: field.value?.trim(), ref: field.ref?.trim() }; +}; + export const toSecretVarFormValue = (field?: SecretVar | string): SecretVar => { if (!field) return emptySecretVar(); if (typeof field === "string") { diff --git a/ui/lib/utils/strings.test.ts b/ui/lib/utils/strings.test.ts index 22413078c44..5fa80b23531 100644 --- a/ui/lib/utils/strings.test.ts +++ b/ui/lib/utils/strings.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "vitest"; -import { cleanNumericInput } from "./strings"; +import { cleanNumericInput, trimFields } from "./strings"; // Simulate what onChange does: clean → Number() function simulateOnChange(raw: string): { display: string; value: number | undefined } { @@ -201,4 +201,31 @@ describe("simulateOnBlur (normalize display)", () => { test("1000 → 1000", () => { expect(simulateOnBlur("1000")).toEqual({ display: "1000", value: 1000 }); }); -}); \ No newline at end of file +}); +describe("trimFields", () => { + test("trims string fields in place", () => { + const obj = { topic: " my-topic ", project: "\tproj\n" }; + trimFields(obj, "topic", "project"); + expect(obj).toEqual({ topic: "my-topic", project: "proj" }); + }); + test("trims string array fields", () => { + const obj = { brokers: [" a:9092", "b:9092 ", " c:9092 "] }; + trimFields(obj, "brokers"); + expect(obj.brokers).toEqual(["a:9092", "b:9092", "c:9092"]); + }); + test("leaves undefined fields untouched", () => { + const obj: { name: string; ml_app?: string } = { name: " x " }; + trimFields(obj, "name", "ml_app"); + expect(obj).toEqual({ name: "x" }); + expect("ml_app" in obj).toBe(false); + }); + test("only touches the named fields", () => { + const obj = { a: " x ", b: " y " }; + trimFields(obj, "a"); + expect(obj).toEqual({ a: "x", b: " y " }); + }); + test("returns the same object", () => { + const obj = { a: " x " }; + expect(trimFields(obj, "a")).toBe(obj); + }); +}); diff --git a/ui/lib/utils/strings.ts b/ui/lib/utils/strings.ts index 0679bb1d694..61e780bfe66 100644 --- a/ui/lib/utils/strings.ts +++ b/ui/lib/utils/strings.ts @@ -2,6 +2,22 @@ export function capitalize(name: string) { return name.charAt(0).toUpperCase() + name.slice(1); } +export type TrimmableKeys = { [K in keyof T]: T[K] extends string | string[] | undefined ? K : never }[keyof T]; + +// trimFields trims whitespace from the named string (or string[]) fields of obj, in place. +// Undefined fields are left untouched. +export function trimFields(obj: T, ...keys: TrimmableKeys[]): T { + for (const key of keys) { + const value = obj[key]; + if (typeof value === "string") { + obj[key] = value.trim() as T[typeof key]; + } else if (Array.isArray(value)) { + obj[key] = value.map((item) => (typeof item === "string" ? item.trim() : item)) as T[typeof key]; + } + } + return obj; +} + // Cleans raw input into a valid numeric string: // - Single non-alphabetic separator between digits (commas, spaces, underscores) → stripped // - Alphabetic characters → stop processing