mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-04 12:13:15 +00:00
* feat(providers): explicit prompt cache for DashScope/Qwen Extend OpenAI-compat path with Anthropic-style cache_control:ephemeral inline blocks for Alibaba DashScope endpoints (provider_type=bailian or URL contains "dashscope"). Verified live: qwen3.6-plus / qwen3.5-plus / qwen3-coder-plus all return 99.7% cache hit rate on 6K-token prefix, yielding ~89% token cost reduction on cached prefix (90% Alibaba discount). - isDashScope(): 3-source detection (URL + providerType + name) handles reverse-proxied endpoints; covers both "dashscope" and "bailian" - buildRequestBody wraps system content with SplitSystemPromptForCache (exported from anthropic_request.go) using <!-- GOCLAW_CACHE_BOUNDARY --> - Tool prefix cache: cache_control on last tool definition, with 4-marker budget guard - Parse cache_creation_input_tokens from prompt_tokens_details into Usage.CacheCreationTokens; propagates via existing span metadata writers - Runtime escape hatch: GOCLAW_DISABLE_DASHSCOPE_CACHE=true - Live integration smoke test (build tag integration, env-gated) * chore(providers): polish per PR #1127 review - Use strings.Repeat instead of custom repeat() helper in smoke test - Clarify BuildRequestBodyForTest is test-only, not public API * feat(providers): enable thinking for Qwen 3.7/3.6, observe cache in smoke test Qwen3.7-plus and qwen3.6-plus support deep thinking but were missing from dashscopeThinkingModels, so enable_thinking/thinking_budget was silently skipped. Add both to the whitelist and test. Add qwen3.7-plus to the cache smoke test as cache-optional: Alibaba's context-cache doc does not yet list 3.7/3.6 for explicit cache, and the cache wrap is a safe no-op when unsupported, so a no-cache result is logged rather than failed to avoid a flaky live assertion. Claude-Session: https://claude.ai/code/session_01X3jkrc7N3ar8ZGzUJWyNS5 * fix(providers): preserve DashScope detection for proxy routes
63 lines
2.0 KiB
Go
63 lines
2.0 KiB
Go
package providers
|
|
|
|
import "os"
|
|
|
|
// wrapSystemForDashScopeCache transforms a system message string content into
|
|
// Anthropic-style content blocks with cache_control:ephemeral markers.
|
|
//
|
|
// DashScope verified 2026-05-08 to accept and process this wire format on
|
|
// coding-intl.dashscope.aliyuncs.com. Result: 90% discount on cached prefix
|
|
// tokens, 5min sliding TTL.
|
|
//
|
|
// Non-system messages and non-string content pass through unchanged
|
|
// (idempotent, supports already-blocked input).
|
|
func wrapSystemForDashScopeCache(msg map[string]any) map[string]any {
|
|
if msg["role"] != "system" {
|
|
return msg
|
|
}
|
|
content, ok := msg["content"].(string)
|
|
if !ok {
|
|
return msg
|
|
}
|
|
msg["content"] = SplitSystemPromptForCache(content)
|
|
return msg
|
|
}
|
|
|
|
// applyDashScopeToolPrefixCache adds cache_control:ephemeral to the last tool
|
|
// definition, caching the entire tool prefix (descriptions, schemas).
|
|
//
|
|
// alreadyMarked: cache markers already consumed by system message blocks.
|
|
// DashScope limits 4 markers/request; skip tool marker if limit reached.
|
|
func applyDashScopeToolPrefixCache(tools []map[string]any, alreadyMarked int) []map[string]any {
|
|
if len(tools) == 0 || alreadyMarked >= 4 {
|
|
return tools
|
|
}
|
|
last := tools[len(tools)-1]
|
|
last["cache_control"] = map[string]any{"type": "ephemeral"}
|
|
return tools
|
|
}
|
|
|
|
// countCacheControlMarkers counts cache_control fields in a message's content
|
|
// blocks. Used to track marker budget across system + tools.
|
|
func countCacheControlMarkers(msg map[string]any) int {
|
|
blocks, ok := msg["content"].([]map[string]any)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
count := 0
|
|
for _, b := range blocks {
|
|
if b["cache_control"] != nil {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// dashScopeCacheDisabled returns true when env var GOCLAW_DISABLE_DASHSCOPE_CACHE
|
|
// is set to a truthy value. Provides runtime escape hatch without requiring
|
|
// code redeploy or config change.
|
|
func dashScopeCacheDisabled() bool {
|
|
v := os.Getenv("GOCLAW_DISABLE_DASHSCOPE_CACHE")
|
|
return v == "true" || v == "1" || v == "yes"
|
|
}
|