Skip to content

[2/3] feat(ai): Add extension support in ai crate#726

Merged
ehayes2000 merged 7 commits intomainfrom
ehayesdev/m-5447-ai-crate-api-extension-support
Dec 29, 2025
Merged

[2/3] feat(ai): Add extension support in ai crate#726
ehayes2000 merged 7 commits intomainfrom
ehayesdev/m-5447-ai-crate-api-extension-support

Conversation

@ehayes2000
Copy link
Copy Markdown
Contributor

@ehayes2000 ehayes2000 commented Dec 19, 2025

  • Add trait to support streams that extend openai stream with additional
    items
  • Rename client -> tool_loop
  • Update tool loop to use new client trait and handle extended streams
  • Implement extension client for existing clients (placeholder for
    anthropic)

ExtendedClient trait is the core change of this PR. All changes are made around this update.

@ehayes2000 ehayes2000 requested a review from a team as a code owner December 19, 2025 16:47
@linear
Copy link
Copy Markdown

linear bot commented Dec 19, 2025

@ehayes2000 ehayes2000 changed the title feat(ai): Add extension support in ai crate [2/3] feat(ai): Add extension support in ai crate Dec 21, 2025
Copy link
Copy Markdown
Contributor

@synoet synoet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly good, just please remove the panic.

yield Ok(StreamPart::ToolCall(call));
yield Ok(PartOrExt::Part(StreamPart::ToolCall(call)));
} else {
panic!("Failed to try from")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove the panic

while let Some(item) = stream.next().await {
if let Err(e) = item {
yield Err(e);
break;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional that the outer loop continues after the stream errors ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No

impl Default for AnthropicClient {
fn default() -> Self {
Self::new()
Self::new(AnthropicRequestExtensions(vec![]))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Why does this impl have no default tools ? But the ToolLoop by default includes web search. Can we unify them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think its correct to include web search by default any time a connection to anthropic is created. If this client is used for insights, completions, markdown edits, or explain it would be strange to see AI search the web. The tool_loop/chat interface (a rename of the preexisting ai/tools/client/chat interface) is constructed explicitly for user chats so it's appropriate to instantiate chat business requirements with this interface.

Comment on lines +110 to +118
ref part @ PartOrExt::Part(ref p) => {
yield Ok(p.to_owned());
stream_parts.push(part.to_owned());
}
ref part @ PartOrExt::Ext(ref e) => {
if let Some(p) = self.client.handle_extension_item(e.to_owned()) {
yield Ok(p);
}
stream_parts.push(part.to_owned());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you read ref but then immediately call to_owned().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm binding references to the the whole part and the destructured p / e because I need owned data for both. If I don't use ref bindings the destructure moves.

Comment on lines +152 to +153
// list of tool responses as openai items that are not returned / yielded
let mut non_yielding_responses = vec![];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused why these are split up, if they are just yielded at the end anyway ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea it's a bit ugly. They're split because standard tool responses are trigger tool execution to get a response then trigger another round trip to the provider. Extension callbacks don't do either, but still need to be be transformed and appended to build the next request which will either be triggered by a user request or a standard tool call.

@ehayes2000 ehayes2000 requested a review from a team as a code owner December 29, 2025 17:31
Base automatically changed from ehayesdev/m-5446-anthropic-crate-support to main December 29, 2025 17:36
ehayes2000 and others added 5 commits December 29, 2025 12:00
- Add definition for web search server tool
- Add example to test anthropic web search
- Add extension pattern to openai bindings
- Add trait to support streams that extend openai stream with additional
  items
- Rename `client` -> `tool_loop`
- Update tool loop to use new client trait and handle extended streams
- Implement extension client for existing clients (placeholder for
  anthropic)
@ehayes2000 ehayes2000 force-pushed the ehayesdev/m-5447-ai-crate-api-extension-support branch from df56c81 to a0f178e Compare December 29, 2025 19:10
@ehayes2000 ehayes2000 merged commit b24d684 into main Dec 29, 2025
37 checks passed
@ehayes2000 ehayes2000 deleted the ehayesdev/m-5447-ai-crate-api-extension-support branch December 29, 2025 19:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants