feat(llm): add consistent tagged checks
This commit is contained in:
+44
-10
@@ -169,10 +169,19 @@ export const ReasoningPart = TypeStruct("reasoning", "LLM.Content.Reasoning", {
|
||||
})
|
||||
export type ReasoningPart = Schema.Schema.Type<typeof ReasoningPart>
|
||||
|
||||
export const ContentPart = Schema.Union([TextPart, MediaPart, ToolCallPart, ToolResultPart, ReasoningPart]).pipe(
|
||||
const contentPartTagged = Schema.Union([TextPart, MediaPart, ToolCallPart, ToolResultPart, ReasoningPart]).pipe(
|
||||
Schema.toTaggedUnion("type"),
|
||||
)
|
||||
export type ContentPart = Schema.Schema.Type<typeof ContentPart>
|
||||
export const ContentPart = Object.assign(contentPartTagged, {
|
||||
is: {
|
||||
text: contentPartTagged.guards.text,
|
||||
media: contentPartTagged.guards.media,
|
||||
toolCall: contentPartTagged.guards["tool-call"],
|
||||
toolResult: contentPartTagged.guards["tool-result"],
|
||||
reasoning: contentPartTagged.guards.reasoning,
|
||||
},
|
||||
})
|
||||
export type ContentPart = Schema.Schema.Type<typeof contentPartTagged>
|
||||
|
||||
export class Message extends Schema.Class<Message>("LLM.Message")({
|
||||
id: Schema.optional(Schema.String),
|
||||
@@ -214,12 +223,19 @@ export class CacheIntent extends Schema.Class<CacheIntent>("LLM.CacheIntent")({
|
||||
key: Schema.optional(Schema.String),
|
||||
}) {}
|
||||
|
||||
export const ResponseFormat = Schema.Union([
|
||||
const responseFormatTagged = Schema.Union([
|
||||
TypeStruct("text", "LLM.ResponseFormat.Text", {}),
|
||||
TypeStruct("json", "LLM.ResponseFormat.Json", { schema: JsonSchema }),
|
||||
TypeStruct("tool", "LLM.ResponseFormat.Tool", { tool: ToolDefinition }),
|
||||
]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type ResponseFormat = Schema.Schema.Type<typeof ResponseFormat>
|
||||
export const ResponseFormat = Object.assign(responseFormatTagged, {
|
||||
is: {
|
||||
text: responseFormatTagged.guards.text,
|
||||
json: responseFormatTagged.guards.json,
|
||||
tool: responseFormatTagged.guards.tool,
|
||||
},
|
||||
})
|
||||
export type ResponseFormat = Schema.Schema.Type<typeof responseFormatTagged>
|
||||
|
||||
export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
|
||||
id: Schema.optional(Schema.String),
|
||||
@@ -457,9 +473,27 @@ export class ToolFailure extends Schema.TaggedErrorClass<ToolFailure>()("LLM.Too
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
}) {}
|
||||
|
||||
export type LLMError =
|
||||
| InvalidRequestError
|
||||
| NoAdapterError
|
||||
| ProviderChunkError
|
||||
| ProviderRequestError
|
||||
| TransportError
|
||||
const llmErrorTagged = Schema.Union([
|
||||
InvalidRequestError,
|
||||
NoAdapterError,
|
||||
ProviderChunkError,
|
||||
ProviderRequestError,
|
||||
TransportError,
|
||||
]).pipe(Schema.toTaggedUnion("_tag"))
|
||||
|
||||
/**
|
||||
* Tagged-union helpers for every error that can escape the LLM client runtime.
|
||||
* Individual classes still support `Effect.catchTag("LLM.ProviderChunkError", ...)`;
|
||||
* this union adds `LLMError.is.*`, `LLMError.guards`, `LLMError.isAnyOf`, and
|
||||
* `LLMError.match` for plain values, arrays, and UI/rendering code.
|
||||
*/
|
||||
export const LLMError = Object.assign(llmErrorTagged, {
|
||||
is: {
|
||||
invalidRequest: llmErrorTagged.guards["LLM.InvalidRequestError"],
|
||||
noAdapter: llmErrorTagged.guards["LLM.NoAdapterError"],
|
||||
providerChunk: llmErrorTagged.guards["LLM.ProviderChunkError"],
|
||||
providerRequest: llmErrorTagged.guards["LLM.ProviderRequestError"],
|
||||
transport: llmErrorTagged.guards["LLM.TransportError"],
|
||||
},
|
||||
})
|
||||
export type LLMError = Schema.Schema.Type<typeof llmErrorTagged>
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { ContentPart, LLMEvent, LLMRequest, ModelCapabilities, ModelID, ModelLimits, ModelRef, ProviderID } from "../src/schema"
|
||||
import {
|
||||
ContentPart,
|
||||
InvalidRequestError,
|
||||
LLMError,
|
||||
LLMEvent,
|
||||
LLMRequest,
|
||||
ModelCapabilities,
|
||||
ModelID,
|
||||
ModelLimits,
|
||||
ModelRef,
|
||||
ProviderID,
|
||||
ResponseFormat,
|
||||
} from "../src/schema"
|
||||
|
||||
const capabilities = new ModelCapabilities({
|
||||
input: { text: true, image: false, audio: false, video: false, pdf: false },
|
||||
@@ -55,4 +67,25 @@ describe("llm schema", () => {
|
||||
expect(ContentPart.guards.text({ type: "text", text: "hi" })).toBe(true)
|
||||
expect(ContentPart.guards.media({ type: "text", text: "hi" })).toBe(false)
|
||||
})
|
||||
|
||||
test("tagged unions expose consistent camel-case is helpers", () => {
|
||||
expect(ContentPart.is.toolCall({ type: "tool-call", id: "call_1", name: "lookup", input: {} })).toBe(true)
|
||||
expect(ContentPart.is.toolResult({ type: "tool-call", id: "call_1", name: "lookup", input: {} })).toBe(false)
|
||||
expect(ResponseFormat.is.json({ type: "json", schema: { type: "object" } })).toBe(true)
|
||||
expect(LLMEvent.is.providerError({ type: "provider-error", message: "Nope" })).toBe(true)
|
||||
})
|
||||
|
||||
test("LLMError exposes tagged error guards and matching", () => {
|
||||
const error = new InvalidRequestError({ message: "Bad request" })
|
||||
|
||||
expect(LLMError.is.invalidRequest(error)).toBe(true)
|
||||
expect(LLMError.guards["LLM.InvalidRequestError"](error)).toBe(true)
|
||||
expect(LLMError.match(error, {
|
||||
"LLM.InvalidRequestError": (value) => value.message,
|
||||
"LLM.NoAdapterError": (value) => value.protocol,
|
||||
"LLM.ProviderChunkError": (value) => value.adapter,
|
||||
"LLM.ProviderRequestError": (value) => String(value.status),
|
||||
"LLM.TransportError": (value) => value.reason ?? value.message,
|
||||
})).toBe("Bad request")
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user