mirror of
https://github.com/simstudioai/sim.git
synced 2026-08-31 01:11:53 +08:00
17d77795b4
* improvement(providers): validation pass, and stream tool loop improvements * remove deploy options correctly * fix * feat(providers): prompt caching capability and usage-based cache pricing Replace the arbitrary cached-rate heuristic with a single cache-aware pricing function, and add prompt caching as an opt-in capability for Anthropic. Pricing: priceModelUsage in cost-policy.ts is now the only place cache arithmetic happens. Provider adapters normalize their wire shape into ModelUsage (input always excludes cache buckets); the pricing function never branches on provider. This removes five divergent behaviors, including the !!request.context heuristic that gave Router and Evaluator an unearned 10x input discount, and the overwrite that silently billed Anthropic cache reads and writes at zero. Also parses OpenAI cache_write_tokens, previously ignored. Caching: Anthropic gets a capability-gated advanced switch that places cache_control on the last tool and last system block; system is now always a TextBlockParam array. OpenAI gets a stable per-block prompt_cache_key with no UI, since its caching is automatic. * fix(providers): route OpenAI and Gemini block cost through cache-aware pricing Cache-aware pricing only reached trace segments. The billable block cost still called calculateCost on the cache-inclusive prompt total, so OpenAI cache hits and Gemini implicit-cache hits were charged at the full input rate and GPT-5.6+ cache writes went unbilled. Both providers now accumulate cache buckets and price through priceModelUsage, matching the Anthropic token convention where input excludes cache reads and writes. Cached counts are clamped to the prompt total so an over-reporting payload cannot bill more input than the request contained. * fix(streaming): redact tool payloads on selected outputs in public chat Redaction only ran on the empty-selection branch, but a deployment almost always selects outputs, so it was dead in the case it exists for. Selecting toolCalls streamed the raw arguments and results to a public chat client in a chunk frame, and providerTiming carried thinking content the same way. Both paths now extract from the sanitized block output rather than the raw log: the streamed selected output, which is the reachable vector, and the final envelope. Sanitizing the source rather than per selected path means a newly selectable field cannot reopen the hole. * refactor(providers): drop unreachable billing fallbacks Every provider pricing helper took a policy parameter no caller passed. Worse than dead: passing one would have double-applied the margin the central layer already applies. Removed, so providers can only price at list. Also removed guards that cannot fire. The central fallback normalized cache buckets no provider can reach it with (all three that report cache usage price themselves) and did so at a 1x write multiplier no vendor charges. priceModelUsage re-validated token counts the adapter had already clamped, and applyModelCostPolicy defaulted a required total field. Validation now happens once, in the adapter that parses the vendor payload and is the only layer that knows cache buckets are a subset of the prompt total.
186 lines
6.5 KiB
TypeScript
186 lines
6.5 KiB
TypeScript
/**
|
|
* Generates the "Streamed thinking and tool calls" support tables on the Agent
|
|
* block docs page from the provider registry, so the docs can never drift from
|
|
* the code:
|
|
*
|
|
* - Thinking visibility per model comes from `capabilities.thinking.streamed`
|
|
* (explicit) or the per-provider defaults in `getThinkingStreamVisibility`.
|
|
* - Live tool-call streaming comes from `STREAMING_TOOL_CALL_PROVIDERS`.
|
|
*
|
|
* Content is rewritten between the `agent-stream-capabilities` markers in
|
|
* `apps/docs/content/docs/en/workflows/blocks/agent.mdx`.
|
|
*
|
|
* Usage:
|
|
* bun run scripts/sync-agent-stream-docs.ts # write
|
|
* bun run scripts/sync-agent-stream-docs.ts --check # fail on drift or missing metadata
|
|
*/
|
|
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
import {
|
|
getThinkingStreamVisibility,
|
|
PROVIDER_DEFINITIONS,
|
|
type ThinkingStreamVisibility,
|
|
} from '../apps/sim/providers/models'
|
|
import { STREAMING_TOOL_CALL_PROVIDERS } from '../apps/sim/providers/streaming-tool-loop-shared'
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const rootDir = path.resolve(path.dirname(__filename), '..')
|
|
|
|
const AGENT_DOC_PATH = path.join(rootDir, 'apps/docs/content/docs/en/workflows/blocks/agent.mdx')
|
|
|
|
const BEGIN_MARKER =
|
|
'{/* agent-stream-capabilities:begin — generated by `bun run agent-stream-docs:generate`; do not edit between markers */}'
|
|
const END_MARKER = '{/* agent-stream-capabilities:end */}'
|
|
|
|
/**
|
|
* Providers whose thinking visibility varies per model generation and must
|
|
* therefore be declared explicitly on every thinking-capable model.
|
|
*/
|
|
const EXPLICIT_VISIBILITY_PROVIDERS = new Set(['anthropic', 'azure-anthropic'])
|
|
|
|
const VISIBILITY_LABELS: Record<ThinkingStreamVisibility, string> = {
|
|
full: 'Full thinking deltas',
|
|
summary: 'Summaries only',
|
|
none: 'Not streamed',
|
|
}
|
|
|
|
const VISIBILITY_NOTES: Partial<Record<string, string>> = {
|
|
'openai:summary': 'Requires OpenAI organization verification; falls back to no summaries.',
|
|
'azure-openai:summary': 'Requires OpenAI organization verification; falls back to no summaries.',
|
|
'anthropic:summary':
|
|
'These generations omit full thinking; Sim requests summarized thinking on streaming runs.',
|
|
'azure-anthropic:summary':
|
|
'These generations omit full thinking; Sim requests summarized thinking on streaming runs.',
|
|
'anthropic:none': 'These model generations return thinking with omitted display by default.',
|
|
'azure-anthropic:none':
|
|
'These model generations return thinking with omitted display by default.',
|
|
'bedrock:none': 'Sim does not request reasoning on Bedrock.',
|
|
}
|
|
|
|
interface VisibilityRow {
|
|
providerName: string
|
|
visibility: ThinkingStreamVisibility
|
|
note: string
|
|
models: string[]
|
|
}
|
|
|
|
function buildVisibilityRows(): { rows: VisibilityRow[]; errors: string[] } {
|
|
const rows: VisibilityRow[] = []
|
|
const errors: string[] = []
|
|
|
|
for (const provider of Object.values(PROVIDER_DEFINITIONS)) {
|
|
const grouped = new Map<ThinkingStreamVisibility, string[]>()
|
|
|
|
for (const model of provider.models) {
|
|
if (model.sunset?.status === 'deprecated') continue
|
|
const reasoningCapable = model.capabilities.thinking || model.capabilities.reasoningEffort
|
|
if (!reasoningCapable) continue
|
|
|
|
if (
|
|
EXPLICIT_VISIBILITY_PROVIDERS.has(provider.id) &&
|
|
model.capabilities.thinking &&
|
|
model.capabilities.thinking.streamed === undefined
|
|
) {
|
|
errors.push(
|
|
`${provider.id}/${model.id}: thinking-capable models on this provider must declare capabilities.thinking.streamed ('full' | 'summary' | 'none') — visibility varies per Claude generation`
|
|
)
|
|
continue
|
|
}
|
|
|
|
const visibility = getThinkingStreamVisibility(model.id)
|
|
if (!visibility) continue
|
|
const models = grouped.get(visibility) ?? []
|
|
models.push(model.id)
|
|
grouped.set(visibility, models)
|
|
}
|
|
|
|
for (const visibility of ['full', 'summary', 'none'] as const) {
|
|
const models = grouped.get(visibility)
|
|
if (!models?.length) continue
|
|
rows.push({
|
|
providerName: provider.name,
|
|
visibility,
|
|
note: VISIBILITY_NOTES[`${provider.id}:${visibility}`] ?? '',
|
|
models,
|
|
})
|
|
}
|
|
}
|
|
|
|
return { rows, errors }
|
|
}
|
|
|
|
function buildGeneratedContent(): { content: string; errors: string[] } {
|
|
const { rows, errors } = buildVisibilityRows()
|
|
|
|
const liveToolProviders = Object.values(PROVIDER_DEFINITIONS)
|
|
.filter((provider) => STREAMING_TOOL_CALL_PROVIDERS.has(provider.id))
|
|
.map((provider) => provider.name)
|
|
|
|
const lines: string[] = []
|
|
lines.push('')
|
|
lines.push(
|
|
`Live tool-call chips stream for **${liveToolProviders.join(', ')}** models. Other providers run tools without live chips and project the settled final answer when the run completes; they do not ask the model to regenerate that answer just to create a stream.`
|
|
)
|
|
lines.push('')
|
|
lines.push('| Provider | Streamed thinking | Models |')
|
|
lines.push('|----------|-------------------|--------|')
|
|
for (const row of rows) {
|
|
const models = row.models.map((id) => `\`${id}\``).join(', ')
|
|
const visibility = row.note
|
|
? `${VISIBILITY_LABELS[row.visibility]} — ${row.note}`
|
|
: VISIBILITY_LABELS[row.visibility]
|
|
lines.push(`| ${row.providerName} | ${visibility} | ${models} |`)
|
|
}
|
|
lines.push('')
|
|
|
|
return { content: lines.join('\n'), errors }
|
|
}
|
|
|
|
function main(): void {
|
|
const checkMode = process.argv.includes('--check')
|
|
|
|
const { content, errors } = buildGeneratedContent()
|
|
if (errors.length > 0) {
|
|
console.error('agent-stream-docs: missing stream-visibility metadata:')
|
|
for (const error of errors) {
|
|
console.error(` - ${error}`)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
|
|
const doc = fs.readFileSync(AGENT_DOC_PATH, 'utf8')
|
|
const beginIndex = doc.indexOf(BEGIN_MARKER)
|
|
const endIndex = doc.indexOf(END_MARKER)
|
|
if (beginIndex === -1 || endIndex === -1 || endIndex < beginIndex) {
|
|
console.error(
|
|
`agent-stream-docs: markers not found in ${path.relative(rootDir, AGENT_DOC_PATH)}`
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
const next =
|
|
doc.slice(0, beginIndex + BEGIN_MARKER.length) + `\n${content}\n` + doc.slice(endIndex)
|
|
|
|
if (checkMode) {
|
|
if (next !== doc) {
|
|
console.error(
|
|
'agent-stream-docs: docs are out of date — run `bun run agent-stream-docs:generate`'
|
|
)
|
|
process.exit(1)
|
|
}
|
|
console.log('agent-stream-docs: up to date.')
|
|
return
|
|
}
|
|
|
|
if (next !== doc) {
|
|
fs.writeFileSync(AGENT_DOC_PATH, next)
|
|
console.log(`agent-stream-docs: updated ${path.relative(rootDir, AGENT_DOC_PATH)}`)
|
|
} else {
|
|
console.log('agent-stream-docs: no changes.')
|
|
}
|
|
}
|
|
|
|
main()
|