Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions packages/typescript/ai-anthropic/src/adapters/summarize.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { BaseSummarizeAdapter } from '@tanstack/ai/adapters'
import { ANTHROPIC_MODELS } from '../model-meta'
import { createAnthropicClient, getAnthropicApiKeyFromEnv } from '../utils'
import type { SummarizationOptions, SummarizationResult } from '@tanstack/ai'
import {
createAnthropicClient,
generateId,
getAnthropicApiKeyFromEnv,
} from '../utils'
import type {
StreamChunk,
SummarizationOptions,
SummarizationResult,
} from '@tanstack/ai'
import type { AnthropicClientConfig } from '../utils'

/**
Expand Down Expand Up @@ -68,6 +76,64 @@ export class AnthropicSummarizeAdapter extends BaseSummarizeAdapter<
}
}

async *summarizeStream(
options: SummarizationOptions,
): AsyncIterable<StreamChunk> {
const systemPrompt = this.buildSummarizationPrompt(options)
const id = generateId(this.name)
const model = options.model
let accumulatedContent = ''
let inputTokens = 0
let outputTokens = 0

const stream = await this.client.messages.create({
model: options.model,
messages: [{ role: 'user', content: options.text }],
system: systemPrompt,
max_tokens: options.maxLength || 500,
temperature: 0.3,
stream: true,
})

for await (const event of stream) {
if (event.type === 'message_start') {
inputTokens = event.message.usage.input_tokens
} else if (event.type === 'content_block_delta') {
if (event.delta.type === 'text_delta') {
const delta = event.delta.text
accumulatedContent += delta
yield {
type: 'content',
id,
model,
timestamp: Date.now(),
delta,
content: accumulatedContent,
role: 'assistant',
}
}
} else if (event.type === 'message_delta') {
outputTokens = event.usage.output_tokens
yield {
type: 'done',
id,
model,
timestamp: Date.now(),
finishReason: event.delta.stop_reason as
| 'stop'
| 'length'
| 'content_filter'
| null,
usage: {
promptTokens: inputTokens,
completionTokens: outputTokens,
totalTokens: inputTokens + outputTokens,
},
}
}
}
}

private buildSummarizationPrompt(options: SummarizationOptions): string {
let prompt = 'You are a professional summarizer. '

Expand Down
18 changes: 7 additions & 11 deletions packages/typescript/ai-anthropic/src/adapters/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { ANTHROPIC_MODELS } from '../model-meta'
import { convertToolsToProviderFormat } from '../tools/tool-converter'
import { validateTextProviderOptions } from '../text/text-provider-options'
import {
convertZodToAnthropicSchema,
createAnthropicClient,
generateId,
getAnthropicApiKeyFromEnv,
Expand Down Expand Up @@ -126,29 +125,26 @@ export class AnthropicTextAdapter extends BaseTextAdapter<
* Generate structured output using Anthropic's tool-based approach.
* Anthropic doesn't have native structured output, so we use a tool with the schema
* and force the model to call it.
* The outputSchema is already JSON Schema (converted in the ai layer).
*/
async structuredOutput(
options: StructuredOutputOptions<AnthropicTextProviderOptions>,
): Promise<StructuredOutputResult<unknown>> {
const { chatOptions, outputSchema } = options

// Convert Zod schema to Anthropic-compatible JSON Schema
const jsonSchema = convertZodToAnthropicSchema(outputSchema)

const requestParams = this.mapCommonOptionsToAnthropic(chatOptions)

// Create a tool that will capture the structured output
// Ensure the schema has type: 'object' as required by Anthropic's SDK
const inputSchema = {
type: 'object' as const,
...jsonSchema,
}

// Anthropic's SDK requires input_schema with type: 'object' literal
const structuredOutputTool = {
name: 'structured_output',
description:
'Use this tool to provide your response in the required structured format.',
input_schema: inputSchema,
input_schema: {
type: 'object' as const,
properties: outputSchema.properties ?? {},
required: outputSchema.required ?? [],
},
}

try {
Expand Down
13 changes: 7 additions & 6 deletions packages/typescript/ai-anthropic/src/tools/custom-tool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { convertZodToAnthropicSchema } from '../utils/schema-converter'
import type { Tool } from '@tanstack/ai'
import type { JSONSchema, Tool } from '@tanstack/ai'
import type { z } from 'zod'
import type { CacheControl } from '../text/text-provider-options'

Expand Down Expand Up @@ -29,10 +28,12 @@ export function convertCustomToolToAdapterFormat(tool: Tool): CustomTool {
const metadata =
(tool.metadata as { cacheControl?: CacheControl | null } | undefined) || {}

// Convert Zod schema to Anthropic-compatible JSON Schema
const jsonSchema = tool.inputSchema
? convertZodToAnthropicSchema(tool.inputSchema)
: { type: 'object', properties: {}, required: [] }
// Tool schemas are already converted to JSON Schema in the ai layer
const jsonSchema = (tool.inputSchema ?? {
type: 'object',
properties: {},
required: [],
}) as JSONSchema

const inputSchema = {
type: 'object' as const,
Expand Down
1 change: 0 additions & 1 deletion packages/typescript/ai-anthropic/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@ export {
getAnthropicApiKeyFromEnv,
type AnthropicClientConfig,
} from './client'
export { convertZodToAnthropicSchema } from './schema-converter'
87 changes: 0 additions & 87 deletions packages/typescript/ai-anthropic/src/utils/schema-converter.ts

This file was deleted.

91 changes: 90 additions & 1 deletion packages/typescript/ai-gemini/src/adapters/summarize.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FinishReason } from '@google/genai'
import {
createGeminiClient,
generateId,
Expand All @@ -6,7 +7,11 @@ import {

import type { GoogleGenAI } from '@google/genai'
import type { SummarizeAdapter } from '@tanstack/ai/adapters'
import type { SummarizationOptions, SummarizationResult } from '@tanstack/ai'
import type {
StreamChunk,
SummarizationOptions,
SummarizationResult,
} from '@tanstack/ai'

/**
* Available Gemini models for summarization
Expand Down Expand Up @@ -114,6 +119,90 @@ export class GeminiSummarizeAdapter implements SummarizeAdapter<
}
}

async *summarizeStream(
options: SummarizationOptions,
): AsyncIterable<StreamChunk> {
const model = options.model || this.defaultModel
const id = generateId('sum')
let accumulatedContent = ''
let inputTokens = 0
let outputTokens = 0

// Build the system prompt based on format
const formatInstructions = this.getFormatInstructions(options.style)
const lengthInstructions = options.maxLength
? ` Keep the summary under ${options.maxLength} words.`
: ''

const systemPrompt = `You are a helpful assistant that summarizes text. ${formatInstructions}${lengthInstructions}`

const result = await this.client.models.generateContentStream({
model,
contents: [
{
role: 'user',
parts: [
{ text: `Please summarize the following:\n\n${options.text}` },
],
},
],
config: {
systemInstruction: systemPrompt,
},
})

for await (const chunk of result) {
// Track usage metadata
if (chunk.usageMetadata) {
inputTokens = chunk.usageMetadata.promptTokenCount ?? inputTokens
outputTokens = chunk.usageMetadata.candidatesTokenCount ?? outputTokens
}

if (chunk.candidates?.[0]?.content?.parts) {
for (const part of chunk.candidates[0].content.parts) {
if (part.text) {
accumulatedContent += part.text
yield {
type: 'content',
id,
model,
timestamp: Date.now(),
delta: part.text,
content: accumulatedContent,
role: 'assistant',
}
}
}
}

// Check for finish reason
const finishReason = chunk.candidates?.[0]?.finishReason
if (
finishReason === FinishReason.STOP ||
finishReason === FinishReason.MAX_TOKENS ||
finishReason === FinishReason.SAFETY
) {
yield {
type: 'done',
id,
model,
timestamp: Date.now(),
finishReason:
finishReason === FinishReason.STOP
? 'stop'
: finishReason === FinishReason.MAX_TOKENS
? 'length'
: 'content_filter',
usage: {
promptTokens: inputTokens,
completionTokens: outputTokens,
totalTokens: inputTokens + outputTokens,
},
}
}
}
}

private getFormatInstructions(
style?: 'paragraph' | 'bullet-points' | 'concise',
): string {
Expand Down
8 changes: 2 additions & 6 deletions packages/typescript/ai-gemini/src/adapters/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { BaseTextAdapter } from '@tanstack/ai/adapters'
import { GEMINI_MODELS } from '../model-meta'
import { convertToolsToProviderFormat } from '../tools/tool-converter'
import {
convertZodToGeminiSchema,
createGeminiClient,
generateId,
getGeminiApiKeyFromEnv,
Expand Down Expand Up @@ -106,16 +105,13 @@ export class GeminiTextAdapter extends BaseTextAdapter<
/**
* Generate structured output using Gemini's native JSON response format.
* Uses responseMimeType: 'application/json' and responseSchema for structured output.
* Converts the Zod schema to JSON Schema format compatible with Gemini's API.
* The outputSchema is already JSON Schema (converted in the ai layer).
*/
async structuredOutput(
options: StructuredOutputOptions<GeminiTextProviderOptions>,
): Promise<StructuredOutputResult<unknown>> {
const { chatOptions, outputSchema } = options

// Convert Zod schema to Gemini-compatible JSON Schema
const jsonSchema = convertZodToGeminiSchema(outputSchema)

const mappedOptions = this.mapCommonOptionsToGemini(chatOptions)

try {
Expand All @@ -125,7 +121,7 @@ export class GeminiTextAdapter extends BaseTextAdapter<
config: {
...mappedOptions.config,
responseMimeType: 'application/json',
responseSchema: jsonSchema,
responseSchema: outputSchema,
},
})

Expand Down
Loading
Loading