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

111 lines
2.7 KiB
Go

package openai_compat
import (
"context"
"fmt"
"io"
"net/http"
"time"
runtime "iop/packages/go/agentruntime"
)
// TunnelProvider relays an arbitrary HTTP request to the OpenAI-compatible
// endpoint and streams the response back through the sink.
func (a *Adapter) TunnelProvider(ctx context.Context, req runtime.ProviderTunnelRequest, sink runtime.ProviderTunnelSink) error {
var seq int64 = 0
if a.endpoint == "" && a.profile == nil {
err := fmt.Errorf("openai_compat adapter: endpoint or profile is required")
_ = emitTunnelError(ctx, sink, req, seq, err)
return err
}
isJSON := req.Method == http.MethodPost
httpReq, err := a.prepareRequest(ctx, req.Method, req.Operation, req.Path, req.Body, req.Headers, isJSON)
if err != nil {
_ = emitTunnelError(ctx, sink, req, seq, err)
return err
}
resp, err := a.client.Do(httpReq)
if err != nil {
err = fmt.Errorf("request failed: %w", err)
_ = emitTunnelError(ctx, sink, req, seq, err)
return err
}
defer resp.Body.Close()
respHeaders := make(map[string]string)
for k, values := range resp.Header {
if len(values) > 0 {
respHeaders[k] = values[0]
}
}
err = sink.EmitTunnelFrame(ctx, runtime.ProviderTunnelFrame{
RunID: req.RunID,
TunnelID: req.TunnelID,
Sequence: seq,
Kind: runtime.ProviderTunnelFrameKindResponseStart,
StatusCode: resp.StatusCode,
Headers: respHeaders,
Timestamp: time.Now(),
})
if err != nil {
return err
}
seq++
buf := make([]byte, 4096)
for {
n, readErr := resp.Body.Read(buf)
if n > 0 {
err = sink.EmitTunnelFrame(ctx, runtime.ProviderTunnelFrame{
RunID: req.RunID,
TunnelID: req.TunnelID,
Sequence: seq,
Kind: runtime.ProviderTunnelFrameKindBody,
Body: append([]byte(nil), buf[:n]...),
Timestamp: time.Now(),
})
if err != nil {
return err
}
seq++
}
if readErr != nil {
if readErr == io.EOF {
break
}
if ctx.Err() != nil {
_ = emitTunnelError(ctx, sink, req, seq, ctx.Err())
return ctx.Err()
}
err = fmt.Errorf("read response body: %w", readErr)
_ = emitTunnelError(ctx, sink, req, seq, err)
return err
}
}
err = sink.EmitTunnelFrame(ctx, runtime.ProviderTunnelFrame{
RunID: req.RunID,
TunnelID: req.TunnelID,
Sequence: seq,
Kind: runtime.ProviderTunnelFrameKindEnd,
End: true,
Timestamp: time.Now(),
})
return err
}
func emitTunnelError(ctx context.Context, sink runtime.ProviderTunnelSink, req runtime.ProviderTunnelRequest, seq int64, err error) error {
return sink.EmitTunnelFrame(ctx, runtime.ProviderTunnelFrame{
RunID: req.RunID,
TunnelID: req.TunnelID,
Sequence: seq,
Kind: runtime.ProviderTunnelFrameKindError,
Error: err.Error(),
Timestamp: time.Now(),
})
}