Skip to content

Commit f90497a

Browse files
Merge branch 'master' into feat/vllm-cpp-engine-args
2 parents 9666846 + 84972cb commit f90497a

75 files changed

Lines changed: 5488 additions & 356 deletions

Some content is hidden

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

.github/backend-matrix.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,34 @@ include:
6666
dockerfile: "./backend/Dockerfile.python"
6767
context: "./"
6868
ubuntu-version: '2404'
69+
- build-type: ''
70+
cuda-major-version: ""
71+
cuda-minor-version: ""
72+
platforms: 'linux/amd64'
73+
platform-tag: 'amd64'
74+
tag-latest: 'auto'
75+
tag-suffix: '-cpu-kokoro'
76+
runs-on: 'ubuntu-latest'
77+
base-image: "ubuntu:24.04"
78+
skip-drivers: 'true'
79+
backend: "kokoro"
80+
dockerfile: "./backend/Dockerfile.python"
81+
context: "./"
82+
ubuntu-version: '2404'
83+
- build-type: ''
84+
cuda-major-version: ""
85+
cuda-minor-version: ""
86+
platforms: 'linux/arm64'
87+
platform-tag: 'arm64'
88+
tag-latest: 'auto'
89+
tag-suffix: '-cpu-kokoro'
90+
runs-on: 'ubuntu-24.04-arm'
91+
base-image: "ubuntu:24.04"
92+
skip-drivers: 'true'
93+
backend: "kokoro"
94+
dockerfile: "./backend/Dockerfile.python"
95+
context: "./"
96+
ubuntu-version: '2404'
6997
- build-type: ''
7098
cuda-major-version: ""
7199
cuda-minor-version: ""

backend/backend.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,12 @@ message ProxyOptions {
508508
string api_key_file = 5;
509509
string upstream_model = 6;
510510
int32 request_timeout_seconds = 7;
511+
// cache_prompt enables automatic Anthropic prompt-cache breakpoints
512+
// (cache_control: ephemeral) on the stable prefix — system, tools, and
513+
// the last message block — when translating to the Anthropic provider.
514+
// Cuts input cost on repeated/agentic calls (cache read = 0.1x). Only
515+
// meaningful for mode=translate + provider=anthropic; ignored otherwise.
516+
bool cache_prompt = 8;
511517
}
512518

