package proxy import ( "fmt" "io" "net" "net/http" "os/exec" "strings" "sync" "time" "github.com/rs/zerolog/log" ) // SniffedProfile holds everything captured from a real Claude Code request. // The proxy replays these verbatim — no hardcoded values needed. type SniffedProfile struct { // Raw headers exactly as Claude Code sent them (name→value). // Excludes only host, content-length, and auth (we substitute our own token). Headers [][2]string // The full request body with system prompt, tools, metadata, thinking config, etc. // We swap out model + messages from the incoming client request. Body []byte // Parsed from User-Agent for billing header fingerprint computation. Version string } var skipHeaders = map[string]bool{ "host": true, "content-length": true, "authorization": true, "x-api-key": true, "connection": true, } const fakeJSONResponse = `{"id":"msg_fake","type":"message","role":"assistant","content":[{"type":"text","text":"ok"}],"model":"claude-sonnet-4-6","stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1}}` const fakeStreamResponse = "event: message_start\n" + "data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_fake\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"claude-sonnet-4-6\",\"stop_reason\":null,\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}}\n\n" + "event: content_block_start\n" + "data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n" + "event: content_block_delta\n" + "data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"ok\"}}\n\n" + "event: content_block_stop\n" + "data: {\"type\":\"content_block_stop\",\"index\":0}\n\n" + "event: message_delta\n" + "data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":1}}\n\n" + "event: message_stop\n" + "data: {\"type\":\"message_stop\"}\n\n" func SniffClaudeCode(claudeBinary string) (*SniffedProfile, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, fmt.Errorf("listen: %w", err) } port := listener.Addr().(*net.TCPAddr).Port var profile *SniffedProfile var mu sync.Mutex captured := make(chan struct{}, 1) mux := http.NewServeMux() mux.HandleFunc("/", sniffHandler(&mu, &profile, captured)) srv := &http.Server{Handler: mux} go srv.Serve(listener) defer srv.Close() cmd := exec.Command(claudeBinary, "--print", "say hi") cmd.Env = append(cmd.Environ(), fmt.Sprintf("ANTHROPIC_BASE_URL=http://127.0.0.1:%d", port)) if err := cmd.Start(); err != nil { return nil, fmt.Errorf("start claude: %w", err) } done := make(chan error, 1) go func() { done <- cmd.Wait() }() select { case <-captured: cmd.Process.Kill() case err := <-done: if err != nil && profile == nil { return nil, fmt.Errorf("claude exited: %w", err) } case <-time.After(30 * time.Second): cmd.Process.Kill() return nil, fmt.Errorf("sniff timed out after 30s") } if profile == nil { return nil, fmt.Errorf("no API request captured") } log.Info(). Str("version", profile.Version). Int("headers", len(profile.Headers)). Int("body_size", len(profile.Body)). Msg("sniffed claude-code profile") for _, h := range profile.Headers { log.Debug().Str("header", h[0]).Str("value", h[1]).Msg("sniffed header") } return profile, nil } func sniffHandler(mu *sync.Mutex, profile **SniffedProfile, captured chan<- struct{}) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method == "HEAD" { w.WriteHeader(200) return } if r.Method != "POST" || !strings.Contains(r.URL.Path, "/v1/messages") { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) fmt.Fprint(w, fakeJSONResponse) return } body, _ := io.ReadAll(r.Body) mu.Lock() if *profile == nil { *profile = extractProfile(r, body) select { case captured <- struct{}{}: default: } } mu.Unlock() if strings.Contains(string(body), `"stream":true`) { w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(200) fmt.Fprint(w, fakeStreamResponse) } else { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) fmt.Fprint(w, fakeJSONResponse) } } } func extractProfile(r *http.Request, body []byte) *SniffedProfile { var headers [][2]string for name, vals := range r.Header { if skipHeaders[strings.ToLower(name)] { continue } for _, v := range vals { headers = append(headers, [2]string{name, v}) } } seen := map[string]bool{} var deduped [][2]string for _, h := range headers { key := strings.ToLower(h[0]) if seen[key] { continue } seen[key] = true if key == "anthropic-beta" { var filtered []string for _, b := range strings.Split(h[1], ",") { if !strings.Contains(b, "context-1m") { filtered = append(filtered, b) } } h[1] = strings.Join(filtered, ",") } deduped = append(deduped, h) } ua := r.Header.Get("User-Agent") version := "" if i := strings.Index(ua, "/"); i > 0 { rest := ua[i+1:] if j := strings.IndexByte(rest, ' '); j > 0 { version = rest[:j] } else { version = rest } } return &SniffedProfile{ Headers: deduped, Body: body, Version: version, } }