Skip to content
Merged
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
4 changes: 4 additions & 0 deletions core/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions core/providers/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions ui/lib/utils/secretVarForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <T extends { value?: string; ref?: string } | undefined>(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") {
Expand Down
31 changes: 29 additions & 2 deletions ui/lib/utils/strings.test.ts
Original file line number Diff line number Diff line change
@@ -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 } {
Expand Down Expand Up @@ -201,4 +201,31 @@ describe("simulateOnBlur (normalize display)", () => {
test("1000 → 1000", () => {
expect(simulateOnBlur("1000")).toEqual({ display: "1000", value: 1000 });
});
});
});
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);
});
});
16 changes: 16 additions & 0 deletions ui/lib/utils/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ export function capitalize(name: string) {
return name.charAt(0).toUpperCase() + name.slice(1);
}

export type TrimmableKeys<T> = { [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<T extends object>(obj: T, ...keys: TrimmableKeys<T>[]): 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
Expand Down
Loading