513519
message Result {

backend/go/cloud-proxy/provider_anthropic.go

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ import (
3232
type anthropicRequest struct {
3333
Model string `json:"model"`
3434
MaxTokens int32 `json:"max_tokens"`
35-
System string `json:"system,omitempty"`
35+
// System is `any`: a bare string normally, or []anthropicSystemBlock
36+
// when cache_prompt is on (the block form carries cache_control).
37+
System any `json:"system,omitempty"`
3638
Messages []anthropicMessage `json:"messages"`
3739
Stream bool `json:"stream,omitempty"`
3840
Temperature *float64 `json:"temperature,omitempty"`
@@ -52,9 +54,30 @@ type anthropicMessage struct {
5254
}
5355

5456
type anthropicTool struct {
55-
Name string `json:"name"`
56-
Description string `json:"description,omitempty"`
57-
InputSchema json.RawMessage `json:"input_schema"`
57+
Name string `json:"name"`
58+
Description string `json:"description,omitempty"`
59+
InputSchema json.RawMessage `json:"input_schema"`
60+
CacheControl *anthropicCacheControl `json:"cache_control,omitempty"`
61+
}
62+
63+
// anthropicCacheControl marks a prompt-cache breakpoint. Anthropic caches
64+
// everything up to and including a block tagged {"type":"ephemeral"} (5-min
65+
// TTL) and serves that prefix at the cache-read rate (0.1x input) on later
66+
// calls that share it — the win on agentic/multi-turn workloads.
67+
type anthropicCacheControl struct {
68+
Type string `json:"type"` // "ephemeral"
69+
}
70+
71+
// ephemeralCacheControl is the single reused breakpoint marker.
72+
var ephemeralCacheControl = &anthropicCacheControl{Type: "ephemeral"}
73+
74+
// anthropicSystemBlock is the block form of the top-level system field.
75+
// Anthropic accepts system as a bare string OR a list of text blocks; the
76+
// block form is required to attach cache_control to the system prompt.
77+
type anthropicSystemBlock struct {
78+
Type string `json:"type"` // "text"
79+
Text string `json:"text"`
80+
CacheControl *anthropicCacheControl `json:"cache_control,omitempty"`
5881
}
5982

6083
// anthropicToolChoice mirrors the four shapes Anthropic accepts:
@@ -81,8 +104,9 @@ type anthropicContentBlock struct {
81104
// Tool-result block fields. tool_result uses `content` (not
82105
// `text`) and pairs with `tool_use_id`; modelling them as
83106
// distinct fields avoids ambiguity at marshal time.
84-
ToolUseID string `json:"tool_use_id,omitempty"`
85-
ResultContent string `json:"content,omitempty"`
107+
ToolUseID string `json:"tool_use_id,omitempty"`
108+
ResultContent string `json:"content,omitempty"`
109+
CacheControl *anthropicCacheControl `json:"cache_control,omitempty"`
86110
}
87111

88112
type anthropicResponse struct {
@@ -156,6 +180,11 @@ func buildAnthropicRequest(opts *pb.PredictOptions, cfg *proxyConfig, stream boo
156180
if req.ToolChoice != nil && req.ToolChoice.Type == anthropicToolChoiceNone {
157181
req.Tools, req.ToolChoice = nil, nil
158182
}
183+
// Prompt-cache breakpoint on the last tool: Anthropic caches the entire
184+
// tool block up to the marked tool — usually a large, fully stable prefix.
185+
if cfg.cachePrompt && len(req.Tools) > 0 {
186+
req.Tools[len(req.Tools)-1].CacheControl = ephemeralCacheControl
187+
}
159188

160189
var systemParts []string
161190
for _, m := range opts.GetMessages() {
@@ -189,15 +218,54 @@ func buildAnthropicRequest(opts *pb.PredictOptions, cfg *proxyConfig, stream boo
189218
})
190219
}
191220
}
192-
req.System = strings.Join(systemParts, "\n\n")
221+
// System: block form (with cache_control) when caching is on, else the
222+
// bare string. Only set when non-empty so `omitempty` still drops it.
223+
if len(systemParts) > 0 {
224+
joined := strings.Join(systemParts, "\n\n")
225+
if cfg.cachePrompt {
226+
req.System = []anthropicSystemBlock{{Type: "text", Text: joined, CacheControl: ephemeralCacheControl}}
227+
} else {
228+
req.System = joined
229+
}
230+
}
193231

194232
if len(req.Messages) == 0 && opts.GetPrompt() != "" {
195233
req.Messages = []anthropicMessage{{Role: "user", Content: opts.GetPrompt()}}
196234
}
197235

236+
// Prompt-cache breakpoint on the final message block caches the whole
237+
// conversation prefix up to the newest turn. With the system + tools
238+
// breakpoints above, Anthropic serves the entire stable head at the
239+
// cache-read rate on the next agentic iteration (max 4 breakpoints; we
240+
// use at most 3, so we never exceed the limit).
241+
if cfg.cachePrompt {
242+
markLastMessageCacheable(req.Messages)
243+
}
244+
198245
return json.Marshal(req)
199246
}
200247

248+
// markLastMessageCacheable tags the final block of the last message with a
249+
// cache_control breakpoint. String content is promoted to a single text
250+
// block so the marker has somewhere to attach; block content gets the marker
251+
// on its last element.
252+
func markLastMessageCacheable(msgs []anthropicMessage) {
253+
if len(msgs) == 0 {
254+
return
255+
}
256+
last := &msgs[len(msgs)-1]
257+
switch c := last.Content.(type) {
258+
case string:
259+
if c != "" {
260+
last.Content = []anthropicContentBlock{{Type: "text", Text: c, CacheControl: ephemeralCacheControl}}
261+
}
262+
case []anthropicContentBlock:
263+
if len(c) > 0 {
264+
c[len(c)-1].CacheControl = ephemeralCacheControl
265+
}
266+
}
267+
}
268+
201269
// appendToolResult appends a tool_result block as a user message,
202270
// merging into a preceding user message that already carries blocks.
203271
// Anthropic concatenates consecutive same-role messages on its end,

backend/go/cloud-proxy/provider_anthropic_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,3 +328,62 @@ func TestBuildAnthropic_RoundTripsAssistantToolCalls(t *testing.T) {
328328
g.Expect(r0["tool_use_id"]).To(Equal("call_abc"))
329329
g.Expect(r0["content"]).To(Equal(`{"models":["a","b"]}`))
330330
}
331+
332+
// TestPredict_Anthropic_PromptCache verifies that cache_prompt injects
333+
// exactly the intended cache_control breakpoints (system, last tool, last
334+
// message) when on, and none when off — asserting on the raw upstream body
335+
// because System becomes a block list that the typed struct hides.
336+
func TestPredict_Anthropic_PromptCache(t *testing.T) {
337+
g := NewWithT(t)
338+
339+
// run issues one translate Predict and returns the raw body the fake
340+
// Anthropic upstream received.
341+
run := func(cachePrompt bool) string {
342+
var rawBody string
343+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
344+
b, _ := io.ReadAll(r.Body)
345+
rawBody = string(b)
346+
w.Header().Set("Content-Type", "application/json")
347+
_, _ = io.WriteString(w, `{"id":"m","type":"message","role":"assistant","content":[{"type":"text","text":"ok"}],"model":"claude-3-5-sonnet-20241022","usage":{"input_tokens":5,"output_tokens":2}}`)
348+
}))
349+
defer srv.Close()
350+
351+
t.Setenv("CLOUD_PROXY_ANTHROPIC_FAKE", "sk-ant-fake")
352+
cp := NewCloudProxy()
353+
err := cp.Load(&pb.ModelOptions{
354+
Model: "claude-local",
355+
Proxy: &pb.ProxyOptions{
356+
UpstreamUrl: srv.URL,
357+
Mode: modeTranslate,
358+
Provider: providerAnthropic,
359+
ApiKeyEnv: "CLOUD_PROXY_ANTHROPIC_FAKE",
360+
UpstreamModel: "claude-3-5-sonnet-20241022",
361+
CachePrompt: cachePrompt,
362+
},
363+
})
364+
g.Expect(err).NotTo(HaveOccurred())
365+
366+
_, err = cp.Predict(&pb.PredictOptions{
367+
Messages: []*pb.Message{
368+
{Role: "system", Content: "be brief"},
369+
{Role: "user", Content: "hello"},
370+
},
371+
Tools: `[{"type":"function","function":{"name":"t","parameters":{"type":"object"}}}]`,
372+
Tokens: 32,
373+
})
374+
g.Expect(err).NotTo(HaveOccurred())
375+
return rawBody
376+
}
377+
378+
// cache_prompt ON: three ephemeral breakpoints (system + last tool +
379+
// last message), and system is emitted in block form.
380+
on := run(true)
381+
g.Expect(strings.Count(on, `"cache_control":{"type":"ephemeral"}`)).To(Equal(3),
382+
"expected 3 breakpoints (system, tool, last message); body=%s", on)
383+
g.Expect(on).To(ContainSubstring(`"system":[{"type":"text","text":"be brief"`))
384+
385+
// cache_prompt OFF: no breakpoints, system stays a bare string.
386+
off := run(false)
387+
g.Expect(off).NotTo(ContainSubstring("cache_control"))
388+
g.Expect(off).To(ContainSubstring(`"system":"be brief"`))
389+
}

