diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts index f426a50f87..72d9796588 100644 --- a/packages/client/src/generated/types.ts +++ b/packages/client/src/generated/types.ts @@ -581,7 +581,26 @@ export type SessionsContextOutput = { readonly reasoning: number readonly cache: { readonly read: number; readonly write: number } } - readonly error?: { readonly type: "unknown"; readonly message: string } + readonly error?: + | { readonly type: "unknown"; readonly message: string } + | { + readonly type: "provider" + readonly category: + | "invalid-request" + | "no-route" + | "authentication" + | "rate-limit" + | "quota-exceeded" + | "content-policy" + | "provider-internal" + | "transport" + | "invalid-provider-output" + | "unknown" + readonly message: string + readonly status?: number | null + readonly retryable: boolean + readonly retryAfterMs?: number | null + } } | { readonly type: "compaction" @@ -794,7 +813,26 @@ export type SessionsEventsOutput = readonly timestamp: number readonly sessionID: string readonly assistantMessageID: string - readonly error: { readonly type: "unknown"; readonly message: string } + readonly error: + | { readonly type: "unknown"; readonly message: string } + | { + readonly type: "provider" + readonly category: + | "invalid-request" + | "no-route" + | "authentication" + | "rate-limit" + | "quota-exceeded" + | "content-policy" + | "provider-internal" + | "transport" + | "invalid-provider-output" + | "unknown" + readonly message: string + readonly status?: number | undefined + readonly retryable: boolean + readonly retryAfterMs?: number | undefined + } } } | { @@ -1198,7 +1236,26 @@ export type SessionsMessageOutput = { readonly reasoning: number readonly cache: { readonly read: number; readonly write: number } } - readonly error?: { readonly type: "unknown"; readonly message: string } + readonly error?: + | { readonly type: "unknown"; readonly message: string } + | { + readonly type: "provider" + readonly category: + | "invalid-request" + | "no-route" + | "authentication" + | "rate-limit" + | "quota-exceeded" + | "content-policy" + | "provider-internal" + | "transport" + | "invalid-provider-output" + | "unknown" + readonly message: string + readonly status?: number | null + readonly retryable: boolean + readonly retryAfterMs?: number | null + } } | { readonly type: "compaction" diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 9e29ae71ee..f1d6023669 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -6,6 +6,7 @@ import { Message, SystemPart, isContextOverflowFailure, + type LLMErrorReason, type ProviderErrorEvent, } from "@opencode-ai/llm" import { Cause, DateTime, Effect, FiberSet, Layer, Option, Semaphore, Stream } from "effect" @@ -32,7 +33,7 @@ import { SessionSchema } from "../schema" import { SessionStore } from "../store" import { type RunError, Service } from "./index" import { SessionRunnerModel } from "./model" -import { createLLMEventPublisher } from "./publish-llm-event" +import { createLLMEventPublisher, providerError } from "./publish-llm-event" import { toLLMMessages } from "./to-llm-message" import { MAX_STEPS_PROMPT } from "./max-steps" import { Snapshot } from "../../snapshot" @@ -87,6 +88,29 @@ import { Snapshot } from "../../snapshot" * explicit loop starts the next provider turn after local settlement. Configured agent step limits bound the loop. */ +const providerCategories = { + InvalidRequest: "invalid-request", + NoRoute: "no-route", + Authentication: "authentication", + RateLimit: "rate-limit", + QuotaExceeded: "quota-exceeded", + ContentPolicy: "content-policy", + ProviderInternal: "provider-internal", + Transport: "transport", + InvalidProviderOutput: "invalid-provider-output", + UnknownProvider: "unknown", +} as const satisfies Record[0]["category"]> + +const toSessionProviderError = (reason: LLMErrorReason) => + providerError({ + category: providerCategories[reason._tag], + status: + ("http" in reason ? reason.http?.response?.status : undefined) ?? + ("status" in reason ? reason.status : undefined), + retryable: reason.retryable, + retryAfterMs: "retryAfterMs" in reason ? reason.retryAfterMs : undefined, + }) + export const layer = Layer.effect( Service, Effect.gen(function* () { @@ -283,7 +307,7 @@ export const layer = Layer.effect( const llmFailure = failure instanceof LLMError ? failure : undefined if (llmFailure && !publisher.hasProviderError()) { yield* withPublication(publisher.failUnsettledTools("Provider did not return a tool result", true)) - yield* withPublication(publisher.failAssistant(llmFailure.reason.message)) + yield* withPublication(publisher.failAssistant(toSessionProviderError(llmFailure.reason))) } if (stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)) yield* FiberSet.clear(toolFibers) const settled = yield* restore(awaitToolFibers(toolFibers)).pipe(Effect.exit) @@ -299,7 +323,7 @@ export const layer = Layer.effect( yield* FiberSet.clear(toolFibers) yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted")) if (publisher.hasActiveAssistant()) - yield* withPublication(publisher.failAssistant("Provider turn interrupted")) + yield* withPublication(publisher.failAssistant({ type: "unknown", message: "Provider turn interrupted" })) } if (settled._tag === "Failure" && !Cause.hasInterrupts(settled.cause)) { const failure = Cause.squash(settled.cause) diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index 33652a618c..3d43b05748 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -50,6 +50,33 @@ const settledOutput = (value: ToolOutput | undefined, result: ToolResultValue): return { structured: record(settled.structured), content: settled.content } } +const providerMessages: Record = { + "invalid-request": "Provider rejected the request", + "no-route": "Provider unavailable: no provider route", + authentication: "Provider authentication failed", + "rate-limit": "Provider rate limit exceeded", + "quota-exceeded": "Provider quota exceeded", + "content-policy": "Provider rejected the invalid request due to content policy", + "provider-internal": "Provider service unavailable", + transport: "Provider connection failed", + "invalid-provider-output": "Provider returned an invalid response", + unknown: "Provider request failed", +} + +export const providerError = (input: { + readonly category: SessionMessage.ProviderErrorCategory + readonly status?: number + readonly retryable?: boolean + readonly retryAfterMs?: number +}): SessionMessage.ProviderError => ({ + type: "provider", + category: input.category, + message: providerMessages[input.category], + status: input.status, + retryable: input.retryable ?? (input.category === "rate-limit" || input.category === "provider-internal"), + retryAfterMs: input.retryAfterMs, +}) + /** Persist one provider turn without executing tools or starting a continuation turn. */ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) => { const tools = new Map< @@ -67,7 +94,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) const timestamp = DateTime.now let assistantMessageID: SessionMessage.ID | undefined let assistantActive = false - let assistantFailed = false let providerFailed = false let stepSettlement: { readonly finish: string; readonly tokens: ReturnType } | undefined @@ -196,17 +222,17 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) yield* flushFragments() }) - const failAssistant = Effect.fnUntraced(function* (message: string) { - if (assistantFailed) return + const failAssistant = Effect.fnUntraced(function* (error: SessionMessage.Error) { + if (providerFailed) return + providerFailed = true yield* flush() const assistantMessageID = yield* startAssistant() assistantActive = false - assistantFailed = true yield* events.publish(SessionEvent.Step.Failed, { sessionID: input.sessionID, timestamp: yield* timestamp, assistantMessageID, - error: { type: "unknown", message }, + error, }) }) @@ -402,8 +428,13 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) case "finish": return case "provider-error": - providerFailed = true - yield* failAssistant(event.message) + yield* failAssistant( + providerError({ + category: event.category ?? (event.classification === "context-overflow" ? "invalid-request" : "unknown"), + status: event.status, + retryable: event.retryable, + }), + ) return } }) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index c9594e8244..23ff2980e2 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -4,6 +4,11 @@ import { LLMError, LLMEvent, Model, + HttpContext, + HttpRateLimitDetails, + HttpRequestDetails, + HttpResponseDetails, + RateLimitReason, TransportReason, InvalidRequestReason, type LLMClientShape, @@ -515,13 +520,21 @@ const verifyPartialFlushOnFailure = (kind: FragmentKind) => responseStream = Stream.concat(Stream.fromIterable(fixture.partialEvents), Stream.fail(failure)) expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure) + const expectedContent = + kind === "tool input" + ? { + type: "tool", + id: fragmentID(kind, "partial"), + state: { status: "error", error: { message: "Tool execution interrupted" } }, + } + : fixture.expectedContent expect(yield* session.context(sessionID)).toMatchObject([ { type: "user", text: prompt }, { type: "assistant", finish: "error", - error: { type: "unknown", message: "Provider unavailable" }, - content: [fixture.expectedContent], + error: { type: "provider", category: "transport", message: "Provider connection failed", retryable: false }, + content: [expectedContent], }, ]) }) @@ -1196,7 +1209,16 @@ describe("SessionRunnerLLM", () => { expect(requests).toHaveLength(3) expect(yield* session.context(sessionID)).toMatchObject([ { type: "compaction" }, - { type: "assistant", finish: "error", error: { message: "prompt too long" } }, + { + type: "assistant", + finish: "error", + error: { + type: "provider", + category: "invalid-request", + message: "Provider rejected the request", + retryable: false, + }, + }, ]) }), ) @@ -1244,7 +1266,16 @@ describe("SessionRunnerLLM", () => { expect(context.some((message) => message.type === "compaction")).toBe(false) expect(context.slice(-2)).toMatchObject([ { type: "user", text: "Continue" }, - { type: "assistant", finish: "error", error: { message: "prompt too long" } }, + { + type: "assistant", + finish: "error", + error: { + type: "provider", + category: "invalid-request", + message: "Provider rejected the request", + retryable: false, + }, + }, ]) }), ) @@ -2920,7 +2951,16 @@ describe("SessionRunnerLLM", () => { expect(requests).toHaveLength(1) expect(yield* session.context(sessionID)).toMatchObject([ { type: "user", text: "Fail durably" }, - { type: "assistant", finish: "error", error: { type: "unknown", message: "Provider unavailable" } }, + { + type: "assistant", + finish: "error", + error: { + type: "provider", + category: "unknown", + message: "Provider request failed", + retryable: false, + }, + }, ]) }), ) @@ -2939,7 +2979,16 @@ describe("SessionRunnerLLM", () => { expect(requests).toHaveLength(1) expect(yield* session.context(sessionID)).toMatchObject([ { type: "user", text: "Fail before step" }, - { type: "assistant", finish: "error", error: { type: "unknown", message: "Provider unavailable" } }, + { + type: "assistant", + finish: "error", + error: { + type: "provider", + category: "unknown", + message: "Provider request failed", + retryable: false, + }, + }, ]) }), ) @@ -2966,7 +3015,12 @@ describe("SessionRunnerLLM", () => { { type: "assistant", finish: "error", - error: { message: "prompt too long" }, + error: { + type: "provider", + category: "invalid-request", + message: "Provider rejected the request", + retryable: false, + }, content: [{ type: "text", text: "Partial" }], }, ]) @@ -2985,11 +3039,138 @@ describe("SessionRunnerLLM", () => { yield* replaySessionProjection(sessionID) expect(yield* session.context(sessionID)).toMatchObject([ { type: "user", text: "Fail raw stream durably" }, - { type: "assistant", finish: "error", error: { type: "unknown", message: "Provider unavailable" } }, + { + type: "assistant", + finish: "error", + error: { type: "provider", category: "transport", message: "Provider connection failed", retryable: false }, + }, ]) }), ) + it.effect("does not publish step ended when the provider throws after step finish", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const db = yield* Database.Service + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail after finish" }), resume: false }) + const failure = providerUnavailable() + responseStream = Stream.concat( + Stream.fromIterable([LLMEvent.stepFinish({ index: 0, reason: "stop" })]), + Stream.fail(failure), + ) + + expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure) + + const types = yield* db.db.select({ type: EventTable.type }).from(EventTable).all().pipe(Effect.orDie) + expect(types).toContainEqual({ + type: EventV2.versionedType(SessionEvent.Step.Failed.type, 2), + }) + expect(types).not.toContainEqual({ + type: EventV2.versionedType(SessionEvent.Step.Ended.type, 2), + }) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Fail after finish" }, + { + type: "assistant", + finish: "error", + error: { type: "provider", category: "transport", message: "Provider connection failed" }, + }, + ]) + }), + ) + + it.effect("preserves sanitized HTTP rate-limit details in the durable event and projection", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const db = yield* Database.Service + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail with rate limit" }), resume: false }) + const failure = new LLMError({ + module: "RequestExecutor", + method: "execute", + reason: new RateLimitReason({ + message: "secret provider response message", + retryAfterMs: 12_000, + rateLimit: new HttpRateLimitDetails({ retryAfterMs: 12_000, limit: { requests: "secret-limit" } }), + http: new HttpContext({ + request: new HttpRequestDetails({ + method: "POST", + url: "https://secret.example/v1/responses?api_key=credential", + headers: { authorization: "Bearer credential" }, + }), + response: new HttpResponseDetails({ status: 429, headers: { "x-secret": "secret-header" } }), + body: '{"secret":"provider body"}', + requestId: "secret-request-id", + }), + }), + }) + responseStream = Stream.fail(failure) + + expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure) + + const event = yield* db.db + .select({ data: EventTable.data }) + .from(EventTable) + .where(eq(EventTable.type, EventV2.versionedType(SessionEvent.Step.Failed.type, 2))) + .get() + .pipe(Effect.orDie) + const expected = { + type: "provider", + category: "rate-limit", + message: "Provider rate limit exceeded", + status: 429, + retryable: true, + retryAfterMs: 12_000, + } + expect(event?.data.error).toEqual(expected) + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Fail with rate limit" }, + { type: "assistant", finish: "error", error: expected }, + ]) + expect(JSON.stringify(event)).not.toMatch( + /secret provider|secret\.example|credential|secret-header|provider body|secret-request-id|secret-limit/, + ) + expect(JSON.stringify(yield* session.context(sessionID))).not.toMatch( + /secret provider|secret\.example|credential|secret-header|provider body|secret-request-id|secret-limit/, + ) + }), + ) + + it.effect("projects categorized in-band failures without fabricating an HTTP status", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail in band" }), resume: false }) + response = [ + LLMEvent.providerError({ + message: "rate_limit_exceeded: secret provider message", + category: "rate-limit", + retryable: false, + }), + ] + + yield* session.resume(sessionID) + + expect(yield* session.context(sessionID)).toMatchObject([ + { type: "user", text: "Fail in band" }, + { + type: "assistant", + finish: "error", + error: { + type: "provider", + category: "rate-limit", + message: "Provider rate limit exceeded", + retryable: false, + }, + }, + ]) + const assistant = (yield* session.context(sessionID))[1] + expect(assistant?.type === "assistant" ? assistant.error : undefined).not.toHaveProperty("status") + expect(JSON.stringify(assistant)).not.toContain("secret provider message") + }), + ) + it.effect("does not continue automatically after a provider error follows a local tool call", () => Effect.gen(function* () { yield* setup @@ -3005,13 +3186,25 @@ describe("SessionRunnerLLM", () => { response = [ LLMEvent.stepStart({ index: 0 }), LLMEvent.toolCall({ id: "call-before-provider-error", name: "echo", input: { text: "settled" } }), - LLMEvent.providerError({ message: "Provider unavailable" }), + LLMEvent.providerError({ message: "secret provider failure" }), ] yield* session.resume(sessionID) expect(requests).toHaveLength(1) expect(executions.slice(executionCount)).toEqual(["settled"]) + const assistant = (yield* session.context(sessionID))[1] + expect(assistant).toMatchObject({ + type: "assistant", + finish: "error", + error: { + type: "provider", + category: "unknown", + message: "Provider request failed", + retryable: false, + }, + }) + expect(JSON.stringify(assistant)).not.toContain("secret provider failure") }), ) @@ -3101,7 +3294,12 @@ describe("SessionRunnerLLM", () => { { type: "assistant", finish: "error", - error: { type: "unknown", message: "Provider unavailable" }, + error: { + type: "provider", + category: "transport", + message: "Provider connection failed", + retryable: false, + }, content: [{ type: "tool", id: "call-hosted-raw-failure", state: { status: "error" } }], }, ]) diff --git a/packages/core/test/shared-schema.test.ts b/packages/core/test/shared-schema.test.ts index 959024cd4a..434d98bf05 100644 --- a/packages/core/test/shared-schema.test.ts +++ b/packages/core/test/shared-schema.test.ts @@ -144,6 +144,8 @@ test("Core reuses the canonical shared schemas", async () => { [coreSessionInput.Admitted, SessionInput.Admitted], [coreSessionMessage.ID, SessionMessage.ID], [coreSessionMessage.UnknownError, SessionMessage.UnknownError], + [coreSessionMessage.ProviderError, SessionMessage.ProviderError], + [coreSessionMessage.Error, SessionMessage.Error], [coreSessionMessage.AgentSwitched, SessionMessage.AgentSwitched], [coreSessionMessage.ModelSwitched, SessionMessage.ModelSwitched], [coreSessionMessage.User, SessionMessage.User], @@ -204,3 +206,17 @@ test("shared record schemas construct and decode plain objects", () => { expect(Prompt.fromUserMessage({ text: "hello" })).toEqual(made) expect(Workspace.ID.ascending("")).toStartWith("wrk_") }) + +test("assistant errors retain legacy unknown decode compatibility", () => { + const assistant = Schema.decodeUnknownSync(SessionMessage.Assistant)({ + id: "msg_legacy_error", + type: "assistant", + agent: "build", + model: { id: "model", providerID: "provider" }, + content: [], + error: { type: "unknown", message: "Legacy failure" }, + time: { created: 0 }, + }) + + expect(assistant.error).toEqual({ type: "unknown", message: "Legacy failure" }) +}) diff --git a/packages/llm/src/schema/errors.ts b/packages/llm/src/schema/errors.ts index 072e4e8389..9cea9d75d6 100644 --- a/packages/llm/src/schema/errors.ts +++ b/packages/llm/src/schema/errors.ts @@ -4,6 +4,20 @@ import { ModelID, ProviderID, ProviderMetadata, RouteID } from "./ids" export const ProviderFailureClassification = Schema.Literal("context-overflow") export type ProviderFailureClassification = typeof ProviderFailureClassification.Type +export const ProviderFailureCategory = Schema.Literals([ + "invalid-request", + "no-route", + "authentication", + "rate-limit", + "quota-exceeded", + "content-policy", + "provider-internal", + "transport", + "invalid-provider-output", + "unknown", +]) +export type ProviderFailureCategory = typeof ProviderFailureCategory.Type + export class HttpRequestDetails extends Schema.Class("LLM.HttpRequestDetails")({ method: Schema.String, url: Schema.String, diff --git a/packages/llm/src/schema/events.ts b/packages/llm/src/schema/events.ts index 3e46013521..9d839f407f 100644 --- a/packages/llm/src/schema/events.ts +++ b/packages/llm/src/schema/events.ts @@ -2,7 +2,7 @@ import { Schema } from "effect" import { ContentBlockID, FinishReason, ProtocolID, ProviderMetadata, RouteID, ToolCallID } from "./ids" import { ModelSchema } from "./options" import { ToolOutput, ToolResultValue } from "./messages" -import { ProviderFailureClassification } from "./errors" +import { ProviderFailureCategory, ProviderFailureClassification } from "./errors" /** * Token usage reported by an LLM provider. @@ -201,6 +201,8 @@ export const ProviderErrorEvent = Schema.Struct({ type: Schema.tag("provider-error"), message: Schema.String, classification: Schema.optional(ProviderFailureClassification), + category: Schema.optional(ProviderFailureCategory), + status: Schema.optional(Schema.Number), retryable: Schema.optional(Schema.Boolean), providerMetadata: Schema.optional(ProviderMetadata), }).annotate({ identifier: "LLM.Event.ProviderError" }) diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts index f58c3dcce0..1917bcf701 100644 --- a/packages/schema/src/session-event.ts +++ b/packages/schema/src/session-event.ts @@ -50,6 +50,10 @@ const stepSettlementOptions = { export const UnknownError = SessionMessage.UnknownError export type UnknownError = SessionMessage.UnknownError +export const ProviderError = SessionMessage.ProviderError +export type ProviderError = SessionMessage.ProviderError +export const Error = SessionMessage.Error +export type Error = SessionMessage.Error export const AgentSwitched = Event.define({ type: "session.next.agent.switched", @@ -188,7 +192,7 @@ export namespace Step { schema: { ...Base, assistantMessageID: SessionMessage.ID, - error: UnknownError, + error: Error, }, }) export type Failed = typeof Failed.Type diff --git a/packages/schema/src/session-message.ts b/packages/schema/src/session-message.ts index f32bb0582e..0f0d96709e 100644 --- a/packages/schema/src/session-message.ts +++ b/packages/schema/src/session-message.ts @@ -21,6 +21,33 @@ export const UnknownError = Schema.Struct({ message: Schema.String, }).annotate({ identifier: "Session.Error.Unknown" }) +export const ProviderErrorCategory = Schema.Literals([ + "invalid-request", + "no-route", + "authentication", + "rate-limit", + "quota-exceeded", + "content-policy", + "provider-internal", + "transport", + "invalid-provider-output", + "unknown", +]) +export type ProviderErrorCategory = typeof ProviderErrorCategory.Type + +export interface ProviderError extends Schema.Schema.Type {} +export const ProviderError = Schema.Struct({ + type: Schema.Literal("provider"), + category: ProviderErrorCategory, + message: Schema.String, + status: Schema.Finite.pipe(Schema.optional), + retryable: Schema.Boolean, + retryAfterMs: Schema.Finite.pipe(Schema.optional), +}).annotate({ identifier: "Session.Error.Provider" }) + +export const Error = Schema.Union([UnknownError, ProviderError]).pipe(Schema.toTaggedUnion("type")) +export type Error = UnknownError | ProviderError + const Base = { id: ID, metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional), @@ -177,7 +204,7 @@ export const Assistant = Schema.Struct({ reasoning: Schema.Finite, cache: Schema.Struct({ read: Schema.Finite, write: Schema.Finite }), }).pipe(optional), - error: UnknownError.pipe(optional), + error: Error.pipe(optional), time: Schema.Struct({ created: DateTimeUtcFromMillis, completed: DateTimeUtcFromMillis.pipe(optional), diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index ae610a3f12..12253d6945 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -952,7 +952,7 @@ export type GlobalEvent = { timestamp: number sessionID: string assistantMessageID: string - error: SessionErrorUnknown + error: SessionErrorUnknown | SessionErrorProvider } } | { @@ -2955,6 +2955,25 @@ export type SessionErrorUnknown = { message: string } +export type SessionErrorProvider = { + type: "provider" + category: + | "invalid-request" + | "no-route" + | "authentication" + | "rate-limit" + | "quota-exceeded" + | "content-policy" + | "provider-internal" + | "transport" + | "invalid-provider-output" + | "unknown" + message: string + status?: number + retryable: boolean + retryAfterMs?: number +} + export type LlmProviderMetadata = { [key: string]: { [key: string]: unknown @@ -3399,7 +3418,7 @@ export type SyncEventSessionNextStepFailed = { timestamp: number sessionID: string assistantMessageID: string - error: SessionErrorUnknown + error: SessionErrorUnknown | SessionErrorProvider } } } @@ -4009,7 +4028,7 @@ export type SessionMessageAssistant = { write: number } } - error?: SessionErrorUnknown + error?: SessionErrorUnknown | SessionErrorProvider } export type SessionMessageCompaction = { @@ -4289,7 +4308,7 @@ export type SessionNextStepFailed = { timestamp: number sessionID: string assistantMessageID: string - error: SessionErrorUnknown + error: SessionErrorUnknown | SessionErrorProvider } } @@ -5344,7 +5363,7 @@ export type V2EventSessionNextStepFailed = { timestamp: number sessionID: string assistantMessageID: string - error: SessionErrorUnknown + error: SessionErrorUnknown | SessionErrorProvider } } @@ -6931,7 +6950,7 @@ export type EventSessionNextStepFailed = { timestamp: number sessionID: string assistantMessageID: string - error: SessionErrorUnknown + error: SessionErrorUnknown | SessionErrorProvider } }