Files
WeHub Mirror 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
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

209 lines
7.3 KiB
TypeScript

import type { ConverseStreamOutput } from '@aws-sdk/client-bedrock-runtime'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { randomFloat } from '@sim/utils/random'
import type { AgentStreamEvent } from '@/providers/stream-events'
import { trackForcedToolUsage } from '@/providers/utils'
const logger = createLogger('BedrockUtils')
export interface BedrockStreamUsage {
inputTokens: number
outputTokens: number
}
/**
* Converts an AWS event-stream exception member into an Error.
*/
export function getBedrockStreamError(event: ConverseStreamOutput): Error | undefined {
const exception =
event.internalServerException ??
event.modelStreamErrorException ??
event.validationException ??
event.throttlingException ??
event.serviceUnavailableException
if (!exception) return undefined
return new Error(exception.message || getErrorMessage(exception, 'Bedrock stream error'), {
cause: exception,
})
}
/**
* Bedrock ConverseStream → agent-events-v1 for the legacy (non-tool-loop)
* streaming path. Text deltas only: tools on this path are never executed, so
* emitting `tool_call_start` here would leave a chip running forever with no
* matching end. Sim does not request Bedrock reasoning, so there is no
* thinking to forward either.
*/
export function createReadableStreamFromBedrockStream(
bedrockStream: AsyncIterable<ConverseStreamOutput>,
onComplete?: (content: string, usage: BedrockStreamUsage) => void
): ReadableStream<AgentStreamEvent> {
let fullContent = ''
let inputTokens = 0
let outputTokens = 0
let cancelled = false
let streamIterator: AsyncIterator<ConverseStreamOutput> | undefined
return new ReadableStream({
async start(controller) {
try {
streamIterator = bedrockStream[Symbol.asyncIterator]()
while (true) {
const next = await streamIterator.next()
if (next.done || cancelled) break
const event = next.value
const streamError = getBedrockStreamError(event)
if (streamError) throw streamError
if (event.contentBlockDelta?.delta?.text) {
const text = event.contentBlockDelta.delta.text
fullContent += text
controller.enqueue({ type: 'text_delta', text, turn: 'final' })
} else if (event.metadata?.usage) {
inputTokens = event.metadata.usage.inputTokens ?? 0
outputTokens = event.metadata.usage.outputTokens ?? 0
}
}
if (cancelled) return
if (onComplete) {
onComplete(fullContent, { inputTokens, outputTokens })
}
controller.close()
} catch (err) {
if (!cancelled) {
controller.error(err)
}
}
},
async cancel() {
cancelled = true
await streamIterator?.return?.()
},
})
}
export function checkForForcedToolUsage(
toolUseBlocks: Array<{ name: string }>,
toolChoice: any,
forcedTools: string[],
usedForcedTools: string[]
): { hasUsedForcedTool: boolean; usedForcedTools: string[] } | null {
if (typeof toolChoice === 'object' && toolChoice !== null && toolUseBlocks.length > 0) {
const adaptedToolCalls = toolUseBlocks.map((tool) => ({ name: tool.name }))
const adaptedToolChoice = toolChoice.tool
? { function: { name: toolChoice.tool.name } }
: toolChoice
return trackForcedToolUsage(
adaptedToolCalls,
adaptedToolChoice,
logger,
'bedrock',
forcedTools,
usedForcedTools
)
}
return null
}
/**
* Generates a unique tool use ID for Bedrock.
* AWS Bedrock requires toolUseId to be 1-64 characters, pattern [a-zA-Z0-9_-]+
*/
export function generateToolUseId(toolName: string): string {
const timestamp = Date.now().toString(36) // Base36 timestamp (9 chars)
const random = randomFloat().toString(36).substring(2, 7) // 5 random chars
const suffix = `-${timestamp}-${random}` // ~15 chars
const maxNameLength = 64 - suffix.length
const truncatedName = toolName.substring(0, maxNameLength).replace(/[^a-zA-Z0-9_-]/g, '_')
return `${truncatedName}${suffix}`
}
/**
* Models whose AWS model cards state geo/cross-region inference profiles are
* not supported ("Geo inference ID: Not supported"). These must be invoked
* with the bare in-region model ID — prefixing them with a geo profile
* (e.g. us.mistral...) produces an invalid model identifier.
*/
const GEO_PROFILE_UNSUPPORTED_MODEL_IDS = new Set([
'mistral.mistral-large-3-675b-instruct',
'mistral.mistral-large-2407-v1:0',
'mistral.magistral-small-2509',
'mistral.ministral-3-14b-instruct',
'mistral.ministral-3-8b-instruct',
'mistral.ministral-3-3b-instruct',
'mistral.mixtral-8x7b-instruct-v0:1',
'amazon.titan-text-premier-v1:0',
'cohere.command-r-v1:0',
'cohere.command-r-plus-v1:0',
])
/** Cross-region inference profile prefixes Bedrock prepends to a base model ID. */
const GEO_PROFILE_PREFIX_PATTERN = /^(us-gov|us|eu|apac|au|ca|jp|global)\./
/**
* Strips Sim's `bedrock/` namespace and any cross-region inference prefix,
* leaving the bare `<vendor>.<model>` ID that capability checks key off.
*/
function getBedrockBaseModelId(modelId: string): string {
const withoutNamespace = modelId.startsWith('bedrock/') ? modelId.slice(8) : modelId
return withoutNamespace.replace(GEO_PROFILE_PREFIX_PATTERN, '')
}
/**
* Whether the model accepts `status` on a `toolResult` content block.
*
* Only Amazon Nova and Anthropic Claude 3/4 support it; Llama, Mistral, Cohere,
* and Titan reject the whole request with
* `ValidationException: This model doesn't support the status field.`
*
* Source: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolResultBlock.html
*/
export function supportsToolResultStatus(modelId: string): boolean {
const baseModelId = getBedrockBaseModelId(modelId)
return baseModelId.startsWith('anthropic.') || baseModelId.startsWith('amazon.nova')
}
/**
* Converts a model ID to the Bedrock inference profile format.
* AWS Bedrock requires inference profile IDs (e.g., us.anthropic.claude-...)
* for on-demand invocation of newer models, while some models only accept
* the bare in-region model ID.
*
* @param modelId - The model ID (e.g., "bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0")
* @param region - The AWS region (e.g., "us-east-1")
* @returns The inference profile ID (e.g., "us.anthropic.claude-sonnet-4-5-20250929-v1:0")
*/
export function getBedrockInferenceProfileId(modelId: string, region: string): string {
const baseModelId = modelId.startsWith('bedrock/') ? modelId.slice(8) : modelId
if (GEO_PROFILE_PREFIX_PATTERN.test(baseModelId)) {
return baseModelId
}
if (GEO_PROFILE_UNSUPPORTED_MODEL_IDS.has(baseModelId)) {
return baseModelId
}
let inferencePrefix: string
if (region.startsWith('us-gov-')) {
inferencePrefix = 'us-gov'
} else if (region.startsWith('us-') || region.startsWith('ca-')) {
inferencePrefix = 'us'
} else if (region.startsWith('eu-') || region === 'il-central-1') {
inferencePrefix = 'eu'
} else if (region.startsWith('ap-') || region.startsWith('me-')) {
inferencePrefix = 'apac'
} else if (region.startsWith('sa-')) {
inferencePrefix = 'us'
} else if (region.startsWith('af-')) {
inferencePrefix = 'eu'
} else {
inferencePrefix = 'us'
}
return `${inferencePrefix}.${baseModelId}`
}