Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s

This commit is contained in:
李建琦
2026-08-21 18:30:13 +08:00
commit 6d655c9903
3584 changed files with 1270640 additions and 0 deletions
@@ -0,0 +1,288 @@
// Package anthropicfp provides pure helpers for suppressing client-side
// fingerprints that would otherwise be visible to upstream Anthropic when a
// forwarding gateway sits between the client and api.anthropic.com.
//
// Currently exposes NormalizeDateline: it rewrites the "Today's date is
// YYYY-MM-DD." sentence inside a request body back to a canonical ASCII form,
// erasing three bits of steganographic signal (four apostrophe code points and
// a date-separator variant) that some clients embed in that sentence when
// they detect a non-official base URL.
package anthropicfp
import (
"fmt"
"regexp"
"sort"
"strings"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
// datelineRegexes matches the fingerprinted sentence with any of the four
// apostrophe code points seen in the wild and either separator. Two regexes
// are used because Go's RE2-based regexp package does not support
// backreferences: matching `-` and `/` in two passes keeps the two separators
// inside YYYY?MM?DD forced to agree, so mixed-separator strings like
// "Today's date is 2026-07/01." never match. This is what filters out
// user-authored prose like "Today is foo." or "His date is 2026-06-30." from
// being touched.
var (
datelineRegexHyphen = regexp.MustCompile(`Today(['’ʼʹ])s date is (\d{4})-(\d{2})-(\d{2})\.`)
datelineRegexSlash = regexp.MustCompile(`Today(['’ʼʹ])s date is (\d{4})/(\d{2})/(\d{2})\.`)
)
// systemReminderRegex matches a <system-reminder> block. The dateline lives in
// this block once the conversation has advanced past the first turn (system
// prompt caching hides the top-level system block for subsequent turns), so
// the messages[].content[] scan is confined to what lives inside these tags.
var systemReminderRegex = regexp.MustCompile(`(?s)<system-reminder>.*?</system-reminder>`)
// DatelineHit records what a single rewrite normalized, for observability.
type DatelineHit struct {
// ApostropheVariant is one of "ascii" (U+0027), "u2019", "u02bc", "u02b9".
ApostropheVariant string
// DateSeparator is either "-" or "/" as seen before normalization.
DateSeparator string
}
// canonicalize returns the canonical form of a matched dateline sentence.
// The output always uses ASCII apostrophe and hyphen separators.
func canonicalize(year, month, day string) string {
return fmt.Sprintf("Today's date is %s-%s-%s.", year, month, day)
}
func apostropheVariant(r rune) string {
switch r {
case '':
return "u2019"
case 'ʼ':
return "u02bc"
case 'ʹ':
return "u02b9"
default:
return "ascii"
}
}
type datelineMatch struct {
start, end int
apoRune rune
sep string
year, month, day string
}
func collectMatches(text string, re *regexp.Regexp, sep string) []datelineMatch {
locs := re.FindAllStringSubmatchIndex(text, -1)
if len(locs) == 0 {
return nil
}
out := make([]datelineMatch, 0, len(locs))
for _, m := range locs {
var apoRune rune
for _, r := range text[m[2]:m[3]] {
apoRune = r
break
}
out = append(out, datelineMatch{
start: m[0],
end: m[1],
apoRune: apoRune,
sep: sep,
year: text[m[4]:m[5]],
month: text[m[6]:m[7]],
day: text[m[8]:m[9]],
})
}
return out
}
// NormalizeText replaces every fingerprinted dateline sentence in text with
// its canonical form. It returns the possibly-rewritten text and the list of
// hits observed. When no match is found the original string is returned
// verbatim (byte-identical), and the returned hit slice is nil.
func NormalizeText(text string) (string, []DatelineHit) {
if !strings.Contains(text, "date is ") {
return text, nil
}
matches := collectMatches(text, datelineRegexHyphen, "-")
matches = append(matches, collectMatches(text, datelineRegexSlash, "/")...)
if len(matches) == 0 {
return text, nil
}
sort.Slice(matches, func(i, j int) bool { return matches[i].start < matches[j].start })
var b strings.Builder
b.Grow(len(text))
prev := 0
hits := make([]DatelineHit, 0, len(matches))
changed := false
for _, m := range matches {
full := text[m.start:m.end]
canonical := canonicalize(m.year, m.month, m.day)
if canonical == full {
// Already canonical: no rewrite, no hit.
continue
}
_, _ = b.WriteString(text[prev:m.start])
_, _ = b.WriteString(canonical)
prev = m.end
changed = true
hits = append(hits, DatelineHit{
ApostropheVariant: apostropheVariant(m.apoRune),
DateSeparator: m.sep,
})
}
if !changed {
return text, nil
}
_, _ = b.WriteString(text[prev:])
return b.String(), hits
}
// normalizeSystemReminderScopedText scans only the <system-reminder> blocks
// inside text and normalizes datelines inside them. Text outside the blocks is
// preserved byte-for-byte, so user prose, tool_result content, code blocks,
// or shell commands that happen to contain an apostrophe or a slash date are
// never touched.
func normalizeSystemReminderScopedText(text string) (string, []DatelineHit) {
if !strings.Contains(text, "<system-reminder>") {
return text, nil
}
locs := systemReminderRegex.FindAllStringIndex(text, -1)
if len(locs) == 0 {
return text, nil
}
var b strings.Builder
b.Grow(len(text))
prev := 0
var hits []DatelineHit
changed := false
for _, loc := range locs {
_, _ = b.WriteString(text[prev:loc[0]])
block := text[loc[0]:loc[1]]
normalized, blockHits := NormalizeText(block)
if normalized != block {
changed = true
}
_, _ = b.WriteString(normalized)
hits = append(hits, blockHits...)
prev = loc[1]
}
if !changed {
return text, nil
}
_, _ = b.WriteString(text[prev:])
return b.String(), hits
}
// NormalizeDateline scans an Anthropic /v1/messages request body and rewrites
// every fingerprinted dateline sentence back to its canonical ASCII form.
//
// Scope (mirroring where genuine clients place the sentence):
// - `system` string, or `.text` field of each text-typed block in `system`.
// - Text bodies inside `messages[i].content` — but ONLY the substrings that
// appear inside `<system-reminder>...</system-reminder>` tags. Free user
// prose, tool_use.input, tool_result.content, and other block types are
// never scanned, guaranteeing that legitimate text like a code block, a
// shell command, or a chat message that mentions today's date is never
// accidentally rewritten.
//
// The function is a pure transform: it never modifies the input slice, and if
// no rewrite is needed it returns the original slice (identity), a nil hit
// slice, and changed=false.
func NormalizeDateline(body []byte) ([]byte, []DatelineHit, bool) {
if len(body) == 0 {
return body, nil, false
}
out := body
var hits []DatelineHit
changed := false
sys := gjson.GetBytes(out, "system")
if sys.Exists() {
switch {
case sys.Type == gjson.String:
normalized, sysHits := NormalizeText(sys.String())
if normalized != sys.String() {
if next, err := sjson.SetBytes(out, "system", normalized); err == nil {
out = next
changed = true
hits = append(hits, sysHits...)
}
}
case sys.IsArray():
idx := 0
sys.ForEach(func(_, item gjson.Result) bool {
if item.Get("type").String() == "text" {
t := item.Get("text")
if t.Exists() && t.Type == gjson.String {
normalized, textHits := NormalizeText(t.String())
if normalized != t.String() {
path := fmt.Sprintf("system.%d.text", idx)
if next, err := sjson.SetBytes(out, path, normalized); err == nil {
out = next
changed = true
hits = append(hits, textHits...)
}
}
}
}
idx++
return true
})
}
}
messages := gjson.GetBytes(out, "messages")
if messages.IsArray() {
msgIdx := -1
messages.ForEach(func(_, msg gjson.Result) bool {
msgIdx++
content := msg.Get("content")
if !content.Exists() {
return true
}
switch {
case content.Type == gjson.String:
normalized, contentHits := normalizeSystemReminderScopedText(content.String())
if normalized != content.String() {
path := fmt.Sprintf("messages.%d.content", msgIdx)
if next, err := sjson.SetBytes(out, path, normalized); err == nil {
out = next
changed = true
hits = append(hits, contentHits...)
}
}
case content.IsArray():
contentIdx := -1
content.ForEach(func(_, block gjson.Result) bool {
contentIdx++
if block.Get("type").String() != "text" {
return true
}
t := block.Get("text")
if !t.Exists() || t.Type != gjson.String {
return true
}
normalized, textHits := normalizeSystemReminderScopedText(t.String())
if normalized != t.String() {
path := fmt.Sprintf("messages.%d.content.%d.text", msgIdx, contentIdx)
if next, err := sjson.SetBytes(out, path, normalized); err == nil {
out = next
changed = true
hits = append(hits, textHits...)
}
}
return true
})
}
return true
})
}
if !changed {
return body, nil, false
}
return out, hits, true
}
@@ -0,0 +1,248 @@
package anthropicfp
import (
"bytes"
"strings"
"testing"
)
func TestNormalizeText_ASCIIHyphenIsIdentity(t *testing.T) {
in := "Today's date is 2026-07-01."
out, hits := NormalizeText(in)
if out != in {
t.Fatalf("canonical form should be returned identity, got %q", out)
}
if len(hits) != 0 {
t.Fatalf("no hits expected on canonical input, got %d", len(hits))
}
}
func TestNormalizeText_SlashSeparatorASCIIApostrophe(t *testing.T) {
in := "Today's date is 2026/07/01."
out, hits := NormalizeText(in)
want := "Today's date is 2026-07-01."
if out != want {
t.Fatalf("want %q, got %q", want, out)
}
if len(hits) != 1 || hits[0].ApostropheVariant != "ascii" || hits[0].DateSeparator != "/" {
t.Fatalf("unexpected hit: %+v", hits)
}
}
func TestNormalizeText_U2019Apostrophe(t *testing.T) {
in := "Todays date is 2026-07-01."
out, hits := NormalizeText(in)
want := "Today's date is 2026-07-01."
if out != want {
t.Fatalf("want %q, got %q", want, out)
}
if len(hits) != 1 || hits[0].ApostropheVariant != "u2019" || hits[0].DateSeparator != "-" {
t.Fatalf("unexpected hit: %+v", hits)
}
}
func TestNormalizeText_U02BCApostropheWithSlash(t *testing.T) {
in := "Todayʼs date is 2026/07/01."
out, hits := NormalizeText(in)
want := "Today's date is 2026-07-01."
if out != want {
t.Fatalf("want %q, got %q", want, out)
}
if len(hits) != 1 || hits[0].ApostropheVariant != "u02bc" || hits[0].DateSeparator != "/" {
t.Fatalf("unexpected hit: %+v", hits)
}
}
func TestNormalizeText_U02B9Apostrophe(t *testing.T) {
in := "Todayʹs date is 2026/07/01."
out, hits := NormalizeText(in)
want := "Today's date is 2026-07-01."
if out != want {
t.Fatalf("want %q, got %q", want, out)
}
if len(hits) != 1 || hits[0].ApostropheVariant != "u02b9" || hits[0].DateSeparator != "/" {
t.Fatalf("unexpected hit: %+v", hits)
}
}
func TestNormalizeText_MixedSeparatorsNoMatch(t *testing.T) {
// backreference \3 forces the two separators to agree
in := "Today's date is 2026-07/01."
out, hits := NormalizeText(in)
if out != in {
t.Fatalf("mixed-separator input should not be matched, got %q", out)
}
if len(hits) != 0 {
t.Fatalf("expected no hits, got %d", len(hits))
}
}
func TestNormalizeText_NegativeLookalike(t *testing.T) {
cases := []string{
"Today is a great day.",
"His date is 2026-07-01.",
"Yesterday's date was 2026-06-30.",
"'s date is 2026-07-01.",
}
for _, c := range cases {
out, hits := NormalizeText(c)
if out != c {
t.Fatalf("input %q should not be modified, got %q", c, out)
}
if len(hits) != 0 {
t.Fatalf("input %q should produce no hits", c)
}
}
}
func TestNormalizeText_Idempotent(t *testing.T) {
in := "Todays date is 2026/07/01."
out1, _ := NormalizeText(in)
out2, hits2 := NormalizeText(out1)
if out1 != out2 {
t.Fatalf("normalization not idempotent: %q vs %q", out1, out2)
}
if len(hits2) != 0 {
t.Fatalf("second pass should produce no hits, got %d", len(hits2))
}
}
func TestNormalizeText_MultipleOccurrences(t *testing.T) {
in := "First line.\nTodays date is 2026/07/01.\nMore text.\nTodayʼs date is 2026-07-01.\nEnd."
out, hits := NormalizeText(in)
if strings.Count(out, "Today's date is") != 2 {
t.Fatalf("expected two canonicalized sentences, got: %q", out)
}
if strings.Contains(out, "") || strings.Contains(out, "ʼ") || strings.Contains(out, "2026/07/01") {
t.Fatalf("fingerprint characters must be gone, got: %q", out)
}
if len(hits) != 2 {
t.Fatalf("expected 2 hits, got %d", len(hits))
}
}
func TestNormalizeDateline_SystemString(t *testing.T) {
body := []byte(`{"system":"You are helpful.\nTodays date is 2026/07/01.\nBe brief.","messages":[]}`)
out, hits, changed := NormalizeDateline(body)
if !changed {
t.Fatalf("expected changed=true")
}
if len(hits) != 1 {
t.Fatalf("expected 1 hit, got %d", len(hits))
}
if !bytes.Contains(out, []byte("Today's date is 2026-07-01.")) {
t.Fatalf("output missing canonical dateline: %s", string(out))
}
if bytes.Contains(out, []byte("2026/07/01")) {
t.Fatalf("output should not contain slash date: %s", string(out))
}
}
func TestNormalizeDateline_SystemBlocksArray(t *testing.T) {
body := []byte(`{"system":[{"type":"text","text":"You are helpful."},{"type":"text","text":"Todayʼs date is 2026/07/01."}],"messages":[]}`)
out, hits, changed := NormalizeDateline(body)
if !changed {
t.Fatalf("expected changed=true")
}
if len(hits) != 1 || hits[0].ApostropheVariant != "u02bc" {
t.Fatalf("unexpected hits: %+v", hits)
}
if !bytes.Contains(out, []byte("Today's date is 2026-07-01.")) {
t.Fatalf("output missing canonical dateline: %s", string(out))
}
}
func TestNormalizeDateline_MessagesContentStringInSystemReminder(t *testing.T) {
body := []byte(`{"messages":[{"role":"user","content":"<system-reminder>\n# currentDate\nTodays date is 2026/07/01.\n</system-reminder>\nHello, please help."}]}`)
out, hits, changed := NormalizeDateline(body)
if !changed {
t.Fatalf("expected changed=true")
}
if len(hits) != 1 {
t.Fatalf("expected 1 hit, got %d", len(hits))
}
if !bytes.Contains(out, []byte("Today's date is 2026-07-01.")) {
t.Fatalf("canonical dateline missing: %s", string(out))
}
}
func TestNormalizeDateline_MessagesContentBlocksInSystemReminder(t *testing.T) {
body := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"<system-reminder>\nTodays date is 2026/07/01.\n</system-reminder>"},{"type":"text","text":"do X"}]}]}`)
out, hits, changed := NormalizeDateline(body)
if !changed {
t.Fatalf("expected changed=true")
}
if len(hits) != 1 {
t.Fatalf("expected 1 hit, got %d", len(hits))
}
if !bytes.Contains(out, []byte("Today's date is 2026-07-01.")) {
t.Fatalf("canonical dateline missing: %s", string(out))
}
}
func TestNormalizeDateline_LeavesOutOfScopeUntouched(t *testing.T) {
// User prose outside <system-reminder> that mentions today's date must
// not be modified. tool_use.input / tool_result.content are never scanned.
body := []byte(`{"messages":[` +
`{"role":"user","content":"Todays date is 2026/07/01. Please help."},` +
`{"role":"assistant","content":[{"type":"tool_use","id":"x","name":"y","input":{"note":"Todays date is 2026/07/01."}}]},` +
`{"role":"user","content":[{"type":"tool_result","tool_use_id":"x","content":"log: Todays date is 2026/07/01."}]}` +
`]}`)
out, hits, changed := NormalizeDateline(body)
if changed {
t.Fatalf("expected changed=false, hits=%v out=%s", hits, string(out))
}
if !bytes.Equal(out, body) {
t.Fatalf("output should equal input byte-for-byte")
}
}
func TestNormalizeDateline_Idempotent(t *testing.T) {
body := []byte(`{"messages":[{"role":"user","content":"<system-reminder>\nTodays date is 2026/07/01.\n</system-reminder>"}]}`)
first, _, changed1 := NormalizeDateline(body)
if !changed1 {
t.Fatalf("expected first pass to change body")
}
second, _, changed2 := NormalizeDateline(first)
if changed2 {
t.Fatalf("second pass should not report changes")
}
if !bytes.Equal(first, second) {
t.Fatalf("second pass diverged: %s vs %s", string(first), string(second))
}
}
func TestNormalizeDateline_EmptyBody(t *testing.T) {
out, hits, changed := NormalizeDateline(nil)
if changed || out != nil || hits != nil {
t.Fatalf("empty body should be no-op")
}
}
func TestNormalizeDateline_NoDateline(t *testing.T) {
body := []byte(`{"messages":[{"role":"user","content":"hello"}],"system":"just a system prompt"}`)
out, hits, changed := NormalizeDateline(body)
if changed || len(hits) != 0 {
t.Fatalf("expected no changes; changed=%v hits=%v", changed, hits)
}
if &out[0] != &body[0] {
// Identity is a bonus but not strict; verify content equality at minimum
if !bytes.Equal(out, body) {
t.Fatalf("output must byte-match input when no changes needed")
}
}
}
func TestNormalizeDateline_MultipleSystemReminderBlocksInSameText(t *testing.T) {
body := []byte(`{"messages":[{"role":"user","content":"<system-reminder>\nTodays date is 2026/07/01.\n</system-reminder>\nsome prose\n<system-reminder>\nAlso Todayʼs date is 2026/07/01.\n</system-reminder>"}]}`)
out, hits, changed := NormalizeDateline(body)
if !changed {
t.Fatalf("expected changed=true")
}
if len(hits) != 2 {
t.Fatalf("expected 2 hits, got %d", len(hits))
}
if bytes.Contains(out, []byte("2026/07/01")) {
t.Fatalf("slash separator must be gone: %s", string(out))
}
}