From 6ac6549a0aed2c69cc31eff17abdc1d690a585a6 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:15:06 -0500 Subject: [PATCH] fix(ai): classify failed Google Interactions by reported errors (#58771) --- .../ai/src/protocols/google-interactions.ts | 12 +++++-- .../google-vertex-interactions.test.ts | 31 +++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/ai/src/protocols/google-interactions.ts b/packages/ai/src/protocols/google-interactions.ts index 896e1d183712..a04083fd0783 100644 --- a/packages/ai/src/protocols/google-interactions.ts +++ b/packages/ai/src/protocols/google-interactions.ts @@ -155,6 +155,9 @@ const Interaction = Schema.StructWithRest( id: Schema.optional(Schema.String), status: Schema.String, usage: Schema.optional(RawUsage), + errors: Schema.optional( + Schema.Array(Schema.Struct({ code: Schema.optional(Schema.String), message: Schema.optional(Schema.String) })), + ), }), [Schema.Record(Schema.String, Schema.Unknown)], ) @@ -521,14 +524,17 @@ const step = Effect.fnUntraced(function* (state: ParserState, event: Event) { }) case "interaction.completed": { const interaction = event.interaction - if (interaction.status === "failed" || interaction.status === "cancelled") + if (interaction.status === "failed" || interaction.status === "cancelled") { + const messages = (interaction.errors ?? []).flatMap((error) => (error.message?.trim() ? [error.message] : [])) return yield* new AIError({ reason: classifyProviderFailure({ - message: `Google Interactions ${interaction.status}`, - data: interaction, + message: messages.length > 0 ? messages.join("\n") : `Google Interactions ${interaction.status}`, + // The first coded error decides the classification; every message stays visible. + data: interaction.errors?.find((error) => error.code) ?? interaction, rawBody: encodeJson(event), }), }) + } if (!["completed", "requires_action", "incomplete"].includes(interaction.status)) return yield* ProviderShared.eventError( ADAPTER, diff --git a/packages/ai/test/provider/google-vertex-interactions.test.ts b/packages/ai/test/provider/google-vertex-interactions.test.ts index e9b80d2e10bf..68e2677b41fd 100644 --- a/packages/ai/test/provider/google-vertex-interactions.test.ts +++ b/packages/ai/test/provider/google-vertex-interactions.test.ts @@ -187,6 +187,37 @@ describe("Google Vertex Interactions", () => { }), ) + it.effect("classifies failed interactions by their reported errors", () => + Effect.gen(function* () { + const model = GoogleVertexInteractions.configure({ apiKey: "vertex-key" }).model("gemini-3.8-flash") + const cases = [ + { + errors: [{ code: "permission_denied", message: "Access denied" }], + tag: "Authentication", + message: "Access denied", + }, + { + errors: [{ message: "Out of credits" }, { code: "insufficient_quota", message: "Add billing" }], + tag: "QuotaExceeded", + message: "Out of credits\nAdd billing", + }, + { errors: [], tag: "UnknownProvider", message: "Google Interactions failed" }, + ] + for (const item of cases) { + const event = { event_type: "interaction.completed", interaction: { status: "failed", errors: item.errors } } + const error = yield* LLMClient.generate(LLM.request({ model, prompt: "Hi" })).pipe( + Effect.provide(fixedResponse(sseEvents(event))), + Effect.flip, + ) + expect([error.reason._tag, error.reason.message, error.reason.body]).toEqual([ + item.tag, + item.message, + JSON.stringify(event), + ]) + } + }), + ) + it.effect("round-trips tool signatures under Vertex metadata without enabling storage", () => Effect.gen(function* () { const model = GoogleVertexInteractions.configure({