-
-
Notifications
You must be signed in to change notification settings - Fork 426
Refactor the Effect AI SDKs #5469
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
c8c0ddd
wip
IMax153 0966d01
wip
IMax153 d8956b1
wip
IMax153 7b5d124
wip
IMax153 5e35a9f
rename content to response
IMax153 d90bda7
refactor responses to be more ergonomic on the provider side
IMax153 14412c1
implement provider-defined tools
IMax153 8b78088
comprehensive documentation for base ai sdk modules
IMax153 b0c725c
cleanup language model return types
IMax153 afe2bcd
start work on openai port to new sdk protocol
IMax153 3b8b923
add patch for includable
IMax153 a101fb8
update amazon bedrock provider for new sdk protocol
IMax153 8ec0f77
start conversion of google provider to new sdk protocol
IMax153 f77754c
finish porting providers
IMax153 aa1b1b4
handle error parts
IMax153 6f14648
add detail to changeset
IMax153 826efbd
cleanup
IMax153 8e1c6b0
fix item id handling in openai responses api
IMax153 29a6755
add back embedMany
IMax153 e3f2114
remove weird formatting in McpServer
IMax153 d0924c3
expose getProviderDefinedToolName as non-internal method
IMax153 c4494b3
remove doc instructions from repo
IMax153 900d378
fix tool call parts type util
IMax153 cffc415
fix execution plan doc
IMax153 e1e6240
update changeset
IMax153 315f1af
fix unsafeSetProviderMetadata types
IMax153 e089dcf
fix openai tool names
IMax153 cb4129f
use module augmentation for provider options
IMax153 ca01195
use module augmentation for provider metadata
IMax153 b9eb1e1
cleanup embedding models
IMax153 8785bac
fix types of provider metadata
IMax153 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,247 @@ | ||
| --- | ||
| "@effect/ai-amazon-bedrock": minor | ||
| "@effect/ai-anthropic": minor | ||
| "@effect/ai-google": minor | ||
| "@effect/ai-openai": minor | ||
| "@effect/ai": minor | ||
| --- | ||
|
|
||
| Refactor the Effect AI SDK and associated provider packages | ||
|
|
||
| This pull request contains a complete refactor of the base Effect AI SDK package | ||
| as well as the associated provider integration packages to improve flexibility | ||
| and enhance ergonomics. Major changes are outlined below. | ||
|
|
||
| ## Modules | ||
|
|
||
| All modules in the base Effect AI SDK have had the leading `Ai` prefix dropped | ||
| from their name (except for the `AiError` module). | ||
|
|
||
| For example, the `AiLanguageModel` module is now the `LanguageModel` module. | ||
|
|
||
| In addition, the `AiInput` module has been renamed to the `Prompt` module. | ||
|
|
||
| ## Prompts | ||
|
|
||
| The `Prompt` module has been completely redesigned with flexibility in mind. | ||
|
|
||
| The `Prompt` module now supports building a prompt using either the constructors | ||
| exposed from the `Prompt` module, or using raw prompt content parts / messages, | ||
| which should be familiar to those coming from other AI SDKs. | ||
|
|
||
| In addition, the `system` option has been removed from all `LanguageModel` methods | ||
| and must now be provided as part of the prompt. | ||
|
|
||
| **Prompt Constructors** | ||
|
|
||
| ```ts | ||
| import { LanguageModel, Prompt } from "@effect/ai" | ||
|
|
||
| const textPart = Prompt.makePart("text", { | ||
| text: "What is machine learning?" | ||
| }) | ||
|
|
||
| const userMessage = Prompt.makeMessage("user", { | ||
| content: [textPart] | ||
| }) | ||
|
|
||
| const systemMessage = Prompt.makeMessage("system", { | ||
| content: "You are an expert in machine learning" | ||
| }) | ||
|
|
||
| const program = LanguageModel.generateText({ | ||
| prompt: Prompt.fromMessages([ | ||
| systemMessage, | ||
| userMessage | ||
| ]) | ||
| }) | ||
| ``` | ||
|
|
||
| **Raw Prompt Input** | ||
|
|
||
| ```ts | ||
| import { LanguageModel } from "@effect/ai" | ||
|
|
||
| const program = LanguageModel.generateText({ | ||
| prompt: [ | ||
| { role: "system", content: "You are an expert in machine learning" }, | ||
| { role: "user", content: [{ type: "text", text: "What is machine learning?" }] } | ||
| ] | ||
| }) | ||
| ``` | ||
|
|
||
| **NOTE**: Providing a plain string as a prompt is still supported, and will be converted | ||
| internally into a user message with a single text content part. | ||
|
|
||
| ### Provider-Specific Options | ||
|
|
||
| To support specification of provider-specific options when interacting with large | ||
| language model providers, support has been added for adding provider-specific | ||
| options to the parts of a `Prompt`. | ||
|
|
||
| ```ts | ||
| import { LanguageModel } from "@effect/ai" | ||
| import { AnthropicLanguageModel } from "@effect/ai-anthropic" | ||
|
|
||
| const Claude = AnthropicLanguageModel.model("claude-sonnet-4-20250514") | ||
|
|
||
| const program = LanguageModel.generateText({ | ||
| prompt: [ | ||
| { | ||
| role: "user", | ||
| content: [{ type: "text", text: "What is machine learning?" }], | ||
| options: { | ||
| anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } } | ||
| } | ||
| } | ||
| ] | ||
| }).pipe(Effect.provide(Claude)) | ||
| ``` | ||
|
|
||
| ## Responses | ||
|
|
||
| The `Response` module has also been completely redesigned to support a wider | ||
| variety of response parts, particularly when streaming. | ||
|
|
||
| ### Streaming Responses | ||
|
|
||
| When streaming text via the `LanguageModel.streamText` method, you will now | ||
| receive a stream of content parts instead of a stream of responses, which should | ||
| make it much simpler to filter down the stream to the parts you are interested in. | ||
|
|
||
| In addition, additional content parts will be present in the stream to allow you to track, | ||
| for example, when a text content part starts / ends. | ||
|
|
||
| ### Tool Calls / Tool Call Results | ||
|
|
||
| The decoded parts of a `Response` (as returned by the methods of `LanguageModel`) | ||
| are now fully type-safe on tool calls / tool call results. Filtering the content parts of a | ||
| response to tool calls will narrow the type of the tool call `params` based on the tool | ||
| `name`. Similarly, filtering the response to tool call results will narrow the type of the | ||
| tool call `result` based on the tool `name`. | ||
|
|
||
| ```ts | ||
| import { LanguageModel, Tool, Toolkit } from "@effect/ai" | ||
| import { Effect, Schema } from "effect" | ||
|
|
||
| const DadJokeTool = Tool.make("DadJokeTool", { | ||
| parameters: { topic: Schema.String }, | ||
| success: Schema.Struct({ joke: Schema.String }) | ||
| }) | ||
|
|
||
| const FooTool = Tool.make("FooTool", { | ||
| parameters: { foo: Schema.Number }, | ||
| success: Schema.Struct({ bar: Schema.Boolean }) | ||
| }) | ||
|
|
||
| const MyToolkit = Toolkit.make(DadJokeTool, FooTool) | ||
|
|
||
| const program = Effect.gen(function*() { | ||
| const response = yield* LanguageModel.generateText({ | ||
| prompt: "Tell me a dad joke", | ||
| toolkit: MyToolkit | ||
| }) | ||
|
|
||
| for (const toolCall of response.toolCalls) { | ||
| if (toolCall.name === "DadJokeTool") { | ||
| // ^? "DadJokeTool" | "FooTool" | ||
| toolCall.params | ||
| // ^? { readonly topic: string } | ||
| } | ||
| } | ||
|
|
||
| for (const toolResult of response.toolResults) { | ||
| if (toolResult.name === "DadJokeTool") { | ||
| // ^? "DadJokeTool" | "FooTool" | ||
| toolResult.result | ||
| // ^? { readonly joke: string } | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
|
|
||
| ### Provider Metadata | ||
|
|
||
| As with provider-specific options, provider-specific metadata is now returned as | ||
| part of the response from the large language model provider. | ||
|
|
||
| ```ts | ||
| import { LanguageModel } from "@effect/ai" | ||
| import { AnthropicLanguageModel } from "@effect/ai-anthropic" | ||
| import { Effect } from "effect" | ||
|
|
||
| const Claude = AnthropicLanguageModel.model("claude-4-sonnet-20250514") | ||
|
|
||
| const program = Effect.gen(function*() { | ||
| const response = yield* LanguageModel.generateText({ | ||
| prompt: "What is the meaning of life?" | ||
| }) | ||
|
|
||
| for (const part of response.content) { | ||
| // When metadata **is not** defined for a content part, accessing the | ||
| // provider's key on the part's metadata will return an untyped record | ||
| if (part.type === "text") { | ||
| const metadata = part.metadata.anthropic | ||
| // ^? { readonly [x: string]: unknown } | undefined | ||
| } | ||
| // When metadata **is** defined for a content part, accessing the | ||
| // provider's key on the part's metadata will return typed metadata | ||
| if (part.type === "reasoning") { | ||
| const metadata = part.metadata.anthropic | ||
| // ^? AnthropicReasoningInfo | undefined | ||
| } | ||
| } | ||
| }).pipe(Effect.provide(Claude)) | ||
| ``` | ||
|
|
||
| ## Tool Calls | ||
|
|
||
| The `Tool` module has been enhanced to support provider-defined tools (e.g. | ||
| web search, computer use, etc.). Large language model providers which support | ||
| calling their own tools now have a separate module present in their provider | ||
| integration packages which contain definitions for their tools. | ||
|
|
||
| These provider-defined tools can be included alongside user-defined tools in | ||
| existing `Toolkit`s. Provider-defined tools that require a user-space handler | ||
| will be raise a type error in the associated `Toolkit` layer if no such handler | ||
| is defined. | ||
|
|
||
| ```ts | ||
| import { LanguageModel, Tool, Toolkit } from "@effect/ai" | ||
| import { AnthropicTool } from "@effect/ai-anthropic" | ||
| import { Schema } from "effect" | ||
|
|
||
| const DadJokeTool = Tool.make("DadJokeTool", { | ||
| parameters: { topic: Schema.String }, | ||
| success: Schema.Struct({ joke: Schema.String }) | ||
| }) | ||
|
|
||
| const MyToolkit = Toolkit.make( | ||
| DadJokeTool, | ||
| AnthropicTool.WebSearch_20250305({ max_uses: 1 }) | ||
| ) | ||
|
|
||
| const program = LanguageModel.generateText({ | ||
| prompt: "Search the web for a dad joke", | ||
| toolkit: MyToolkit | ||
| }) | ||
| ``` | ||
|
|
||
| ## AiError | ||
|
|
||
| The `AiError` type has been refactored into a union of different error types | ||
| which can be raised by the Effect AI SDK. The goal of defining separate error | ||
| types is to allow providing the end-user with more granular information about | ||
| the error that occurred. | ||
|
|
||
| For now, the following errors have been defined. More error types may be added | ||
| over time based upon necessity / use case. | ||
|
|
||
| ```ts | ||
| type AiError = | ||
| | HttpRequestError, | ||
| | HttpResponseError, | ||
| | MalformedInput, | ||
| | MalformedOutput, | ||
| | UnknownError | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great to have the
OverloadedErrorbe extracted out into a separate caseUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because different providers implement these errors differently, I'm keeping the
AiErrorunion intentionally lean for now. You should be able to get all the information about why a request / response failed as well as the associated headers and whatnot via theHttpRequestError/HttpResponseErrorcases.That being said, I'm open to re-evaluating what error cases we expose after more folks are able to test / provide feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true, but having this kind of error unification would be valuable addition to the library. Providers have different errors, but most users would want to implement retry on overloads, for example.