iop/apps/edge/internal/openai/anthropic_native.go
toki f2306f4dc8 feat(protocol-profile): 멀티 프로토콜 프로필을 추가한다
OpenAI 호환 및 Anthropic Messages 경로의 프로토콜 프로필, 라우팅 계약, 테스트와 실행 증거를 함께 반영한다.
2026-08-01 10:33:10 +09:00

116 lines
3.3 KiB
Go

package openai
import (
"net/http"
"strings"
"time"
edgeservice "iop/apps/edge/internal/service"
iop "iop/proto/gen/iop"
)
var anthropicResponseHeaderAllowlist = map[string]struct{}{
"Cache-Control": {},
"Content-Length": {},
"Content-Type": {},
"Request-Id": {},
"Retry-After": {},
"X-Request-Id": {},
"X-Robots-Tag": {},
}
func (s *Server) writeAnthropicNativeTunnelResponse(w http.ResponseWriter, r *http.Request, handle edgeservice.ProviderTunnelResult) {
frames := handle.Stream().Frames
if frames == nil {
writeAnthropicError(w, http.StatusBadGateway, "api_error", "provider tunnel is unavailable")
return
}
flusher, _ := w.(http.Flusher)
timer := time.NewTimer(handle.WaitTimeout())
defer timer.Stop()
wroteHeader := false
for {
select {
case <-r.Context().Done():
s.cancelRunOnHTTPGiveUp(handle.Dispatch(), r.Context().Err())
return
case <-timer.C:
s.cancelRunOnHTTPGiveUp(handle.Dispatch(), errRunTimedOut)
if !wroteHeader {
writeAnthropicError(w, http.StatusBadGateway, "api_error", "provider response timed out")
}
return
case frame, ok := <-frames:
if !ok {
if !wroteHeader {
writeAnthropicError(w, http.StatusBadGateway, "api_error", "provider tunnel closed before a response")
}
return
}
switch frame.GetKind() {
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START:
if wroteHeader {
continue
}
copyAnthropicResponseHeaders(w.Header(), frame.GetHeaders())
status := int(frame.GetStatusCode())
if status == 0 {
status = http.StatusOK
}
w.WriteHeader(status)
wroteHeader = true
if flusher != nil {
flusher.Flush()
}
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY:
if len(frame.GetBody()) == 0 {
continue
}
if !wroteHeader {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
wroteHeader = true
}
if _, err := w.Write(frame.GetBody()); err != nil {
s.sendCancelRun(handle.Dispatch())
return
}
if flusher != nil {
flusher.Flush()
}
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_ERROR:
if !wroteHeader {
writeAnthropicError(w, http.StatusBadGateway, "api_error", "provider tunnel failed")
} else if strings.Contains(strings.ToLower(w.Header().Get("Content-Type")), "text/event-stream") {
_ = writeAnthropicSSEEvent(w, "error", anthropicErrorResponse{
Type: "error", Error: errorBody{Type: "api_error", Message: "provider tunnel failed"},
})
if flusher != nil {
flusher.Flush()
}
}
return
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END:
if !wroteHeader {
writeAnthropicError(w, http.StatusBadGateway, "api_error", "provider tunnel ended before a response")
}
return
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_USAGE:
continue
}
}
}
}
func copyAnthropicResponseHeaders(dst http.Header, headers map[string]string) {
for key, value := range headers {
canonical := http.CanonicalHeaderKey(key)
_, exact := anthropicResponseHeaderAllowlist[canonical]
lower := strings.ToLower(key)
if !exact && !strings.HasPrefix(lower, "anthropic-ratelimit-") && !strings.HasPrefix(lower, "ratelimit-") {
continue
}
dst.Set(canonical, value)
}
}