This commit is contained in:
centdix
2025-08-29 11:10:06 +00:00
parent 1881ad44cd
commit 5e96efdccc
3 changed files with 172 additions and 263 deletions
@@ -11,16 +11,13 @@ import HistoryManager from './HistoryManager.svelte'
import {
extractCodeFromMarkdown,
getLatestAssistantMessage,
processToolCall,
type DisplayMessage,
type Tool,
type ToolCallbacks,
type ToolDisplayMessage
} from './shared'
import type {
ChatCompletionChunk,
ChatCompletionMessageParam,
ChatCompletionMessageToolCall,
ChatCompletionSystemMessageParam,
ChatCompletionUserMessageParam
} from 'openai/resources/chat/completions.mjs'
@@ -34,7 +31,7 @@ import { loadApiTools } from './api/apiTools'
import { prepareScriptUserMessage } from './script/core'
import { prepareNavigatorUserMessage } from './navigator/core'
import { sendUserToast } from '$lib/toast'
import { getCompletion, getModelContextWindow } from '../lib'
import { getCompletion, getModelContextWindow, parseOpenAICompletion } from '../lib'
import { dfs } from '$lib/components/flows/previousResults'
import { getStringError } from './utils'
import type { FlowModuleState, FlowState } from '$lib/components/flows/flowState'
@@ -380,10 +377,8 @@ class AIChatManager {
}
systemMessage?: ChatCompletionSystemMessageParam
}) => {
let addedMessages: ChatCompletionMessageParam[] = []
try {
let completion: any = null
let addedMessages: ChatCompletionMessageParam[] = []
while (true) {
const systemMessage = systemMessageOverride ?? this.systemMessage
const helpers = this.helpers
@@ -413,99 +408,23 @@ class AIChatManager {
}
this.pendingPrompt = ''
}
completion = await getCompletion(
const completion = await getCompletion(
[systemMessage, ...messages, ...(pendingUserMessage ? [pendingUserMessage] : [])],
abortController,
tools.map((t) => t.def)
)
if (completion) {
const finalToolCalls: Record<number, ChatCompletionChunk.Choice.Delta.ToolCall> = {}
let answer = ''
for await (const chunk of completion) {
if (!('choices' in chunk && chunk.choices.length > 0 && 'delta' in chunk.choices[0])) {
continue
}
const c = chunk as ChatCompletionChunk
const delta = c.choices[0].delta.content
if (delta) {
answer += delta
callbacks.onNewToken(delta)
}
const toolCalls = c.choices[0].delta.tool_calls || []
if (toolCalls.length > 0 && answer) {
// if tool calls are present but we have some textual content already, we need to display it to the user first
callbacks.onMessageEnd()
answer = ''
}
for (const toolCall of toolCalls) {
const { index } = toolCall
let finalToolCall = finalToolCalls[index]
if (!finalToolCall) {
finalToolCalls[index] = toolCall
} else {
if (toolCall.function?.arguments) {
if (!finalToolCall.function) {
finalToolCall.function = toolCall.function
} else {
finalToolCall.function.arguments =
(finalToolCall.function.arguments ?? '') + toolCall.function.arguments
}
}
}
finalToolCall = finalToolCalls[index]
if (finalToolCall?.function) {
const {
function: { name: funcName },
id: toolCallId
} = finalToolCall
if (funcName && toolCallId) {
const tool = tools.find((t) => t.def.function.name === funcName)
if (tool && tool.preAction) {
tool.preAction({ toolCallbacks: callbacks, toolId: toolCallId })
}
}
}
}
}
if (answer) {
const toAdd = { role: 'assistant' as const, content: answer }
addedMessages.push(toAdd)
messages.push(toAdd)
}
callbacks.onMessageEnd()
const toolCalls = Object.values(finalToolCalls).filter(
(toolCall) => toolCall.id !== undefined && toolCall.function?.arguments !== undefined
) as ChatCompletionMessageToolCall[]
if (toolCalls.length > 0) {
const toAdd = {
role: 'assistant' as const,
tool_calls: toolCalls.map((t) => ({
...t,
function: {
...t.function,
arguments: t.function.arguments || '{}'
}
}))
}
messages.push(toAdd)
addedMessages.push(toAdd)
for (const toolCall of toolCalls) {
const messageToAdd = await processToolCall({
tools,
toolCall,
helpers,
toolCallbacks: callbacks
})
messages.push(messageToAdd)
addedMessages.push(messageToAdd)
}
} else {
const continueCompletion = await parseOpenAICompletion(
completion,
callbacks,
messages,
addedMessages,
tools,
helpers
)
if (!continueCompletion) {
break
}
}
@@ -5,8 +5,57 @@ import type {
TextBlockParam,
ToolUnion,
ToolUseBlockParam,
Tool
Tool as AnthropicTool
} from '@anthropic-ai/sdk/resources'
import { getProviderAndCompletionConfig, workspaceAIClients } from '../lib'
import type { Stream } from 'openai/streaming.mjs'
import type { Tool, ToolCallbacks } from './shared'
export async function getAnthropicCompletion(
messages: ChatCompletionMessageParam[],
abortController: AbortController,
tools?: OpenAI.Chat.Completions.ChatCompletionTool[]
) {
const { provider, config } = getProviderAndCompletionConfig({ messages, stream: true })
const { system, messages: anthropicMessages } = convertOpenAIToAnthropicMessages(messages)
const anthropicTools = convertOpenAIToolsToAnthropic(tools)
const anthropicClient = workspaceAIClients.getAnthropicClient()
const anthropicParams = {
model: config.model,
max_tokens: config.max_tokens as number,
messages: anthropicMessages,
...(system && { system }),
...(anthropicTools && { tools: anthropicTools }),
...(typeof config.temperature === 'number' && { temperature: config.temperature })
}
const stream = anthropicClient.messages.stream(anthropicParams, {
signal: abortController.signal,
headers: {
'X-Provider': provider,
'anthropic-version': '2023-06-01'
}
})
return stream
}
export async function parseAnthropicCompletion(
completion: Stream<any>,
callbacks: ToolCallbacks & {
onNewToken: (token: string) => void
onMessageEnd: () => void
},
messages: ChatCompletionMessageParam[],
addedMessages: ChatCompletionMessageParam[],
tools: Tool<any>[],
helpers: any
): Promise<boolean> {
// TODO: Implement Anthropic completion parsing
return true
}
export function convertOpenAIToAnthropicMessages(messages: ChatCompletionMessageParam[]): {
system: TextBlockParam[] | undefined
@@ -125,7 +174,7 @@ export function convertOpenAIToolsToAnthropic(
input_schema: (tool.function.parameters || {
type: 'object',
properties: {}
}) as Tool.InputSchema
}) as AnthropicTool.InputSchema
}))
// Add cache_control to the last tool to cache all tool definitions
@@ -135,134 +184,3 @@ export function convertOpenAIToolsToAnthropic(
return anthropicTools
}
export async function* convertAnthropicStreamToOpenAI(
model: string,
stream: Stream<MessageStreamEvent>
): AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk> {
let currentToolCall: { id: string; name: string; args: string } | null = null
const messageId = `chatcmpl-${Date.now()}`
try {
for await (const event of stream) {
switch (event.type) {
case 'message_start':
yield {
id: messageId,
object: 'chat.completion.chunk',
created: Math.floor(Date.now() / 1000),
model,
choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }]
}
break
case 'content_block_start':
if (event.content_block?.type === 'tool_use') {
currentToolCall = {
id: event.content_block.id,
name: event.content_block.name,
args: ''
}
}
// Text and thinking blocks start here but don't need special handling
break
case 'content_block_delta':
switch (event.delta?.type) {
case 'text_delta':
yield {
id: messageId,
object: 'chat.completion.chunk',
created: Math.floor(Date.now() / 1000),
model,
choices: [{ index: 0, delta: { content: event.delta.text }, finish_reason: null }]
}
break
case 'input_json_delta':
if (currentToolCall) {
currentToolCall.args += event.delta.partial_json
}
break
case 'thinking_delta':
// Skip thinking deltas - they're internal reasoning
break
default:
// Handle unknown delta types gracefully
break
}
break
case 'content_block_stop':
if (currentToolCall) {
// Emit completed tool call
yield {
id: messageId,
object: 'chat.completion.chunk',
created: Math.floor(Date.now() / 1000),
model,
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: currentToolCall.id,
type: 'function' as const,
function: { name: currentToolCall.name, arguments: currentToolCall.args }
}
]
},
finish_reason: null
}
]
}
currentToolCall = null
}
break
case 'message_delta':
const finishReason =
event.delta?.stop_reason === 'end_turn'
? 'stop'
: event.delta?.stop_reason === 'tool_use'
? 'tool_calls'
: event.delta?.stop_reason === 'max_tokens'
? 'length'
: null
yield {
id: messageId,
object: 'chat.completion.chunk',
created: Math.floor(Date.now() / 1000),
model,
choices: [{ index: 0, delta: {}, finish_reason: finishReason }]
}
break
case 'message_stop':
// Stream completed successfully
break
case 'ping':
// Ignore ping events
break
case 'error':
// Handle error events
throw new Error(`Anthropic stream error: ${JSON.stringify(event.error)}`)
default:
// Handle unknown event types gracefully as per Anthropic docs
console.debug('Unknown Anthropic stream event type:', event.type)
break
}
}
} catch (error) {
console.error('Error processing Anthropic stream:', error)
throw error
}
}
+108 -36
View File
@@ -9,10 +9,12 @@ import {
import { buildClientSchema, printSchema } from 'graphql'
import { OpenAI } from 'openai'
import type {
ChatCompletionChunk,
ChatCompletionCreateParams,
ChatCompletionCreateParamsNonStreaming,
ChatCompletionCreateParamsStreaming,
ChatCompletionMessageParam
ChatCompletionMessageParam,
ChatCompletionMessageToolCall
} from 'openai/resources/index.mjs'
import Anthropic from '@anthropic-ai/sdk'
import { get, type Writable } from 'svelte/store'
@@ -20,11 +22,8 @@ import { OpenAPI, ResourceService, type Script } from '../../gen'
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
import { formatResourceTypes } from './utils'
import { z } from 'zod'
import {
convertAnthropicStreamToOpenAI,
convertOpenAIToAnthropicMessages,
convertOpenAIToolsToAnthropic
} from './chat/anthropic'
import type { Stream } from 'openai/streaming.mjs'
import { processToolCall, type Tool, type ToolCallbacks } from './chat/shared'
export const SUPPORTED_LANGUAGES = new Set(Object.keys(GEN_CONFIG.prompts))
@@ -484,7 +483,7 @@ const PROMPTS_CONFIGS = {
gen: GEN_CONFIG
}
function getProviderAndCompletionConfig<K extends boolean>({
export function getProviderAndCompletionConfig<K extends boolean>({
messages,
stream,
tools,
@@ -649,35 +648,6 @@ export async function getCompletion(
) {
const { provider, config } = getProviderAndCompletionConfig({ messages, stream: true, tools })
// If using Anthropic, use the Anthropic SDK with format conversion
if (provider === 'anthropic') {
const { system, messages: anthropicMessages } = convertOpenAIToAnthropicMessages(messages)
const anthropicTools = convertOpenAIToolsToAnthropic(tools)
const anthropicClient = workspaceAIClients.getAnthropicClient()
const anthropicParams = {
model: config.model,
max_tokens: config.max_tokens as number,
messages: anthropicMessages,
stream: true as const,
...(system && { system }),
...(anthropicTools && { tools: anthropicTools }),
...(typeof config.temperature === 'number' && { temperature: config.temperature })
}
const stream = await anthropicClient.messages.create(anthropicParams, {
signal: abortController.signal,
headers: {
'X-Provider': provider,
'anthropic-version': '2023-06-01'
}
})
return convertAnthropicStreamToOpenAI(config.model, stream)
}
// For other providers, use the existing OpenAI client
const openaiClient = workspaceAIClients.getOpenaiClient()
const completion = await openaiClient.chat.completions.create(config, {
signal: abortController.signal,
@@ -688,6 +658,108 @@ export async function getCompletion(
return completion
}
export async function parseOpenAICompletion(
completion: Stream<any>,
callbacks: ToolCallbacks & {
onNewToken: (token: string) => void
onMessageEnd: () => void
},
messages: ChatCompletionMessageParam[],
addedMessages: ChatCompletionMessageParam[],
tools: Tool<any>[],
helpers: any
): Promise<boolean> {
const finalToolCalls: Record<number, ChatCompletionChunk.Choice.Delta.ToolCall> = {}
let answer = ''
for await (const chunk of completion) {
if (!('choices' in chunk && chunk.choices.length > 0 && 'delta' in chunk.choices[0])) {
continue
}
const c = chunk as ChatCompletionChunk
const delta = c.choices[0].delta.content
if (delta) {
answer += delta
callbacks.onNewToken(delta)
}
const toolCalls = c.choices[0].delta.tool_calls || []
if (toolCalls.length > 0 && answer) {
// if tool calls are present but we have some textual content already, we need to display it to the user first
callbacks.onMessageEnd()
answer = ''
}
for (const toolCall of toolCalls) {
const { index } = toolCall
let finalToolCall = finalToolCalls[index]
if (!finalToolCall) {
finalToolCalls[index] = toolCall
} else {
if (toolCall.function?.arguments) {
if (!finalToolCall.function) {
finalToolCall.function = toolCall.function
} else {
finalToolCall.function.arguments =
(finalToolCall.function.arguments ?? '') + toolCall.function.arguments
}
}
}
finalToolCall = finalToolCalls[index]
if (finalToolCall?.function) {
const {
function: { name: funcName },
id: toolCallId
} = finalToolCall
if (funcName && toolCallId) {
const tool = tools.find((t) => t.def.function.name === funcName)
if (tool && tool.preAction) {
tool.preAction({ toolCallbacks: callbacks, toolId: toolCallId })
}
}
}
}
}
if (answer) {
const toAdd = { role: 'assistant' as const, content: answer }
addedMessages.push(toAdd)
messages.push(toAdd)
}
callbacks.onMessageEnd()
const toolCalls = Object.values(finalToolCalls).filter(
(toolCall) => toolCall.id !== undefined && toolCall.function?.arguments !== undefined
) as ChatCompletionMessageToolCall[]
if (toolCalls.length > 0) {
const toAdd = {
role: 'assistant' as const,
tool_calls: toolCalls.map((t) => ({
...t,
function: {
...t.function,
arguments: t.function.arguments || '{}'
}
}))
}
messages.push(toAdd)
addedMessages.push(toAdd)
for (const toolCall of toolCalls) {
const messageToAdd = await processToolCall({
tools,
toolCall,
helpers,
toolCallbacks: callbacks
})
messages.push(messageToAdd)
addedMessages.push(messageToAdd)
}
} else {
return false
}
return true
}
export function getResponseFromEvent(part: OpenAI.Chat.Completions.ChatCompletionChunk): string {
return part.choices?.[0]?.delta?.content || ''
}