6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
160 lines
5.0 KiB
TypeScript
160 lines
5.0 KiB
TypeScript
import type { RawMessageStreamEvent } from '@anthropic-ai/sdk/resources'
|
|
import { createLogger } from '@sim/logger'
|
|
import {
|
|
type AnthropicUsageAccumulator,
|
|
type AnthropicUsageLike,
|
|
addAnthropicUsage,
|
|
createAnthropicUsageAccumulator,
|
|
} from '@/providers/anthropic/usage'
|
|
import type { AgentStreamEvent } from '@/providers/stream-events'
|
|
import { trackForcedToolUsage } from '@/providers/utils'
|
|
|
|
const logger = createLogger('AnthropicUtils')
|
|
|
|
export interface AnthropicStreamComplete {
|
|
content: string
|
|
usage: AnthropicUsageAccumulator
|
|
/** Assembled thinking text for traces (redacted blocks become `[redacted]`). */
|
|
thinking: string
|
|
}
|
|
|
|
/**
|
|
* Converts an Anthropic Messages stream into an in-process
|
|
* {@link AgentStreamEvent} object stream (`thinking_delta` + `text_delta`).
|
|
* Tool_use / input_json deltas are ignored here — use
|
|
* {@link createAnthropicStreamingToolLoopStream} for the live tool loop.
|
|
*/
|
|
export function createReadableStreamFromAnthropicStream(
|
|
anthropicStream: AsyncIterable<RawMessageStreamEvent>,
|
|
onComplete?: (result: AnthropicStreamComplete) => void
|
|
): ReadableStream<AgentStreamEvent> {
|
|
let cancelled = false
|
|
let streamIterator: AsyncIterator<RawMessageStreamEvent> | undefined
|
|
|
|
return new ReadableStream<AgentStreamEvent>({
|
|
async start(controller) {
|
|
try {
|
|
let fullContent = ''
|
|
const thinkingBlocks: string[] = []
|
|
let currentThinking = ''
|
|
let usageSnapshot: AnthropicUsageLike = {}
|
|
|
|
const flushThinkingBlock = () => {
|
|
if (currentThinking) {
|
|
thinkingBlocks.push(currentThinking)
|
|
currentThinking = ''
|
|
}
|
|
}
|
|
|
|
streamIterator = anthropicStream[Symbol.asyncIterator]()
|
|
while (true) {
|
|
const next = await streamIterator.next()
|
|
if (next.done || cancelled) break
|
|
const event = next.value
|
|
if (event.type === 'message_start') {
|
|
usageSnapshot = event.message.usage
|
|
continue
|
|
}
|
|
|
|
if (event.type === 'message_delta') {
|
|
usageSnapshot = {
|
|
...usageSnapshot,
|
|
input_tokens: event.usage.input_tokens ?? usageSnapshot.input_tokens,
|
|
output_tokens: event.usage.output_tokens ?? usageSnapshot.output_tokens,
|
|
cache_read_input_tokens:
|
|
event.usage.cache_read_input_tokens ?? usageSnapshot.cache_read_input_tokens,
|
|
cache_creation_input_tokens:
|
|
event.usage.cache_creation_input_tokens ??
|
|
usageSnapshot.cache_creation_input_tokens,
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (event.type === 'content_block_start') {
|
|
if (event.content_block.type === 'redacted_thinking') {
|
|
flushThinkingBlock()
|
|
thinkingBlocks.push('[redacted]')
|
|
} else if (event.content_block.type === 'thinking') {
|
|
flushThinkingBlock()
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (event.type === 'content_block_stop') {
|
|
flushThinkingBlock()
|
|
continue
|
|
}
|
|
|
|
if (event.type !== 'content_block_delta') {
|
|
continue
|
|
}
|
|
|
|
const delta = event.delta
|
|
|
|
if (delta.type === 'thinking_delta' && typeof delta.thinking === 'string') {
|
|
currentThinking += delta.thinking
|
|
controller.enqueue({ type: 'thinking_delta', text: delta.thinking })
|
|
continue
|
|
}
|
|
|
|
if (delta.type === 'text_delta' && typeof delta.text === 'string') {
|
|
flushThinkingBlock()
|
|
fullContent += delta.text
|
|
controller.enqueue({ type: 'text_delta', text: delta.text, turn: 'final' })
|
|
}
|
|
}
|
|
|
|
if (cancelled) return
|
|
flushThinkingBlock()
|
|
|
|
if (onComplete) {
|
|
const usage = createAnthropicUsageAccumulator()
|
|
addAnthropicUsage(usage, usageSnapshot)
|
|
onComplete({
|
|
content: fullContent,
|
|
usage,
|
|
thinking: thinkingBlocks.filter(Boolean).join('\n\n'),
|
|
})
|
|
}
|
|
|
|
controller.close()
|
|
} catch (err) {
|
|
if (!cancelled) {
|
|
controller.error(err)
|
|
}
|
|
}
|
|
},
|
|
async cancel() {
|
|
cancelled = true
|
|
await streamIterator?.return?.()
|
|
},
|
|
})
|
|
}
|
|
|
|
export function checkForForcedToolUsage(
|
|
response: any,
|
|
toolChoice: any,
|
|
forcedTools: string[],
|
|
usedForcedTools: string[]
|
|
): { hasUsedForcedTool: boolean; usedForcedTools: string[] } | null {
|
|
if (typeof toolChoice === 'object' && toolChoice !== null && Array.isArray(response.content)) {
|
|
const toolUses = response.content.filter((item: any) => item.type === 'tool_use')
|
|
|
|
if (toolUses.length > 0) {
|
|
const adaptedToolCalls = toolUses.map((tool: any) => ({ name: tool.name }))
|
|
const adaptedToolChoice =
|
|
toolChoice.type === 'tool' ? { function: { name: toolChoice.name } } : toolChoice
|
|
|
|
return trackForcedToolUsage(
|
|
adaptedToolCalls,
|
|
adaptedToolChoice,
|
|
logger,
|
|
'anthropic',
|
|
forcedTools,
|
|
usedForcedTools
|
|
)
|
|
}
|
|
}
|
|
return null
|
|
}
|