Skip to content

Commit dfb906c

Browse files
committed
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.
1 parent 59faa5c commit dfb906c

67 files changed

Lines changed: 2541 additions & 601 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/content/docs/en/workflows/blocks/agent.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,11 @@ Some settings live under advanced, or appear only for models that support them:
8080
- **Temperature.** How varied the output is. Stay low (0–0.3) when you need accuracy and repeatability, middle (around 0.5) for everyday work, higher (0.7+) when you want creative variety.
8181
- **Max output tokens.** Caps the response length. Defaults to the model's full limit.
8282
- **Reasoning effort / Thinking level.** For models with extended reasoning, how much the model thinks before answering. Higher is more thorough but slower and costs more tokens.
83+
- **Prompt caching.** For Anthropic Claude models, reuses the system prompt and tool definitions between runs instead of re-reading them every time. Cached input costs a tenth of the normal rate, but writing the cache costs 1.25x, so leave it off for one-off runs and turn it on when the same agent runs repeatedly. The cache covers a prefix only if it reaches 1,024 tokens (2,048 on Haiku) — below that Anthropic ignores it and nothing changes. Entries expire after five minutes of no use.
8384
- **API key.** Your key for the chosen provider. Hidden on hosted Sim, which supplies one.
8485

86+
OpenAI and Gemini cache automatically at no extra cost and need no setting; their discount is already reflected in what you are charged.
87+
8588
## Outputs
8689

8790
After the agent runs, later blocks read its result by name:
@@ -141,6 +144,7 @@ The Agent reads the message from Start with `<start.input>` and returns a result
141144
{ question: "What is the difference between the tool usage controls (Auto, Force, None)?", answer: "In Auto, the model decides when to call a tool based on context. In Force, the model must call the tool on every run. In None, the tool is hidden from the model and never sent, which disables it without removing it from the block." },
142145
{ question: "How does the Response Format work?", answer: "It enforces structured output by providing a JSON Schema. When set, the model's response is constrained to match the schema exactly, and each field is read directly by downstream blocks using <agent.fieldName>. Without a response format, the agent returns its standard outputs: content, model, tokens, and toolCalls." },
143146
{ question: "What does the Reasoning Effort / Thinking Level setting do?", answer: "They appear only for models that support extended reasoning. Reasoning Effort (OpenAI o-series and GPT-5 models) and Thinking Level (Anthropic Claude and Gemini models with thinking) control how much compute the model spends reasoning before responding. Higher levels produce more thorough answers but cost more tokens and take longer." },
147+
{ question: "When should I turn on Prompt Caching?", answer: "Turn it on when the same agent runs repeatedly with a large, stable system prompt or tool set — cached input bills at a tenth of the normal input rate. Leave it off for one-off runs, because writing the cache costs 1.25x and nothing reads it back. The setting appears only for Anthropic Claude models; OpenAI and Gemini cache automatically with no setting and no write fee. Anthropic only caches a prefix of at least 1,024 tokens (2,048 on Haiku), and entries expire after five minutes of no use." },
144148
{ question: "How does max output tokens work with Anthropic models?", answer: "The Agent block uses each Anthropic model's full max output token limit by default (for example, 64,000 tokens). You can override this with the Max Output Tokens setting. For non-streaming requests that exceed the SDK's internal threshold, the provider automatically uses internal streaming to avoid timeouts." },
145149
{ question: "Can I use the Agent block with a custom or self-hosted model?", answer: "Yes. Use any Ollama or VLLM-compatible model by typing the model name directly into the model combobox, as long as it exposes a compatible API endpoint." },
146150
]} />

apps/docs/content/docs/en/workflows/deployment/agent-events.mdx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ Agent blocks can emit more than answer text while they run: **provider-exposed t
1212
Sim does **not** invent thinking for providers that do not stream it. Bedrock Converse, many OpenAI-compat models, and non-reasoning chat models stay text-only (plus tools when a live tool loop is wired).
1313
</Callout>
1414

15-
## Independent event gates (public chat / simple SSE)
15+
## Protocol opt-in vs. event gates (public chat / simple SSE)
1616

17-
Thinking and tool frames each require the stream protocol header plus their own deployment policy:
17+
Sending the header is a statement about the **client**: it understands v1 framing, so answer text can stream live and be retracted with `chunk_reset`. That alone does not expose anything — it only changes cadence, and it applies even when both event policies are off.
18+
19+
Thinking and tool frames need the header **plus** their own deployment policy:
1820