backend/go/cloud-proxy/proxy.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type proxyConfig struct {
4848
upstreamModel string
4949
localModel string // ModelOptions.Model — fallback when upstream_model is unset
5050
apiKey string // resolved at Load time
51+
cachePrompt bool // inject Anthropic prompt-cache breakpoints (translate+anthropic)
5152
}
5253

5354
func NewCloudProxy() *CloudProxy {
@@ -106,6 +107,7 @@ func (c *CloudProxy) Load(opts *pb.ModelOptions) error {
106107
upstreamModel: po.GetUpstreamModel(),
107108
localModel: opts.GetModel(),
108109
apiKey: key,
110+
cachePrompt: po.GetCachePrompt(),
109111
})
110112
xlog.Info("cloud-proxy: ready",
111113
"upstream", po.GetUpstreamUrl(),

backend/index.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,6 +1455,7 @@
14551455
alias: "kokoro"
14561456
name: "kokoro"
14571457
capabilities:
1458+
default: "cpu-kokoro"
14581459
nvidia: "cuda12-kokoro"
14591460
intel: "intel-kokoro"
14601461
amd: "rocm-kokoro"
@@ -5186,11 +5187,22 @@
51865187
- !!merge <<: *kokoro
51875188
name: "kokoro-development"
51885189
capabilities:
5190+
default: "cpu-kokoro-development"
51895191
nvidia: "cuda12-kokoro-development"
51905192
intel: "intel-kokoro-development"
51915193
amd: "rocm-kokoro-development"
51925194
nvidia-l4t: "nvidia-l4t-kokoro-development"
51935195
metal: "metal-kokoro-development"
5196+
- !!merge <<: *kokoro
5197+
name: "cpu-kokoro"
5198+
uri: "quay.io/go-skynet/local-ai-backends:latest-cpu-kokoro"
5199+
mirrors:
5200+
- localai/localai-backends:latest-cpu-kokoro
5201+
- !!merge <<: *kokoro
5202+
name: "cpu-kokoro-development"
5203+
uri: "quay.io/go-skynet/local-ai-backends:master-cpu-kokoro"
5204+
mirrors:
5205+
- localai/localai-backends:master-cpu-kokoro
51945206
- !!merge <<: *kokoro
51955207
name: "cuda12-kokoro-development"
51965208
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-12-kokoro"
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
git+https://github.com/Blaizzy/mlx-vlm@v0.4.4
1+
git+https://github.com/Blaizzy/mlx-vlm@v0.4.4
2+
torch
3+
torchvision

core/backend/options.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ func grpcModelOpts(c config.ModelConfig, modelPath string) *pb.ModelOptions {
475475
ApiKeyFile: c.Proxy.APIKeyFile,
476476
UpstreamModel: c.Proxy.UpstreamModel,
477477
RequestTimeoutSeconds: int32(c.Proxy.RequestTimeoutSeconds),
478+
CachePrompt: c.Proxy.CachePrompt,
478479
}
479480
}
480481

