From 66382ed980cba30bfd331b16d5c19b41e5c794e5 Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Sat, 18 Oct 2025 14:51:21 -0700 Subject: [PATCH] AIX: Anthropic: Search/Fetch - done NOTEs: this works without saving the server-side tool invocation and the subsequent responses to AIX particles, and consequently to DMessageFragments of the opportune type. -> Shall do it with execution graph fragments. --- .../adapters/anthropic.messageCreate.ts | 2 +- .../chatGenerate/parsers/anthropic.parser.ts | 40 +++++++++++++++++++ .../dispatch/wiretypes/anthropic.wiretypes.ts | 9 ++++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/modules/aix/server/dispatch/chatGenerate/adapters/anthropic.messageCreate.ts b/src/modules/aix/server/dispatch/chatGenerate/adapters/anthropic.messageCreate.ts index 354acba1b..b3c3397f4 100644 --- a/src/modules/aix/server/dispatch/chatGenerate/adapters/anthropic.messageCreate.ts +++ b/src/modules/aix/server/dispatch/chatGenerate/adapters/anthropic.messageCreate.ts @@ -154,7 +154,7 @@ export function aixToAnthropicMessageCreate(model: AixAPI_Model, _chatGenerate: hostedTools.push({ type: 'web_search_20250305', name: 'web_search', - max_uses: 5, // Allow up to 5 progressive searches + max_uses: 10, // Allow up to 10 progressive searches // FIXME: HARDCODED }); } diff --git a/src/modules/aix/server/dispatch/chatGenerate/parsers/anthropic.parser.ts b/src/modules/aix/server/dispatch/chatGenerate/parsers/anthropic.parser.ts index ebdedda04..53a09410c 100644 --- a/src/modules/aix/server/dispatch/chatGenerate/parsers/anthropic.parser.ts +++ b/src/modules/aix/server/dispatch/chatGenerate/parsers/anthropic.parser.ts @@ -26,6 +26,7 @@ import { AnthropicWire_API_Message_Create } from '../../wiretypes/anthropic.wire * - 'input_json_delta': Partial JSON strings for tool_use and server_tool_use inputs. * - 'thinking_delta': Incremental thinking content updates. * - 'signature_delta': Signature for thinking blocks. + * - 'citations_delta': Citations that stream incrementally for text blocks. * * Client Tools vs Server Tools * @@ -122,6 +123,7 @@ export function createAnthropicMessageParser(): ChatGenerateParseFunction { switch (content_block.type) { case 'text': pt.appendText(content_block.text); + // Note: In streaming mode, citations arrive via citations_delta events, not on content_block_start break; case 'thinking': @@ -274,6 +276,27 @@ export function createAnthropicMessageParser(): ChatGenerateParseFunction { throw new Error('Unexpected signature delta'); break; + case 'citations_delta': + // Citations arrive incrementally during streaming - add to current text block + if (contentBlock.type === 'text') { + const citation = delta.citation; + if (citation.type === 'web_search_result_location') { + // Web search citation from server-side search + pt.appendUrlCitation( + citation.title || citation.url, + citation.url, + undefined, // citationNumber + undefined, // startIndex + undefined, // endIndex + citation.cited_text, // textSnippet + undefined // pubTs + ); + } + // TODO: Handle other citation types (char_location, page_location, content_block_location, search_result_location) + } else + throw new Error('Unexpected citations_delta on non-text block'); + break; + // note: redacted_thinking doesn't have deltas, only start (with payload) and stop default: @@ -368,6 +391,23 @@ export function createAnthropicMessageParserNS(): ChatGenerateParseFunction { switch (contentBlock.type) { case 'text': pt.appendText(contentBlock.text); + // Handle citations if present (non-streaming mode has all citations attached) + if (contentBlock.citations && Array.isArray(contentBlock.citations)) { + for (const citation of contentBlock.citations) { + if (citation.type === 'web_search_result_location') { + pt.appendUrlCitation( + citation.title || citation.url, + citation.url, + undefined, // citationNumber + undefined, // startIndex + undefined, // endIndex + citation.cited_text, // textSnippet + undefined // pubTs + ); + } + // TODO: Handle other citation types (char_location, page_location, content_block_location, search_result_location) + } + } break; case 'thinking': diff --git a/src/modules/aix/server/dispatch/wiretypes/anthropic.wiretypes.ts b/src/modules/aix/server/dispatch/wiretypes/anthropic.wiretypes.ts index c34114384..d76ff8007 100644 --- a/src/modules/aix/server/dispatch/wiretypes/anthropic.wiretypes.ts +++ b/src/modules/aix/server/dispatch/wiretypes/anthropic.wiretypes.ts @@ -42,7 +42,7 @@ export namespace AnthropicWire_Blocks { }); /** Citations schema used in both input and output. Output includes a file_id field for document citations, which input will omit. */ - const _TextBlockCitations_schema = z.discriminatedUnion('type', [ + export const _TextBlockCitations_schema = z.discriminatedUnion('type', [ // PDF citation (page location) z.object({ type: z.literal('page_location'), @@ -73,7 +73,7 @@ export namespace AnthropicWire_Blocks { end_block_index: z.number(), file_id: z.string().nullish(), // Present in response only }), - // Web search result citation + // Web search result citation - produced by the hosted web_search tool z.object({ type: z.literal('web_search_result_location'), cited_text: z.string(), @@ -848,6 +848,11 @@ export namespace AnthropicWire_API_Message_Create { type: z.literal('signature_delta'), signature: z.string(), }), + z.object({ + // created by the hosted web_search tool, at least, in which case the citation is: Extract + type: z.literal('citations_delta'), + citation: AnthropicWire_Blocks._TextBlockCitations_schema, + }), ]), });