1921
1. Thinking frames require chat `includeThinking` (default **off**).
2022
2. Tool lifecycle frames require chat `includeToolCalls` (default **off**).
@@ -24,15 +26,15 @@ Thinking and tool frames each require the stream protocol header plus their own
2426
X-Sim-Stream-Protocol: agent-events-v1
2527
```
2628

27-
Legacy clients that omit the header keep today’s text-only SSE even if either deployment policy is enabled. The hosted chat UI always sends the header for its own deployments.
29+
Legacy clients that omit the header keep today’s text-only SSE even if either deployment policy is enabled. The hosted chat UI always sends the header for its own deployments, so hosted chats stream token by token regardless of the toggles.
2830

2931
## Simple SSE frame shapes
3032

3133
Answer text stays on `chunk`. Thinking and tools never reuse `chunk` (so older clients that append every `chunk` into the answer cannot leak thinking).
3234

3335
| Frame | Meaning |
3436
|-------|---------|
35-
| `{ "blockId", "chunk": "…" }` | Answer text. Legacy clients receive settled final-turn text; opted-in clients receive it live as the model generates (see below) |
37+
| `{ "blockId", "chunk": "…" }` | Answer text. Legacy clients receive settled final-turn text in one piece; opted-in clients receive it live as the model generates (see below) |
3638
| `{ "blockId", "event": "chunk_reset" }` | Opted-in only: discard the block’s streamed answer text — it belonged to a turn that resolved to tool calls |
3739
| `{ "blockId", "event": "thinking", "data": "…" }` | Thinking / reasoning summary delta |
3840
| `{ "blockId", "event": "tool", "phase": "start"\|"end", "id", "name", "status?" }` | Tool lifecycle (no args / results) |
@@ -45,8 +47,8 @@ Answer text stays on `chunk`. Thinking and tools never reuse `chunk` (so older c
4547

4648
During a live tool loop, the model can’t be classified mid-turn: text it emits may turn out to be the final answer or preamble before a tool call (the stop reason arrives only at turn end).
4749

48-
- **Opted-in clients** (protocol header + either event policy) receive answer text as `chunk` frames **live**, token by token. If the turn then resolves to tool calls, a `chunk_reset` frame tells the client to discard that block’s streamed text — the final turn re-streams live after tools settle. Append `chunk`, honor `chunk_reset`, and the displayed answer always converges to the block’s final content.
49-
- **Legacy clients** (no header) never see provisional text: only settled final-turn text is emitted as `chunk`, delivered when the turn completes.
50+
- **Opted-in clients** (protocol header; no event policy required) receive answer text as `chunk` frames **live**, token by token. If the turn then resolves to tool calls, a `chunk_reset` frame tells the client to discard that block’s streamed text — the final turn re-streams live after tools settle. Append `chunk`, honor `chunk_reset`, and the displayed answer always converges to the block’s final content.
51+
- **Legacy clients** (no header) never see provisional text: only settled final-turn text is emitted as `chunk`, delivered in one piece when the turn completes. Honoring `chunk_reset` is what buys live cadence, so send the header if you want it.
5052

5153
Logs, memory, and the block’s `content` output always contain final-turn text only — intermediate preamble is never persisted.
5254

@@ -71,7 +73,9 @@ The terminal output panel shows Thinking / Tools chrome above the block output w
7173

7274
## Chat deployment toggles
7375

74-
In **Deploy → Chat**, enable **Include thinking** for provider-exposed thinking and **Include tool calls** for tool names and lifecycle status. The switches are independent, and both event types still require the protocol header. Tool arguments and results are never included in lifecycle frames. Redeploy or update the chat after changing Agent models or tools.
76+
In **Deploy → Chat**, enable **Include thinking** for provider-exposed thinking and **Include tool calls** for tool names and lifecycle status. The switches are independent, and both event types still require the protocol header. Neither switch affects answer-text cadence.
77+
78+
Tool arguments and results are never exposed to a public chat — not in lifecycle frames, and not through the terminal `final` envelope, where the block's own tool calls are reduced to the same name-and-lifecycle shape. The authenticated workflow API still returns full tool results. Redeploy or update the chat after changing Agent models or tools.
7579

7680
## Capability honesty (high level)
7781

@@ -81,7 +85,7 @@ Per-model support is generated from the model registry on the [Agent block page]
8185
|--------|----------|------------|
8286
| Anthropic / Azure Anthropic | Yes (incl. redacted blocks in traces). The newest Claude generations omit full thinking; Sim requests summarized thinking for them on streaming runs | Yes |
8387
| Gemini / Vertex | Yes when a thinking level is set (thought summaries requested on agent-events runs) | Yes |
84-
| OpenAI Responses | Reasoning **summaries** when streamed (requires OpenAI organization verification; unverified orgs fall back to no summaries) | Silent tool loop; tool chips arrive settled (start + end together) once tools finish, ahead of the streamed answer — no live in-progress chips yet |
88+
| OpenAI Responses | Reasoning **summaries** when streamed (requires OpenAI organization verification; unverified orgs fall back to no summaries) | Yes |
8589
| OpenAI-compat (Groq, DeepSeek, …) | Only if vendor streams `reasoning` / `reasoning_content` | Live loop where wired (e.g. Groq, DeepSeek) |
8690
| Bedrock | Not invented | Yes when streaming tool loop is used |
8791

apps/sim/app/api/chat/[identifier]/route.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -331,11 +331,7 @@ export const POST = withRouteHandler(
331331
status: 200,
332332
headers: {
333333
...SSE_HEADERS,
334-
...agentStreamProtocolResponseHeaders({
335-
includeThinking,
336-
includeToolCalls,
337-
requestHeaders: request.headers,
338-
}),
334+
...agentStreamProtocolResponseHeaders({ requestHeaders: request.headers }),
339335
},
340336
})
341337
return streamResponse

apps/sim/app/api/chat/manage/[id]/route.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,38 @@ describe('Chat Edit API Route', () => {
236236
expect(data.message).toBe('Chat deployment updated successfully')
237237
})
238238

239+
it('turns grandfathered tool calls off when the same update disables thinking', async () => {
240+
// Before includeToolCalls existed, includeThinking gated tool frames too.
241+
// Resolving the fallback against the stored value would materialize the
242+
// stale `true` and silently leave tool frames on.
243+
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-id' } })
244+
245+
mockCheckChatAccess.mockResolvedValue({
246+
hasAccess: true,
247+
chat: {
248+
id: 'chat-123',
249+
identifier: 'test-chat',
250+
title: 'Test Chat',
251+
authType: 'public',
252+
workflowId: 'workflow-123',
253+
includeThinking: true,
254+
includeToolCalls: null,
255+
},
256+
workspaceId: 'workspace-123',
257+
})
258+
259+
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
260+
method: 'PATCH',
261+
body: JSON.stringify({ includeThinking: false }),
262+
})
263+
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
264+
265+
expect(response.status).toBe(200)
266+
expect(dbChainMockFns.set).toHaveBeenCalledWith(
267+
expect.objectContaining({ includeThinking: false, includeToolCalls: false })
268+
)
269+
})
270+
239271
it('returns 403 when the updated auth type changes to a blocked mode', async () => {
240272
authMockFns.mockGetSession.mockResolvedValue({
241273
user: { id: 'user-id' },

apps/sim/app/api/chat/manage/[id]/route.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,11 +257,16 @@ export const PATCH = withRouteHandler(
257257
updateData.includeThinking = includeThinking
258258
}
259259

260+
/**
261+
* Grandfathering must resolve against the thinking value this request is
262+
* setting, not the stored one. Before `includeToolCalls` existed,
263+
* `includeThinking` gated tool frames too, so a partial update turning
264+
* thinking off has to turn them off as well rather than materializing the
265+
* stale `true` and silently leaving tool frames enabled.
266+
*/
267+
const effectiveIncludeThinking = includeThinking ?? existingChatRecord.includeThinking
260268
updateData.includeToolCalls =
261-
includeToolCalls ??
262-
existingChatRecord.includeToolCalls ??
263-
existingChatRecord.includeThinking ??
264-
false
269+
includeToolCalls ?? existingChatRecord.includeToolCalls ?? effectiveIncludeThinking ?? false
265270

266271
const emailCount = Array.isArray(updateData.allowedEmails)
267272
? updateData.allowedEmails.length

apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -285,11 +285,7 @@ export const POST = withRouteHandler(
285285
headers: {
286286
...SSE_HEADERS,
287287
// Echo the negotiated stream protocol (same as the public chat route).
288-
...agentStreamProtocolResponseHeaders({
289-
includeThinking,
290-
includeToolCalls,
291-
requestHeaders: request.headers,
292-
}),
288+
...agentStreamProtocolResponseHeaders({ requestHeaders: request.headers }),
293289
'X-Execution-Id': enqueueResult.resumeExecutionId,
294290
},
295291
})

apps/sim/app/api/tools/firecrawl/parse/route.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
8888

8989
logger.info(`[${requestId}] Firecrawl parse successful`)
9090

91+
const document = firecrawlData.data ?? firecrawlData
9192
return NextResponse.json({
9293
success: true,
93-
output: firecrawlData.data ?? firecrawlData,
94+
output:
95+
// Credits reported on the envelope would otherwise be dropped with it,
96+
// leaving a paid parse with nothing to meter.
97+
firecrawlData.creditsUsed != null
98+
? { ...document, creditsUsed: firecrawlData.creditsUsed }
99+
: document,
94100
})
95101
} catch (error) {
96102
const notReady = docNotReadyResponse(error)

apps/sim/blocks/blocks/agent.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getMaxTemperature,
1414
getModelsWithDeepResearch,
1515
getModelsWithoutMemory,
16+
getModelsWithPromptCaching,
1617
getModelsWithReasoningEffort,
1718
getModelsWithThinking,
1819
getModelsWithVerbosity,
@@ -29,6 +30,7 @@ const logger = createLogger('AgentBlock')
2930
const MODELS_WITH_REASONING_EFFORT = getModelsWithReasoningEffort()
3031
const MODELS_WITH_VERBOSITY = getModelsWithVerbosity()
3132
const MODELS_WITH_THINKING = getModelsWithThinking()
33+
const MODELS_WITH_PROMPT_CACHING = getModelsWithPromptCaching()
3234
const MODELS_WITH_DEEP_RESEARCH = getModelsWithDeepResearch()
3335
const MODELS_WITHOUT_MEMORY = getModelsWithoutMemory()
3436

@@ -308,6 +310,19 @@ Return ONLY the JSON array.`,
308310
value: MODELS_WITH_THINKING,
309311
},
310312
},
313+
{
314+
id: 'promptCaching',
315+
title: 'Prompt Caching',
316+
type: 'switch',
317+
description:
318+
'Cache the system prompt and tool definitions so repeat runs reuse them at a reduced rate. Writing the cache costs more than a normal request, so this pays off when the same prompt runs repeatedly.',
319+
defaultValue: false,
320+
mode: 'advanced',
321+
condition: {
322+
field: 'model',
323+
value: MODELS_WITH_PROMPT_CACHING,
324+
},
325+
},
311326

