Skip to content

Commit 0e93913

Browse files
committed
Fix tool calls and other misc issues
Update anthropic sdk; tools are GA in the latest release.
1 parent 2092259 commit 0e93913

File tree

4 files changed

+271
-119
lines changed

4 files changed

+271
-119
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/anthropic/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"author": "TheFireCo",
2626
"license": "Apache-2.0",
2727
"dependencies": {
28-
"@anthropic-ai/sdk": "^0.21.0",
28+
"@anthropic-ai/sdk": "^0.22.0",
2929
"zod": "^3.23.8"
3030
},
3131
"peerDependencies": {

plugins/anthropic/src/claude.ts

Lines changed: 122 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Message } from '@genkit-ai/ai';
17+
import { Message as GenkitMessage } from '@genkit-ai/ai';
1818
import {
19+
GenerateResponseData,
1920
GenerationCommonConfigSchema,
2021
ModelAction,
2122
defineModel,
@@ -30,8 +31,21 @@ import {
3031
} from '@genkit-ai/ai/model';
3132
import Anthropic from '@anthropic-ai/sdk';
3233
import z from 'zod';
34+
import {
35+
type ImageBlockParam,
36+
type TextBlock,
37+
type TextBlockParam,
38+
type MessageCreateParams,
39+
type Tool,
40+
type ToolResultBlockParam,
41+
type ContentBlock,
42+
type Message,
43+
type MessageParam,
44+
type MessageStreamEvent,
45+
type ToolUseBlockParam,
46+
} from '@anthropic-ai/sdk/resources/messages.mjs';
3347

34-
const AnthropicConfigSchema = GenerationCommonConfigSchema.extend({
48+
export const AnthropicConfigSchema = GenerationCommonConfigSchema.extend({
3549
tool_choice: z
3650
.union([
3751
z.object({
@@ -123,7 +137,7 @@ export const SUPPORTED_CLAUDE_MODELS: Record<
123137
function toAnthropicRole(
124138
role: Role,
125139
toolMessageType?: 'tool_use' | 'tool_result'
126-
): Anthropic.Beta.Tools.ToolsBetaMessageParam['role'] {
140+
): MessageParam['role'] {
127141
switch (role) {
128142
case 'user':
129143
return 'user';
@@ -167,34 +181,41 @@ const extractDataFromBase64Url = (
167181
*/
168182
export function toAnthropicToolResponseContent(
169183
part: Part
170-
): Anthropic.TextBlockParam | Anthropic.ImageBlockParam {
184+
): TextBlockParam | ImageBlockParam {
185+
if (!part.toolResponse) {
186+
throw Error(
187+
`Invalid genkit part provided to toAnthropicToolResponseContent: ${JSON.stringify(
188+
part
189+
)}.`
190+
);
191+
}
171192
const isMedia = isMediaObject(part.toolResponse?.output);
172193
const isString = typeof part.toolResponse?.output === 'string';
173-
if (!isMedia && !isString) {
174-
throw Error(
175-
`Invalid genkit part provided to toAnthropicToolResponseContent: ${part}.`
194+
let base64Data;
195+
if (isMedia) {
196+
base64Data = extractDataFromBase64Url(
197+
(part.toolResponse?.output as Media).url
176198
);
199+
} else if (isString) {
200+
base64Data = extractDataFromBase64Url(part.toolResponse?.output as string);
177201
}
178-
const base64Data = extractDataFromBase64Url(
179-
isMedia
180-
? (part.toolResponse?.output as Media).url
181-
: (part.toolResponse?.output as string)
182-
);
183-
// @ts-expect-error TODO: improve these types
184202
return base64Data
185203
? {
186204
type: 'image',
187205
source: {
188206
type: 'base64',
189207
data: base64Data.data,
190208
media_type:
191-
(part.toolResponse?.output as Media)?.contentType ??
209+
((part.toolResponse?.output as Media)
210+
?.contentType as ImageBlockParam.Source['media_type']) ??
192211
base64Data.contentType,
193212
},
194213
}
195214
: {
196215
type: 'text',
197-
text: part.toolResponse?.output as string,
216+
text: isString
217+
? (part.toolResponse?.output as string)
218+
: JSON.stringify(part.toolResponse?.output),
198219
};
199220
}
200221

@@ -206,11 +227,7 @@ export function toAnthropicToolResponseContent(
206227
*/
207228
export function toAnthropicMessageContent(
208229
part: Part
209-
):
210-
| Anthropic.TextBlock
211-
| Anthropic.ImageBlockParam
212-
| Anthropic.Beta.Tools.ToolUseBlockParam
213-
| Anthropic.Beta.Tools.ToolResultBlockParam {
230+
): TextBlock | ImageBlockParam | ToolUseBlockParam | ToolResultBlockParam {
214231
if (part.text) {
215232
return {
216233
type: 'text',
@@ -262,20 +279,18 @@ export function toAnthropicMessageContent(
262279
*/
263280
export function toAnthropicMessages(messages: MessageData[]): {
264281
system?: string;
265-
messages: Anthropic.Beta.Tools.ToolsBetaMessageParam[];
282+
messages: MessageParam[];
266283
} {
267284
const system =
268285
messages[0]?.role === 'system' ? messages[0].content?.[0]?.text : undefined;
269286
const messagesToIterate = system ? messages.slice(1) : messages;
270-
const anthropicMsgs: Anthropic.Beta.Tools.ToolsBetaMessageParam[] = [];
287+
const anthropicMsgs: MessageParam[] = [];
271288
for (const message of messagesToIterate) {
272-
const msg = new Message(message);
289+
const msg = new GenkitMessage(message);
273290
const content = msg.content.map(toAnthropicMessageContent);
274291
const toolMessageType = content.find(
275292
(c) => c.type === 'tool_use' || c.type === 'tool_result'
276-
) as
277-
| Anthropic.Beta.Tools.ToolUseBlockParam
278-
| Anthropic.Beta.Tools.ToolResultBlockParam;
293+
) as ToolUseBlockParam | ToolResultBlockParam;
279294
const role = toAnthropicRole(message.role, toolMessageType?.type);
280295
anthropicMsgs.push({
281296
role: role,
@@ -290,19 +305,16 @@ export function toAnthropicMessages(messages: MessageData[]): {
290305
* @param tool The Genkit ToolDefinition to convert.
291306
* @returns The converted Anthropic Tool object.
292307
*/
293-
export function toAnthropicTool(
294-
tool: ToolDefinition
295-
): Anthropic.Beta.Tools.Tool {
308+
export function toAnthropicTool(tool: ToolDefinition): Tool {
296309
return {
297310
name: tool.name,
298311
description: tool.description,
299-
input_schema:
300-
tool.inputSchema as Anthropic.Beta.Tools.Messages.Tool.InputSchema,
312+
input_schema: tool.inputSchema as Tool.InputSchema,
301313
};
302314
}
303315

304316
const finishReasonMap: Record<
305-
NonNullable<Anthropic.Beta.Tools.ToolsBetaMessage['stop_reason']>,
317+
NonNullable<Message['stop_reason']>,
306318
CandidateData['finishReason']
307319
> = {
308320
end_turn: 'stop',
@@ -312,76 +324,88 @@ const finishReasonMap: Record<
312324
};
313325

314326
/**
315-
* Converts an Anthropic content block to a Genkit CandidateData object.
316-
* @param choice The Anthropic content block to convert.
317-
* @param index The index of the content block.
318-
* @param stopReason The reason the content block generation stopped.
319-
* @returns The converted Genkit CandidateData object.
327+
* Converts an Anthropic content block to a Genkit Part object.
328+
* @param contentBlock The Anthropic content block to convert.
329+
* @returns The converted Genkit Part object.
320330
*/
321-
function fromAnthropicContentBlock(
322-
choice: Anthropic.Beta.Tools.Messages.ToolsBetaContentBlock,
323-
index: number,
324-
stopReason: Anthropic.Beta.Tools.Messages.ToolsBetaMessage['stop_reason']
325-
): CandidateData {
326-
return {
327-
index,
328-
finishReason: (stopReason && finishReasonMap[stopReason]) || 'other',
329-
message:
330-
choice.type === 'text'
331-
? {
332-
role: 'model',
333-
content: [{ text: choice.text }],
334-
}
335-
: {
336-
role: 'tool',
337-
content: [
338-
{
339-
toolRequest: {
340-
ref: choice.id,
341-
name: choice.name,
342-
input: choice.input,
343-
},
344-
},
345-
],
346-
},
347-
};
331+
function fromAnthropicContentBlock(contentBlock: ContentBlock): Part {
332+
return contentBlock.type === 'tool_use'
333+
? {
334+
toolRequest: {
335+
ref: contentBlock.id,
336+
name: contentBlock.name,
337+
input: contentBlock.input,
338+
},
339+
}
340+
: { text: contentBlock.text };
348341
}
349342

350343
/**
351-
* Converts an Anthropic message stream event to a Genkit CandidateData object.
352-
* @param choice The Anthropic message stream event to convert.
353-
* @returns The converted Genkit CandidateData object if the event is a content block start or delta, otherwise undefined.
344+
* Converts an Anthropic message stream event to a Genkit Part object.
345+
* @param event The Anthropic message stream event to convert.
346+
* @returns The converted Genkit Part object if the event is a content block
347+
* start or delta, otherwise undefined.
354348
*/
355349
function fromAnthropicContentBlockChunk(
356-
choice: Anthropic.Beta.Tools.Messages.ToolsBetaMessageStreamEvent
357-
): CandidateData | undefined {
350+
event: MessageStreamEvent
351+
): Part | undefined {
358352
if (
359-
choice.type !== 'content_block_start' &&
360-
choice.type !== 'content_block_delta'
353+
event.type !== 'content_block_start' &&
354+
event.type !== 'content_block_delta'
361355
) {
362356
return;
363357
}
364-
const choiceField =
365-
choice.type === 'content_block_start' ? 'content_block' : 'delta';
358+
const eventField =
359+
event.type === 'content_block_start' ? 'content_block' : 'delta';
360+
return event[eventField].type === 'text'
361+
? {
362+
text: event[eventField].text,
363+
}
364+
: {
365+
toolRequest: {
366+
ref: event[eventField].id,
367+
name: event[eventField].name,
368+
input: event[eventField].input,
369+
},
370+
};
371+
}
372+
373+
function fromAnthropicStopReason(
374+
reason: Message['stop_reason']
375+
): CandidateData['finishReason'] {
376+
switch (reason) {
377+
case 'max_tokens':
378+
return 'length';
379+
case 'end_turn':
380+
// fall through
381+
case 'stop_sequence':
382+
// fall through
383+
case 'tool_use':
384+
return 'stop';
385+
case null:
386+
return 'unknown';
387+
default:
388+
return 'other';
389+
}
390+
}
391+
392+
export function fromAnthropicResponse(response: Message): GenerateResponseData {
366393
return {
367-
index: choice.index,
368-
finishReason: 'unknown',
369-
message: {
370-
role: 'model',
371-
content: [
372-
choice[choiceField].type === 'text'
373-
? {
374-
text: choice[choiceField].text,
375-
}
376-
: {
377-
toolRequest: {
378-
ref: choice[choiceField].id,
379-
name: choice[choiceField].name,
380-
input: choice[choiceField].input,
381-
},
382-
},
383-
],
394+
candidates: [
395+
{
396+
index: 0,
397+
finishReason: fromAnthropicStopReason(response.stop_reason),
398+
message: {
399+
role: 'model',
400+
content: response.content.map(fromAnthropicContentBlock),
401+
},
402+
},
403+
],
404+
usage: {
405+
inputTokens: response.usage.input_tokens,
406+
outputTokens: response.usage.output_tokens,
384407
},
408+
custom: response,
385409
};
386410
}
387411

@@ -397,12 +421,12 @@ export function toAnthropicRequestBody(
397421
modelName: string,
398422
request: GenerateRequest<typeof AnthropicConfigSchema>,
399423
stream?: boolean
400-
): Anthropic.Beta.Tools.Messages.MessageCreateParams {
424+
): MessageCreateParams {
401425
const model = SUPPORTED_CLAUDE_MODELS[modelName];
402426
if (!model) throw new Error(`Unsupported model: ${modelName}`);
403427
const { system, messages } = toAnthropicMessages(request.messages);
404428
const mappedModelName = request.config?.version || model.version || modelName;
405-
const body: Anthropic.Beta.Tools.MessageCreateParams = {
429+
const body: MessageCreateParams = {
406430
system,
407431
messages,
408432
tools: request.tools?.map(toAnthropicTool),
@@ -451,35 +475,24 @@ export function claudeModel(
451475
configSchema: model.configSchema,
452476
},
453477
async (request, streamingCallback) => {
454-
let response: Anthropic.Beta.Tools.ToolsBetaMessage;
478+
let response: Message;
455479
const body = toAnthropicRequestBody(name, request, !!streamingCallback);
456480
if (streamingCallback) {
457-
const stream = client.beta.tools.messages.stream(body);
481+
const stream = client.messages.stream(body);
458482
for await (const chunk of stream) {
459483
const c = fromAnthropicContentBlockChunk(chunk);
460484
if (c) {
461485
streamingCallback({
462-
index: c.index,
463-
content: c.message.content,
486+
index: 0,
487+
content: [c],
464488
});
465489
}
466490
}
467491
response = await stream.finalMessage();
468492
} else {
469-
response = (await client.beta.tools.messages.create(
470-
body
471-
)) as Anthropic.Beta.Tools.ToolsBetaMessage;
493+
response = (await client.messages.create(body)) as Message;
472494
}
473-
return {
474-
candidates: response.content.map((content, index) =>
475-
fromAnthropicContentBlock(content, index, response.stop_reason)
476-
),
477-
usage: {
478-
inputTokens: response.usage.input_tokens,
479-
outputTokens: response.usage.output_tokens,
480-
},
481-
custom: response,
482-
};
495+
return fromAnthropicResponse(response);
483496
}
484497
);
485498
}

0 commit comments

Comments
 (0)