core/config/meta/registry.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,13 @@ func DefaultRegistry() map[string]FieldMetaOverride {
822822
Min: f64(0),
823823
Order: 213,
824824
},
825+
"proxy.cache_prompt": {
826+
Section: "proxy",
827+
Label: "Proxy Anthropic Prompt Cache",
828+
Description: "Inject Anthropic prompt-cache breakpoints (cache_control: ephemeral) on the stable prefix (system, tools, last message) when mode is translate and provider is anthropic. Serves the repeated prefix at the cache-read rate on multi-turn/agentic calls. No effect otherwise.",
829+
Component: "checkbox",
830+
Order: 214,
831+
},
825832

826833
// --- MITM intercept hosts ---
827834
// Each host listed here is claimed by this model config; the

core/config/model_config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,15 @@ type ProxyConfig struct {
232232
// means no per-request timeout (only the request context, which
233233
// is bound to the client connection, applies).
234234
RequestTimeoutSeconds int `yaml:"request_timeout_seconds,omitempty" json:"request_timeout_seconds,omitempty"`
235+
236+
// CachePrompt enables automatic Anthropic prompt-cache breakpoints
237+
// (cache_control: ephemeral) on the stable prefix — system prompt,
238+
// tools, and the last message block — when mode=translate and
239+
// provider=anthropic. Anthropic then serves the repeated prefix at
240+
// the cache-read rate (0.1x input), which sharply cuts cost on
241+
// agentic/multi-turn workloads that re-send a large stable prefix.
242+
// No effect for passthrough mode or non-Anthropic providers.
243+
CachePrompt bool `yaml:"cache_prompt,omitempty" json:"cache_prompt,omitempty"`
235244
}
236245

237246
// Proxy mode names. Validate() normalises an empty Mode to

0 commit comments

Comments
 (0)