chore: generate
This commit is contained in:
@@ -105,18 +105,22 @@ const streamWithTools = Effect.gen(function* () {
|
||||
})
|
||||
const events = Array.from(yield* LLM.stream(request).pipe(Stream.runCollect))
|
||||
for (const event of events) {
|
||||
if (event.type === "tool-call") console.log("tool call", event.name, event.input)
|
||||
if (event.type === "text-delta") process.stdout.write(event.text)
|
||||
if (event.type !== "tool-call" || event.providerExecuted) continue
|
||||
const dispatched = yield* ToolRuntime.dispatch(tools, event)
|
||||
console.log("tool result", event.name, dispatched.result)
|
||||
if (event.type === "tool-call") console.log("tool call", event.name, event.input)
|
||||
if (event.type === "text-delta") process.stdout.write(event.text)
|
||||
if (event.type !== "tool-call" || event.providerExecuted) continue
|
||||
const dispatched = yield* ToolRuntime.dispatch(tools, event)
|
||||
console.log("tool result", event.name, dispatched.result)
|
||||
|
||||
// A durable agent would persist these messages before starting another
|
||||
// raw model turn. This tutorial keeps the boundary visible instead.
|
||||
const followUp = LLM.updateRequest(request, {
|
||||
messages: [...request.messages, Message.assistant([event]), Message.tool({ ...event, result: dispatched.result })],
|
||||
})
|
||||
console.log("follow-up history messages:", followUp.messages.length)
|
||||
// A durable agent would persist these messages before starting another
|
||||
// raw model turn. This tutorial keeps the boundary visible instead.
|
||||
const followUp = LLM.updateRequest(request, {
|
||||
messages: [
|
||||
...request.messages,
|
||||
Message.assistant([event]),
|
||||
Message.tool({ ...event, result: dispatched.result }),
|
||||
],
|
||||
})
|
||||
console.log("follow-up history messages:", followUp.messages.length)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -363,11 +363,15 @@ const validateNativeSystemUpdate = Effect.fn("AnthropicMessages.validateNativeSy
|
||||
const previous = messages[index - 1]
|
||||
const next = messages[index + 1]
|
||||
if (!previous)
|
||||
return yield* invalid("Anthropic Messages chronological system updates cannot be the first message; use LLMRequest.system")
|
||||
return yield* invalid(
|
||||
"Anthropic Messages chronological system updates cannot be the first message; use LLMRequest.system",
|
||||
)
|
||||
if (previous.role === "system")
|
||||
return yield* invalid("Anthropic Messages chronological system updates cannot be consecutive")
|
||||
if (endsInLocalToolUse(previous))
|
||||
return yield* invalid("Anthropic Messages chronological system updates cannot appear between a local tool call and its tool result")
|
||||
return yield* invalid(
|
||||
"Anthropic Messages chronological system updates cannot appear between a local tool call and its tool result",
|
||||
)
|
||||
if (previous.role !== "user" && previous.role !== "tool" && !endsInServerToolUse(previous))
|
||||
return yield* invalid(
|
||||
"Anthropic Messages chronological system updates must follow a user message, tool result, or assistant server tool use",
|
||||
@@ -375,7 +379,9 @@ const validateNativeSystemUpdate = Effect.fn("AnthropicMessages.validateNativeSy
|
||||
if (next?.role === "system")
|
||||
return yield* invalid("Anthropic Messages chronological system updates cannot be consecutive")
|
||||
if (next && next.role !== "assistant")
|
||||
return yield* invalid("Anthropic Messages chronological system updates must end the messages array or immediately precede an assistant message")
|
||||
return yield* invalid(
|
||||
"Anthropic Messages chronological system updates must end the messages array or immediately precede an assistant message",
|
||||
)
|
||||
})
|
||||
|
||||
const lowerNativeSystemUpdate = Effect.fn("AnthropicMessages.lowerNativeSystemUpdate")(function* (
|
||||
@@ -409,7 +415,8 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
||||
const part = yield* ProviderShared.wrappedSystemUpdate("Anthropic Messages", message)
|
||||
const block = { type: "text" as const, text: part.text, cache_control: cacheControl(breakpoints, part.cache) }
|
||||
const previous = messages.at(-1)
|
||||
if (previous?.role === "user") messages[messages.length - 1] = { role: "user", content: [...previous.content, block] }
|
||||
if (previous?.role === "user")
|
||||
messages[messages.length - 1] = { role: "user", content: [...previous.content, block] }
|
||||
else messages.push({ role: "user", content: [block] })
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -243,7 +243,10 @@ const bedrockMetadata = (metadata: Record<string, unknown>): ProviderMetadata =>
|
||||
|
||||
const reasoningSignature = (part: ReasoningPart) => {
|
||||
const bedrock = part.providerMetadata?.bedrock
|
||||
return part.encrypted ?? (ProviderShared.isRecord(bedrock) && typeof bedrock.signature === "string" ? bedrock.signature : undefined)
|
||||
return (
|
||||
part.encrypted ??
|
||||
(ProviderShared.isRecord(bedrock) && typeof bedrock.signature === "string" ? bedrock.signature : undefined)
|
||||
)
|
||||
}
|
||||
|
||||
const lowerToolCall = (part: ToolCallPart): BedrockToolUseBlock => ({
|
||||
@@ -294,7 +297,8 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
|
||||
const part = yield* ProviderShared.wrappedSystemUpdate("Bedrock Converse", message)
|
||||
const content = textWithCache(breakpoints, part.text, part.cache)
|
||||
const previous = messages.at(-1)
|
||||
if (previous?.role === "user") messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
|
||||
if (previous?.role === "user")
|
||||
messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
|
||||
else messages.push({ role: "user", content })
|
||||
continue
|
||||
}
|
||||
@@ -532,7 +536,9 @@ const step = (state: ParserState, event: BedrockEvent) =>
|
||||
Lifecycle.textEnd(state.lifecycle, events, `text-${index}`),
|
||||
events,
|
||||
`reasoning-${index}`,
|
||||
state.reasoningSignatures[index] ? bedrockMetadata({ signature: state.reasoningSignatures[index] }) : undefined,
|
||||
state.reasoningSignatures[index]
|
||||
? bedrockMetadata({ signature: state.reasoningSignatures[index] })
|
||||
: undefined,
|
||||
)
|
||||
events.push(...resultEvents)
|
||||
return [
|
||||
|
||||
@@ -204,7 +204,8 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
|
||||
if (message.role === "system") {
|
||||
const part = yield* ProviderShared.wrappedSystemUpdate("Gemini", message)
|
||||
const previous = contents.at(-1)
|
||||
if (previous?.role === "user") contents[contents.length - 1] = { role: "user", parts: [...previous.parts, { text: part.text }] }
|
||||
if (previous?.role === "user")
|
||||
contents[contents.length - 1] = { role: "user", parts: [...previous.parts, { text: part.text }] }
|
||||
else contents.push({ role: "user", parts: [{ text: part.text }] })
|
||||
continue
|
||||
}
|
||||
@@ -405,7 +406,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
id,
|
||||
name: part.functionCall.name,
|
||||
input,
|
||||
providerMetadata: part.thoughtSignature ? googleMetadata({ thoughtSignature: part.thoughtSignature }) : undefined,
|
||||
providerMetadata: part.thoughtSignature
|
||||
? googleMetadata({ thoughtSignature: part.thoughtSignature })
|
||||
: undefined,
|
||||
}),
|
||||
)
|
||||
hasToolCalls = true
|
||||
|
||||
@@ -226,7 +226,9 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
|
||||
content: content.length === 0 ? null : ProviderShared.joinText(content),
|
||||
tool_calls: toolCalls.length === 0 ? undefined : toolCalls,
|
||||
reasoning_content:
|
||||
reasoning.length > 0 ? reasoning.map((part) => part.text).join("") : openAICompatibleReasoningContent(message.native?.openaiCompatible),
|
||||
reasoning.length > 0
|
||||
? reasoning.map((part) => part.text).join("")
|
||||
: openAICompatibleReasoningContent(message.native?.openaiCompatible),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -343,7 +343,10 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
|
||||
const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Responses", message)
|
||||
const previous = input.at(-1)
|
||||
if (previous && "role" in previous && previous.role === "user")
|
||||
input[input.length - 1] = { role: "user", content: [...previous.content, { type: "input_text", text: part.text }] }
|
||||
input[input.length - 1] = {
|
||||
role: "user",
|
||||
content: [...previous.content, { type: "input_text", text: part.text }],
|
||||
}
|
||||
else input.push({ role: "user", content: [{ type: "input_text", text: part.text }] })
|
||||
continue
|
||||
}
|
||||
@@ -397,7 +400,8 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
|
||||
if (part.type === "tool-result" && part.providerExecuted === true) {
|
||||
flushText()
|
||||
const itemID = hostedToolItemID(part)
|
||||
if (store !== false && itemID && !hostedToolReferences.has(itemID)) input.push({ type: "item_reference", id: itemID })
|
||||
if (store !== false && itemID && !hostedToolReferences.has(itemID))
|
||||
input.push({ type: "item_reference", id: itemID })
|
||||
if (itemID) hostedToolReferences.add(itemID)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -105,7 +105,8 @@ export const parseJson = (route: string, input: string, message: string) =>
|
||||
*/
|
||||
export const joinText = (parts: ReadonlyArray<{ readonly text: string }>) => parts.map((part) => part.text).join("\n")
|
||||
|
||||
const escapeSystemUpdateText = (text: string) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")
|
||||
const escapeSystemUpdateText = (text: string) =>
|
||||
text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")
|
||||
|
||||
/**
|
||||
* Stable fallback representation for chronological `Message.system(...)`
|
||||
|
||||
@@ -193,7 +193,8 @@ export const ToolOutput = Object.assign(
|
||||
type: "content",
|
||||
value: output.content.map((item) => {
|
||||
if (item.type === "text") return { type: "text", text: item.text }
|
||||
if (item.source.type !== "data") throw new Error("Unmaterialized tool file source reached provider conversion")
|
||||
if (item.source.type !== "data")
|
||||
throw new Error("Unmaterialized tool file source reached provider conversion")
|
||||
return { type: "media", mediaType: item.mime, data: item.source.data, filename: item.name }
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { Effect } from "effect"
|
||||
import { LLMEvent, type ToolCallPart, ToolFailure, ToolOutput, ToolResultValue, type ToolOutput as ToolOutputType, type ToolResultValue as ToolResultValueType } from "./schema"
|
||||
import {
|
||||
LLMEvent,
|
||||
type ToolCallPart,
|
||||
ToolFailure,
|
||||
ToolOutput,
|
||||
ToolResultValue,
|
||||
type ToolOutput as ToolOutputType,
|
||||
type ToolResultValue as ToolResultValueType,
|
||||
} from "./schema"
|
||||
import { type AnyTool, type Tools } from "./tool"
|
||||
|
||||
export interface ToolSettlement {
|
||||
@@ -52,11 +60,7 @@ const decodeAndExecute = (tool: AnyTool, call: ToolCallPart): Effect.Effect<Tool
|
||||
),
|
||||
)
|
||||
|
||||
const result = (
|
||||
call: ToolCallPart,
|
||||
value: ToolResultValueType | ToolSettlement,
|
||||
error?: unknown,
|
||||
): DispatchResult => {
|
||||
const result = (call: ToolCallPart, value: ToolResultValueType | ToolSettlement, error?: unknown): DispatchResult => {
|
||||
const settlement = ToolResultValue.is(value) ? { result: value } : value
|
||||
return {
|
||||
result: settlement.result,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Effect, JsonSchema, Schema } from "effect"
|
||||
import type { ToolCallPart, ToolContent, ToolDefinition as ToolDefinitionClass, ToolOutput as ToolOutputType } from "./schema"
|
||||
import type {
|
||||
ToolCallPart,
|
||||
ToolContent,
|
||||
ToolDefinition as ToolDefinitionClass,
|
||||
ToolOutput as ToolOutputType,
|
||||
} from "./schema"
|
||||
import { ToolDefinition, ToolFailure, ToolOutput, toolText } from "./schema"
|
||||
|
||||
/**
|
||||
@@ -51,7 +56,11 @@ export interface Tool<Parameters extends ToolSchema<any>, Success extends ToolSc
|
||||
/** @internal */
|
||||
readonly _encode: (value: Schema.Schema.Type<Success>) => Effect.Effect<unknown, Schema.SchemaError>
|
||||
/** @internal */
|
||||
readonly _project: (parameters: Schema.Schema.Type<Parameters>, callID: ToolCallPart["id"], output: unknown) => ToolOutputType
|
||||
readonly _project: (
|
||||
parameters: Schema.Schema.Type<Parameters>,
|
||||
callID: ToolCallPart["id"],
|
||||
output: unknown,
|
||||
) => ToolOutputType
|
||||
/** @internal */
|
||||
readonly _legacyResult: boolean
|
||||
/** @internal */
|
||||
|
||||
@@ -117,7 +117,11 @@ const appendText = (
|
||||
) => {
|
||||
const last = content.at(-1)
|
||||
if (last?.type === type) {
|
||||
content[content.length - 1] = { ...last, text: `${last.text}${text}`, providerMetadata: providerMetadata ?? last.providerMetadata }
|
||||
content[content.length - 1] = {
|
||||
...last,
|
||||
text: `${last.text}${text}`,
|
||||
providerMetadata: providerMetadata ?? last.providerMetadata,
|
||||
}
|
||||
return
|
||||
}
|
||||
content.push({ type, text, providerMetadata })
|
||||
|
||||
@@ -136,7 +136,9 @@ describe("llm constructors", () => {
|
||||
})
|
||||
|
||||
test("builds chronological text-only system updates separately from the initial system prompt", () => {
|
||||
const update = Message.system([{ type: "text", text: "Use parameterized SQL.", cache: new CacheHint({ type: "ephemeral" }) }])
|
||||
const update = Message.system([
|
||||
{ type: "text", text: "Use parameterized SQL.", cache: new CacheHint({ type: "ephemeral" }) },
|
||||
])
|
||||
const request = LLM.request({
|
||||
model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
|
||||
system: "Initial operator prompt.",
|
||||
|
||||
@@ -87,7 +87,11 @@ describe("Anthropic Messages route", () => {
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [Message.user("Before."), Message.system("Treat </system-update> literally."), Message.assistant("After.")],
|
||||
messages: [
|
||||
Message.user("Before."),
|
||||
Message.system("Treat </system-update> literally."),
|
||||
Message.assistant("After."),
|
||||
],
|
||||
cache: "none",
|
||||
}),
|
||||
)
|
||||
@@ -127,19 +131,19 @@ describe("Anthropic Messages route", () => {
|
||||
LLMClient.prepare(LLM.request({ model: opus48, messages, cache: "none" })).pipe(Effect.flip)
|
||||
|
||||
expect((yield* placementError([Message.system("First.")])).message).toContain("cannot be the first message")
|
||||
expect((yield* placementError([Message.user("Before."), Message.system("One."), Message.system("Two.")])).message)
|
||||
.toContain("cannot be consecutive")
|
||||
expect((yield* placementError([Message.assistant("Plain."), Message.system("After plain assistant.")])).message)
|
||||
.toContain("must follow a user message, tool result, or assistant server tool use")
|
||||
expect(
|
||||
(
|
||||
yield* placementError([
|
||||
Message.user("Use the tool."),
|
||||
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
|
||||
Message.system("Too early."),
|
||||
Message.tool({ id: "call_1", name: "lookup", result: "Done." }),
|
||||
])
|
||||
).message,
|
||||
(yield* placementError([Message.user("Before."), Message.system("One."), Message.system("Two.")])).message,
|
||||
).toContain("cannot be consecutive")
|
||||
expect(
|
||||
(yield* placementError([Message.assistant("Plain."), Message.system("After plain assistant.")])).message,
|
||||
).toContain("must follow a user message, tool result, or assistant server tool use")
|
||||
expect(
|
||||
(yield* placementError([
|
||||
Message.user("Use the tool."),
|
||||
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
|
||||
Message.system("Too early."),
|
||||
Message.tool({ id: "call_1", name: "lookup", result: "Done." }),
|
||||
])).message,
|
||||
).toContain("cannot appear between a local tool call and its tool result")
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -327,7 +327,10 @@ describe("Bedrock Converse route", () => {
|
||||
}),
|
||||
)
|
||||
expect(prepared.body.messages).toEqual([
|
||||
{ role: "assistant", content: [{ reasoningContent: { reasoningText: { text: "Let me think.", signature: "sig_1" } } }] },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ reasoningContent: { reasoningText: { text: "Let me think.", signature: "sig_1" } } }],
|
||||
},
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -55,12 +55,19 @@ describe("OpenAI Chat route", () => {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [Message.user("Before."), Message.system("Treat <admin> & data literally."), Message.assistant("After.")],
|
||||
messages: [
|
||||
Message.user("Before."),
|
||||
Message.system("Treat <admin> & data literally."),
|
||||
Message.assistant("After."),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.messages).toEqual([
|
||||
{ role: "user", content: "Before.\n<system-update>\nTreat <admin> & data literally.\n</system-update>" },
|
||||
{
|
||||
role: "user",
|
||||
content: "Before.\n<system-update>\nTreat <admin> & data literally.\n</system-update>",
|
||||
},
|
||||
{ role: "assistant", content: "After." },
|
||||
])
|
||||
}),
|
||||
@@ -71,7 +78,12 @@ describe("OpenAI Chat route", () => {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [Message.assistant([{ type: "reasoning", text: "thinking" }, { type: "text", text: "Hello" }])],
|
||||
messages: [
|
||||
Message.assistant([
|
||||
{ type: "reasoning", text: "thinking" },
|
||||
{ type: "text", text: "Hello" },
|
||||
]),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -62,7 +62,11 @@ describe("OpenAI Responses route", () => {
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [Message.user("Before."), Message.system("Treat </system-update> literally."), Message.assistant("After.")],
|
||||
messages: [
|
||||
Message.user("Before."),
|
||||
Message.system("Treat </system-update> literally."),
|
||||
Message.assistant("After."),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { GenerationOptions, LLM, LLMEvent, LLMRequest, LLMResponse, ToolChoice, ToolContent, ToolOutput, toolFileSourceFromUri, toDefinitions } from "../src"
|
||||
import {
|
||||
GenerationOptions,
|
||||
LLM,
|
||||
LLMEvent,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
ToolChoice,
|
||||
ToolContent,
|
||||
ToolOutput,
|
||||
toolFileSourceFromUri,
|
||||
toDefinitions,
|
||||
} from "../src"
|
||||
import { Auth, LLMClient } from "../src/route"
|
||||
import * as AnthropicMessages from "../src/protocols/anthropic-messages"
|
||||
import * as OpenAIChat from "../src/protocols/openai-chat"
|
||||
@@ -188,10 +199,12 @@ describe("LLMClient tools", () => {
|
||||
execute: () => Effect.succeed({ ok: true }),
|
||||
})
|
||||
|
||||
expect((yield* ToolRuntime.dispatch({ text }, LLMEvent.toolCall({ id: "call_text", name: "text", input: {} }))).output)
|
||||
.toEqual({ structured: "hello", content: [{ type: "text", text: "hello" }] })
|
||||
expect((yield* ToolRuntime.dispatch({ json }, LLMEvent.toolCall({ id: "call_json", name: "json", input: {} }))).output)
|
||||
.toEqual({ structured: { ok: true }, content: [] })
|
||||
expect(
|
||||
(yield* ToolRuntime.dispatch({ text }, LLMEvent.toolCall({ id: "call_text", name: "text", input: {} }))).output,
|
||||
).toEqual({ structured: "hello", content: [{ type: "text", text: "hello" }] })
|
||||
expect(
|
||||
(yield* ToolRuntime.dispatch({ json }, LLMEvent.toolCall({ id: "call_json", name: "json", input: {} }))).output,
|
||||
).toEqual({ structured: { ok: true }, content: [] })
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -204,12 +217,16 @@ describe("LLMClient tools", () => {
|
||||
source: { type: "data", data: "AAAA" },
|
||||
mime: "image/png",
|
||||
})
|
||||
expect(decode({ type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" })).toEqual({
|
||||
expect(
|
||||
decode({ type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" }),
|
||||
).toEqual({
|
||||
type: "file",
|
||||
source: { type: "url", url: "https://example.test/image.png" },
|
||||
mime: "image/png",
|
||||
})
|
||||
expect(decode({ type: "file", source: { type: "file", uri: "file:///tmp/image.png" }, mime: "image/png" })).toEqual({
|
||||
expect(
|
||||
decode({ type: "file", source: { type: "file", uri: "file:///tmp/image.png" }, mime: "image/png" }),
|
||||
).toEqual({
|
||||
type: "file",
|
||||
source: { type: "file", uri: "file:///tmp/image.png" },
|
||||
mime: "image/png",
|
||||
@@ -226,16 +243,29 @@ describe("LLMClient tools", () => {
|
||||
).toEqual({ type: "content", value: [{ type: "media", mediaType: "image/png", data: "AAAA" }] })
|
||||
expect(
|
||||
ToolOutput.toResultValue(
|
||||
ToolOutput.make({}, [{ type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" }]),
|
||||
ToolOutput.make({}, [
|
||||
{ type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" },
|
||||
]),
|
||||
),
|
||||
).toEqual({ type: "error", value: 'Tool file source "url" must be materialized to inline data before provider conversion' })
|
||||
).toEqual({
|
||||
type: "error",
|
||||
value: 'Tool file source "url" must be materialized to inline data before provider conversion',
|
||||
})
|
||||
expect(
|
||||
ToolOutput.toResultValue(
|
||||
ToolOutput.make({}, [{ type: "file", source: { type: "file", uri: "file:///tmp/image.png" }, mime: "image/png" }]),
|
||||
ToolOutput.make({}, [
|
||||
{ type: "file", source: { type: "file", uri: "file:///tmp/image.png" }, mime: "image/png" },
|
||||
]),
|
||||
),
|
||||
).toEqual({ type: "error", value: 'Tool file source "file" must be materialized to inline data before provider conversion' })
|
||||
).toEqual({
|
||||
type: "error",
|
||||
value: 'Tool file source "file" must be materialized to inline data before provider conversion',
|
||||
})
|
||||
expect(toolFileSourceFromUri("data:image/png;base64,AAAA")).toEqual({ type: "data", data: "AAAA" })
|
||||
expect(toolFileSourceFromUri("https://example.test/image.png")).toEqual({ type: "url", url: "https://example.test/image.png" })
|
||||
expect(toolFileSourceFromUri("https://example.test/image.png")).toEqual({
|
||||
type: "url",
|
||||
url: "https://example.test/image.png",
|
||||
})
|
||||
expect(toolFileSourceFromUri("file:///tmp/image.png")).toEqual({ type: "file", uri: "file:///tmp/image.png" })
|
||||
expect(() => toolFileSourceFromUri("opaque-value")).toThrow("Unsupported tool file URI")
|
||||
expect(() =>
|
||||
|
||||
@@ -27,7 +27,9 @@ Tool.make({
|
||||
parameters: Schema.Struct({ city: Schema.String }),
|
||||
success: Schema.Struct({ forecast: Schema.NumberFromString }),
|
||||
execute: () => Effect.succeed({ forecast: 1 }),
|
||||
toModelOutput: ({ callID, parameters, output }) => [{ type: "text", text: `${callID}:${parameters.city}:${output.forecast}` }],
|
||||
toModelOutput: ({ callID, parameters, output }) => [
|
||||
{ type: "text", text: `${callID}:${parameters.city}:${output.forecast}` },
|
||||
],
|
||||
})
|
||||
|
||||
LLM.stream(request)
|
||||
|
||||
Reference in New Issue
Block a user