312327
...getProviderCredentialSubBlocks(),
313328
{
@@ -646,6 +661,10 @@ Return ONLY the JSON array.`,
646661
type: 'string',
647662
description: 'Thinking level for models with extended thinking (Anthropic Claude, Gemini 3)',
648663
},
664+
promptCaching: {
665+
type: 'boolean',
666+
description: 'Cache the system prompt and tool definitions on models that support it',
667+
},
649668
tools: { type: 'json', description: 'Available tools configuration' },
650669
skills: { type: 'json', description: 'Selected skills configuration' },
651670
},

apps/sim/executor/handlers/agent/agent-handler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,7 @@ export class AgentBlockHandler implements BlockHandler {
954954
reasoningEffort: inputs.reasoningEffort,
955955
verbosity: inputs.verbosity,
956956
thinkingLevel: inputs.thinkingLevel,
957+
promptCaching: inputs.promptCaching === true,
957958
previousInteractionId: inputs.previousInteractionId,
958959
/** Agent-events remains the opt-in for exposing thinking and tool lifecycle events. */
959960
agentEvents: streaming && ctx.metadata?.agentEvents === true,
@@ -1030,6 +1031,9 @@ export class AgentBlockHandler implements BlockHandler {
10301031
reasoningEffort: providerRequest.reasoningEffort,
10311032
verbosity: providerRequest.verbosity,
10321033
thinkingLevel: providerRequest.thinkingLevel,
1034+
promptCaching: providerRequest.promptCaching,
1035+
// Stable per-block identity; providers use it to route cache lookups.
1036+
blockId: block.id,
10331037
previousInteractionId: providerRequest.previousInteractionId,
10341038
agentEvents: providerRequest.agentEvents,
10351039
abortSignal: ctx.abortSignal,

apps/sim/executor/handlers/agent/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface AgentInputs {
3939
reasoningEffort?: string
4040
verbosity?: string
4141
thinkingLevel?: string
42+
promptCaching?: boolean
4243
files?: unknown
4344
}
4445

0 commit comments

Comments
 (0)