diff --git a/packages/ai/src/route/executor.ts b/packages/ai/src/route/executor.ts index 388fd4198ef..6786b9f2a22 100644 --- a/packages/ai/src/route/executor.ts +++ b/packages/ai/src/route/executor.ts @@ -1,4 +1,4 @@ -import { Cause, Context, Effect, Layer, Option, Schema } from "effect" +import { Cause, Context, Effect, Layer, Option, Schema, Stream } from "effect" import { FetchHttpClient, Headers, @@ -297,44 +297,88 @@ export const classifyHttpFailure = (input: { }) } -const toHttpError = (redactedNames: ReadonlyArray) => (error: unknown) => { - const transportError = (input: { - readonly message: string - readonly kind?: string | undefined - readonly request?: HttpClientRequest.HttpClientRequest | undefined - }) => +type HttpOperation = "request" | "read" + +const NativeTransportFailure = Schema.Struct({ + message: Schema.String, + code: Schema.optionalKey(Schema.String), + cause: Schema.optionalKey(Schema.Unknown), +}) +const decodeNativeTransportFailure = Schema.decodeUnknownOption(NativeTransportFailure) + +const nativeTransportFailure = (error: unknown) => { + const failure = Option.getOrUndefined(decodeNativeTransportFailure(error)) + if (!failure) return undefined + if (failure.code !== undefined) return failure + const cause = Option.getOrUndefined(decodeNativeTransportFailure(failure.cause)) + if (cause?.code !== undefined) return cause + return failure +} + +const httpError = (input: { + readonly error: unknown + readonly request: HttpClientRequest.HttpClientRequest + readonly operation: HttpOperation + readonly redactedNames: ReadonlyArray +}) => { + const request = HttpClientError.isHttpClientError(input.error) ? input.error.request : input.request + const transportError = (failure: { readonly message: string; readonly code?: string | undefined }) => new AIError({ module: "RequestExecutor", - method: "execute", + method: input.operation, reason: new TransportReason({ - message: input.message, - kind: input.kind, - url: input.request ? redactUrl(input.request.url) : undefined, - http: input.request ? new HttpContext({ request: requestDetails(input.request, redactedNames) }) : undefined, + message: failure.message, + transport: "http", + operation: input.operation, + code: failure.code, + url: redactUrl(request.url), + http: new HttpContext({ request: requestDetails(request, input.redactedNames) }), }), }) - if (Cause.isTimeoutError(error)) { - return transportError({ message: error.message, kind: "Timeout" }) - } - if (!HttpClientError.isHttpClientError(error)) { - return transportError({ message: error instanceof Error ? error.message : "HTTP transport failed" }) - } - const request = "request" in error ? error.request : undefined - if (error.reason._tag === "TransportError") { + const source = + HttpClientError.isHttpClientError(input.error) && "cause" in input.error.reason + ? input.error.reason.cause + : input.error + const native = nativeTransportFailure(source) + const code = native?.code + const raw = native?.message ?? (input.error instanceof Error ? input.error.message : undefined) + const detail = raw ? redactBody(raw, secretValues(request)) : undefined + const message = code && detail && !detail.includes(code) ? `${code}: ${detail}` : detail + + if (Cause.isTimeoutError(input.error) || Cause.isTimeoutError(source)) + return transportError({ message: message ?? "HTTP transport timed out", code: code ?? "Timeout" }) + if (!HttpClientError.isHttpClientError(input.error)) + return transportError({ message: message ?? "HTTP transport failed", code }) + if (input.error.reason._tag === "TransportError") { return transportError({ - message: error.reason.description ?? "HTTP transport failed", - kind: error.reason._tag, - request, + message: message ?? input.error.reason.description ?? "HTTP transport failed", + code: code ?? input.error.reason._tag, }) } return transportError({ - message: `HTTP transport failed: ${error.reason._tag}`, - kind: error.reason._tag, - request, + message: message ?? `HTTP transport failed: ${input.error.reason._tag}`, + code: code ?? input.error.reason._tag, }) } +export const stream = ( + executor: Interface, + request: HttpClientRequest.HttpClientRequest, + middleware?: HttpMiddleware, +): Stream.Stream => + Stream.unwrap( + Effect.gen(function* () { + const redactedNames = yield* Headers.CurrentRedactedNames + const response = yield* executor.execute(request, middleware) + return response.stream.pipe( + Stream.mapError((error) => + httpError({ error, request: response.request, operation: "read", redactedNames }), + ), + ) + }), + ) + export const layer: Layer.Layer = Layer.effect( Service, Effect.gen(function* () { @@ -343,15 +387,16 @@ export const layer: Layer.Layer = Layer.e Effect.gen(function* () { const redactedNames = yield* Headers.CurrentRedactedNames if (!middleware) - return yield* http - .execute(request) - .pipe(Effect.mapError(toHttpError(redactedNames)), Effect.flatMap(statusError(request, redactedNames))) + return yield* http.execute(request).pipe( + Effect.mapError((error) => httpError({ error, request, operation: "request", redactedNames })), + Effect.flatMap(statusError(request, redactedNames)), + ) const response = yield* middleware(request, (input) => http .execute(input) .pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause))))), - ).pipe(Effect.mapError(toHttpError(redactedNames))) + ).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request", redactedNames }))) return yield* statusError(response.request, redactedNames)(response) }) return Service.of({ diff --git a/packages/ai/src/route/transport/http.ts b/packages/ai/src/route/transport/http.ts index 12482f39188..06cb2fa48cd 100644 --- a/packages/ai/src/route/transport/http.ts +++ b/packages/ai/src/route/transport/http.ts @@ -1,4 +1,4 @@ -import { Effect, Stream } from "effect" +import { Effect } from "effect" import { Headers, HttpClientRequest } from "effect/unstable/http" import { Auth } from "../auth.js" import { render as renderEndpoint } from "../endpoint.js" @@ -6,6 +6,7 @@ import { Framing } from "../framing.js" import type { HttpMiddleware, Transport, TransportPrepareInput } from "./index.js" import * as ProviderShared from "../../protocols/shared.js" import { mergeJsonRecords, type LLMRequest } from "../../schema/index.js" +import { RequestExecutor } from "../executor.js" export type JsonRequestInput = TransportPrepareInput @@ -86,26 +87,8 @@ export const httpJson = (input: HttpJsonInput): HttpJs middleware: prepareInput.middleware, } }), - frames: (prepared, request, runtime) => - Stream.unwrap( - runtime.http - .execute(prepared.request, prepared.middleware) - .pipe( - Effect.map((response) => - prepared.framing.frame( - response.stream.pipe( - Stream.mapError((error) => - ProviderShared.eventError( - `${request.model.provider}/${request.model.route.id}`, - `Failed to read ${request.model.provider}/${request.model.route.id} stream`, - ProviderShared.errorText(error), - ), - ), - ), - ), - ), - ), - ), + frames: (prepared, _request, runtime) => + prepared.framing.frame(RequestExecutor.stream(runtime.http, prepared.request, prepared.middleware)), }) export const sseJson = { diff --git a/packages/ai/src/route/transport/websocket.ts b/packages/ai/src/route/transport/websocket.ts index 0198deb5c63..b1d8854de1e 100644 --- a/packages/ai/src/route/transport/websocket.ts +++ b/packages/ai/src/route/transport/websocket.ts @@ -1,6 +1,6 @@ import { Cause, Context, Effect, Layer, Queue, Stream } from "effect" import { Headers } from "effect/unstable/http" -import { AIError, TransportReason } from "../../schema/index.js" +import { AIError, TransportReason, type TransportOperation } from "../../schema/index.js" import * as HttpTransport from "./http.js" import type { Transport } from "./index.js" @@ -29,12 +29,18 @@ export class Service extends Context.Service()("@opencode/AI const transportError = ( method: string, message: string, - input: { readonly url?: string; readonly kind?: string } = {}, + input: { readonly operation: TransportOperation; readonly url?: string; readonly code?: string }, ) => new AIError({ module: "WebSocketExecutor", method, - reason: new TransportReason({ message, url: input.url, kind: input.kind }), + reason: new TransportReason({ + message, + transport: "websocket", + operation: input.operation, + url: input.url, + code: input.code, + }), }) const eventMessage = (event: Event) => { @@ -55,7 +61,8 @@ const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => { return Effect.fail( transportError("open", `WebSocket closed before opening (state ${ws.readyState})`, { url: input.url, - kind: "open", + operation: "request", + code: "closed", }), ) } @@ -79,7 +86,10 @@ const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => { cleanup() resume( Effect.fail( - transportError("open", `Failed to open WebSocket: ${eventMessage(event)}`, { url: input.url, kind: "open" }), + transportError("open", `Failed to open WebSocket: ${eventMessage(event)}`, { + url: input.url, + operation: "request", + }), ), ) } @@ -89,7 +99,8 @@ const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => { Effect.fail( transportError("open", `WebSocket closed before opening with code ${event.code}`, { url: input.url, - kind: "open", + operation: "request", + code: String(event.code), }), ), ) @@ -118,7 +129,8 @@ const webSocketUrl = (value: string) => catch: (error) => transportError("prepare", error instanceof Error ? error.message : "Invalid WebSocket URL", { url: value, - kind: "websocket", + operation: "request", + code: "invalid-url", }), }) @@ -129,7 +141,7 @@ export const open = (input: WebSocketRequest) => catch: (error) => transportError("open", error instanceof Error ? error.message : "Failed to construct WebSocket", { url: input.url, - kind: "open", + operation: "request", }), }).pipe(Effect.flatMap((ws) => fromWebSocket(ws, input))) @@ -150,7 +162,10 @@ export const fromWebSocket = ( Queue.failCauseUnsafe( messages, Cause.fail( - transportError("message", "Unsupported WebSocket message payload", { url: input.url, kind: "message" }), + transportError("message", "Unsupported WebSocket message payload", { + url: input.url, + operation: "read", + }), ), ) } @@ -158,7 +173,10 @@ export const fromWebSocket = ( Queue.failCauseUnsafe( messages, Cause.fail( - transportError("message", `WebSocket error: ${eventMessage(event)}`, { url: input.url, kind: "message" }), + transportError("message", `WebSocket error: ${eventMessage(event)}`, { + url: input.url, + operation: "read", + }), ), ) } @@ -167,7 +185,11 @@ export const fromWebSocket = ( Queue.failCauseUnsafe( messages, Cause.fail( - transportError("message", `WebSocket closed with code ${event.code}`, { url: input.url, kind: "close" }), + transportError("message", `WebSocket closed with code ${event.code}`, { + url: input.url, + operation: "read", + code: String(event.code), + }), ), ) } @@ -188,7 +210,7 @@ export const fromWebSocket = ( catch: (error) => transportError("sendText", error instanceof Error ? error.message : "Failed to send WebSocket message", { url: input.url, - kind: "write", + operation: "write", }), }), messages: Stream.fromQueue(messages), @@ -243,7 +265,8 @@ export const json = (input: JsonInput): JsonTransp return Stream.fail( transportError("json", "WebSocket JSON transport requires WebSocketExecutor.Service", { url: prepared.url, - kind: "websocket", + operation: "request", + code: "unavailable", }), ) } diff --git a/packages/ai/src/schema/errors.ts b/packages/ai/src/schema/errors.ts index 0c201450fd9..d03f2f3cf7f 100644 --- a/packages/ai/src/schema/errors.ts +++ b/packages/ai/src/schema/errors.ts @@ -92,10 +92,18 @@ export class ProviderInternalReason extends Schema.Class http: Schema.optional(HttpContext), }) {} +export const TransportType = Schema.Literals(["http", "websocket"]) +export type TransportType = typeof TransportType.Type + +export const TransportOperation = Schema.Literals(["request", "read", "write"]) +export type TransportOperation = typeof TransportOperation.Type + export class TransportReason extends Schema.Class("AI.Error.Transport")({ _tag: Schema.tag("Transport"), message: Schema.String, - kind: Schema.optional(Schema.String), + transport: TransportType, + operation: TransportOperation, + code: Schema.optional(Schema.String), url: Schema.optional(Schema.String), http: Schema.optional(HttpContext), }) {} diff --git a/packages/ai/test/executor.test.ts b/packages/ai/test/executor.test.ts index 1d3353e44b8..7a483bbbd4d 100644 --- a/packages/ai/test/executor.test.ts +++ b/packages/ai/test/executor.test.ts @@ -1,10 +1,10 @@ import { describe, expect } from "bun:test" -import { Effect, Layer, Ref } from "effect" -import { Headers, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { Effect, Layer, Ref, Stream } from "effect" +import { Headers, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { LLM, AIError } from "../src/index.js" import { LLMClient, RequestExecutor } from "../src/route.js" import * as OpenAIChat from "../src/protocols/openai-chat.js" -import { dynamicResponse } from "./lib/http.js" +import { dynamicResponse, systemError } from "./lib/http.js" import { deltaChunk } from "./lib/openai-chunks.js" import { sseRaw } from "./lib/sse.js" import { it } from "./lib/effect.js" @@ -67,6 +67,64 @@ const expectAIError = (error: unknown) => { const errorHttp = (error: AIError) => ("http" in error.reason ? error.reason.http : undefined) describe("RequestExecutor", () => { + it.effect("parses response body failures at the executor seam", () => + Effect.gen(function* () { + const executor = yield* RequestExecutor.Service + const error = yield* RequestExecutor.stream(executor, secretRequest).pipe(Stream.runDrain, Effect.flip) + + expectAIError(error) + expect(error.reason).toMatchObject({ + _tag: "Transport", + message: "ECONNRESET: disconnected ", + transport: "http", + operation: "read", + code: "ECONNRESET", + url: "https://provider.test/v1/chat?api_key=%3Credacted%3E&debug=1", + }) + }).pipe( + Effect.provide( + responsesLayer([ + new Response( + new ReadableStream({ + start(controller) { + controller.error(systemError("ECONNRESET", "disconnected query-secret-123 header-secret-456")) + }, + }), + ), + ]), + ), + ), + ) + + it.effect("unwraps native transport failure causes", () => + Effect.gen(function* () { + const executor = yield* RequestExecutor.Service + const error = yield* RequestExecutor.stream(executor, secretRequest).pipe(Stream.runDrain, Effect.flip) + + expectAIError(error) + expect(error.reason).toMatchObject({ + _tag: "Transport", + message: "ECONNRESET: socket closed", + operation: "read", + code: "ECONNRESET", + }) + }).pipe( + Effect.provide( + responsesLayer([ + new Response( + new ReadableStream({ + pull(controller) { + controller.error( + new TypeError("fetch failed", { cause: systemError("ECONNRESET", "socket closed") }), + ) + }, + }), + ), + ]), + ), + ), + ) + it.effect("preserves middleware error messages", () => Effect.gen(function* () { const executor = yield* RequestExecutor.Service @@ -79,6 +137,48 @@ describe("RequestExecutor", () => { }).pipe(Effect.provide(responsesLayer([]))), ) + it.effect("reports the request sent by middleware", () => + Effect.gen(function* () { + const executor = yield* RequestExecutor.Service + const error = yield* executor + .execute(request, (original, handler) => + handler( + original.pipe( + HttpClientRequest.setUrl("https://proxy.test/v1/chat?api_key=proxy-secret"), + HttpClientRequest.setHeader("authorization", "Bearer proxy-secret"), + ), + ), + ) + .pipe(Effect.flip) + + expectAIError(error) + expect(error.reason).toMatchObject({ + _tag: "Transport", + message: "ECONNRESET: proxy disconnected ", + url: "https://proxy.test/v1/chat?api_key=%3Credacted%3E", + http: { + request: { + url: "https://proxy.test/v1/chat?api_key=%3Credacted%3E", + headers: { authorization: "" }, + }, + }, + }) + }).pipe( + Effect.provide( + dynamicResponse((input) => + Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.TransportError({ + request: input.request, + cause: systemError("ECONNRESET", "proxy disconnected proxy-secret"), + }), + }), + ), + ), + ), + ), + ) + it.effect("classifies context overflow responses", () => Effect.gen(function* () { const executor = yield* RequestExecutor.Service diff --git a/packages/ai/test/lib/http.ts b/packages/ai/test/lib/http.ts index e53794aa876..7c98d687ce4 100644 --- a/packages/ai/test/lib/http.ts +++ b/packages/ai/test/lib/http.ts @@ -1,5 +1,5 @@ import { Effect, Layer, Ref } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { LLMClient, RequestExecutor, WebSocketExecutor } from "../../src/route.js" import type { Service as LLMClientService } from "../../src/route/client.js" import type { Service as RequestExecutorService } from "../../src/route/executor.js" @@ -14,7 +14,9 @@ export type HandlerInput = { ) => HttpClientResponse.HttpClientResponse } -export type Handler = (input: HandlerInput) => Effect.Effect +export type Handler = ( + input: HandlerInput, +) => Effect.Effect const handlerLayer = (handler: Handler): Layer.Layer => Layer.succeed( @@ -34,6 +36,12 @@ const handlerLayer = (handler: Handler): Layer.Layer => export type RuntimeEnv = RequestExecutorService | WebSocketExecutorService | LLMClientService +export interface SystemError extends Error { + readonly code: string +} + +export const systemError = (code: string, message: string): SystemError => Object.assign(new Error(message), { code }) + export const runtimeLayer = (layer: Layer.Layer): Layer.Layer => { const requestExecutorLayer = RequestExecutor.layer.pipe(Layer.provide(layer)) const deps = Layer.mergeAll(requestExecutorLayer, WebSocketExecutor.layer) @@ -63,14 +71,20 @@ export const dynamicResponse = (handler: Handler) => runtimeLayer(handlerLayer(h * Layer that emits the supplied SSE chunks and then aborts mid-stream. Used to * exercise transport errors that surface during parsing. */ -export const truncatedStream = (chunks: ReadonlyArray) => +export const truncatedStream = (chunks: ReadonlyArray, error: Error = new Error("connection reset")) => dynamicResponse((input) => Effect.sync(() => { const encoder = new TextEncoder() + let index = 0 const stream = new ReadableStream({ - start(controller) { - for (const chunk of chunks) controller.enqueue(encoder.encode(chunk)) - controller.error(new Error("connection reset")) + pull(controller) { + const chunk = chunks[index] + if (chunk !== undefined) { + index++ + controller.enqueue(encoder.encode(chunk)) + return + } + controller.error(error) }, }) return input.respond(stream, { headers: SSE_HEADERS }) diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts index 910e57d26a5..ea327aa1235 100644 --- a/packages/ai/test/provider/openai-chat.test.ts +++ b/packages/ai/test/provider/openai-chat.test.ts @@ -1,5 +1,5 @@ import { describe, expect } from "bun:test" -import { Effect, Schema, Stream } from "effect" +import { Effect, Ref, Schema, Stream } from "effect" import { HttpClientRequest } from "effect/unstable/http" import { HttpOptions, @@ -22,7 +22,7 @@ import { ProviderShared } from "../../src/protocols/shared.js" import { Auth, LLMClient } from "../../src/route.js" import { compileRequest } from "../../src/route/client.js" import { it } from "../lib/effect.js" -import { dynamicResponse, fixedResponse, truncatedStream } from "../lib/http.js" +import { dynamicResponse, fixedResponse, systemError, truncatedStream } from "../lib/http.js" import { deltaChunk, usageChunk } from "../lib/openai-chunks.js" import { sseEvents } from "../lib/sse.js" @@ -1221,12 +1221,44 @@ describe("OpenAI Chat route", () => { it.effect("surfaces transport errors that occur mid-stream", () => Effect.gen(function* () { - const layer = truncatedStream([ - `data: ${JSON.stringify(deltaChunk({ role: "assistant", content: "Hello" }))}\n\n`, - ]) - const error = yield* LLMClient.generate(request).pipe(Effect.provide(layer), Effect.flip) + const layer = truncatedStream( + [`data: ${JSON.stringify(deltaChunk({ role: "assistant", content: "Hello" }))}\n\n`], + systemError("ECONNRESET", "socket closed unexpectedly"), + ) + const events = yield* Ref.make>([]) + const error = yield* LLMClient.stream(request).pipe( + Stream.tap((event) => Ref.update(events, (current) => [...current, event])), + Stream.runDrain, + Effect.provide(layer), + Effect.flip, + ) - expect(error.message).toContain("Failed to read openai/openai-chat stream") + expect((yield* Ref.get(events)).some((event) => event.type === "text-delta")).toBeTrue() + expect(error.reason).toMatchObject({ + _tag: "Transport", + message: "ECONNRESET: socket closed unexpectedly", + transport: "http", + operation: "read", + code: "ECONNRESET", + url: "https://api.openai.test/v1/chat/completions", + }) + }), + ) + + it.effect("surfaces transport errors before the first stream frame", () => + Effect.gen(function* () { + const error = yield* LLMClient.generate(request).pipe( + Effect.provide(truncatedStream([], systemError("ECONNRESET", "socket closed before output"))), + Effect.flip, + ) + + expect(error.reason).toMatchObject({ + _tag: "Transport", + message: "ECONNRESET: socket closed before output", + transport: "http", + operation: "read", + code: "ECONNRESET", + }) }), ) diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index 1234e1053b3..9652c16921a 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -763,7 +763,9 @@ function apiCallErrorReason(error: APICallError) { if (error.statusCode !== undefined || !error.isRetryable) return reason return new TransportReason({ message: reason.message, - kind: error.name, + transport: "http", + operation: "request", + code: error.name, url: error.url, http: "http" in reason ? reason.http : undefined, }) diff --git a/packages/core/test/aisdk.test.ts b/packages/core/test/aisdk.test.ts index 9decce67b81..a7620759448 100644 --- a/packages/core/test/aisdk.test.ts +++ b/packages/core/test/aisdk.test.ts @@ -49,7 +49,9 @@ const client = LLMClient.layer.pipe( Layer.provide( Layer.succeed( RequestExecutor.Service, - RequestExecutor.Service.of({ execute: () => Effect.die("Unexpected HTTP request") }), + RequestExecutor.Service.of({ + execute: () => Effect.die("Unexpected HTTP request"), + }), ), ), ) @@ -542,7 +544,12 @@ it.effect("retries status-less AI SDK transport failures", () => isRetryable: true, }), ) - expect(error.reason).toMatchObject({ _tag: "Transport", kind: "AI_APICallError" }) + expect(error.reason).toMatchObject({ + _tag: "Transport", + transport: "http", + operation: "request", + code: "AI_APICallError", + }) expect(SessionRunnerRetry.isRetryable(error)).toBeTrue() expect("http" in error.reason ? error.reason.http?.request.url : undefined).toBe("https://api.example.com/chat") }), diff --git a/packages/core/test/session-error.test.ts b/packages/core/test/session-error.test.ts index 625427d65cd..d0e14b8fa70 100644 --- a/packages/core/test/session-error.test.ts +++ b/packages/core/test/session-error.test.ts @@ -39,7 +39,9 @@ describe("toSessionError", () => { ) expect(toSessionError(llm(new QuotaExceededReason({ message: "quota" }))).type).toBe("provider.quota") expect(toSessionError(llm(new ContentPolicyReason({ message: "blocked" }))).type).toBe("provider.content-filter") - expect(toSessionError(llm(new TransportReason({ message: "transport" }))).type).toBe("provider.transport") + expect( + toSessionError(llm(new TransportReason({ message: "transport", transport: "http", operation: "request" }))).type, + ).toBe("provider.transport") expect(toSessionError(llm(new ProviderInternalReason({ message: "internal", status: 500 }))).type).toBe( "provider.internal", ) @@ -111,7 +113,7 @@ describe("toSessionError", () => { const eligible = [ llm(new RateLimitReason({ message: "rate" })), llm(new ProviderInternalReason({ message: "internal", status: 500 })), - llm(new TransportReason({ message: "transport" })), + llm(new TransportReason({ message: "transport", transport: "http", operation: "request" })), ] const ineligible = [ llm(new AuthenticationReason({ message: "auth", kind: "invalid" })), diff --git a/packages/core/test/session-execution.test.ts b/packages/core/test/session-execution.test.ts index e71cd12ade8..e6a6d498d59 100644 --- a/packages/core/test/session-execution.test.ts +++ b/packages/core/test/session-execution.test.ts @@ -32,7 +32,7 @@ describe("SessionExecution lifecycle", () => { new AIError({ module: "test", method: "stream", - reason: new TransportReason({ message: "Disconnected" }), + reason: new TransportReason({ message: "Disconnected", transport: "http", operation: "request" }), }), ), ), diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index fa068812bb6..5bfbedf4d6f 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -515,7 +515,11 @@ const providerUnavailable = () => new AIError({ module: "test", method: "stream", - reason: new TransportReason({ message: "Provider unavailable" }), + reason: new TransportReason({ + message: "Provider unavailable", + transport: "http", + operation: "request", + }), }) const incompleteStream = () =>