package oci import ( "bytes" "context" "fmt" "io" "net/http" "time" "github.com/oracle/oci-go-sdk/v65/common" ) // compatResponsesLimit 限制直通响应体大小;web_search 输出含多段引用,给足余量。 const compatResponsesLimit = int64(8 << 20) // dispatcherWithTimeout 把 dispatcher 换成指定总超时的拷贝(保留 Transport, // 代理链路不受影响);timeout=0 表示无总超时(流式读 body 不能有总时限)。 // 非 *http.Client 的自定义 dispatcher 保持原样,维持既有超时行为。 func dispatcherWithTimeout(d common.HTTPRequestDispatcher, timeout time.Duration) common.HTTPRequestDispatcher { hc, ok := d.(*http.Client) if !ok { return d } return &http.Client{Transport: hc.Transport, Timeout: timeout} } // newCompatResponsesRequest 构造 /actions/v1/responses 直通请求。 func newCompatResponsesRequest(ctx context.Context, cred Credentials, body []byte) (*http.Request, error) { request, err := http.NewRequestWithContext(ctx, http.MethodPost, "/actions/v1/responses", bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("build compat responses request: %w", err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("CompartmentId", cred.TenancyOCID) request.Header.Set("opc-compartment-id", cred.TenancyOCID) return request, nil } // GenAiCompatResponses 实现 Client:把 OpenAI Responses 请求体直通到 OCI // `/20231130/actions/v1/responses`(IAM 签名)。xAI 服务端工具(web_search / // x_search / code_interpreter)与 mcp 已被 Oracle 文档正式支持,工具参数与限制 // 遵循 xAI 规格;调用方须自行校验并改写请求体(store/stream)。 // wait 为整请求总超时:非流式上游要等全部生成完才回响应头,SDK 默认 60s 会掐断慢模型。 func (c *RealClient) GenAiCompatResponses(ctx context.Context, cred Credentials, region string, body []byte, wait time.Duration) ([]byte, error) { ic, err := c.genAiInferenceClient(cred, region) if err != nil { return nil, err } client := ic.BaseClient common.UpdateEndpointTemplateForOptions(&client) common.SetMissingTemplateParams(&client) client.HTTPClient = dispatcherWithTimeout(client.HTTPClient, wait) request, err := newCompatResponsesRequest(ctx, cred, body) if err != nil { return nil, err } response, err := client.Call(ctx, request) if err != nil { return nil, err } defer response.Body.Close() payload, err := readCompatBody(response.Body, compatResponsesLimit, "compat responses") if err != nil { return nil, err } return payload, nil } // cancelReadCloser 在流关闭时同步取消建立阶段派生的 ctx,避免其随流生命周期泄漏。 type cancelReadCloser struct { io.ReadCloser cancel context.CancelFunc } func (c *cancelReadCloser) Close() error { c.cancel() return c.ReadCloser.Close() } // httpCaller 抽象 BaseClient.Call,便于对预算逻辑做无签名单测。 type httpCaller interface { Call(ctx context.Context, request *http.Request) (*http.Response, error) } // callWithHeaderBudget 以 wait 为等待响应头预算执行调用:预算内未返回则取消 // 请求(SDK Call 会把 ctx 重绑到请求);响应头到达即解除预算,之后流的生命 // 周期由 ctx 决定,返回的流 Close 时同步取消派生 ctx。 func callWithHeaderBudget(ctx context.Context, c httpCaller, req *http.Request, wait time.Duration) (io.ReadCloser, error) { callCtx, cancel := context.WithCancel(ctx) timer := time.AfterFunc(wait, cancel) response, err := c.Call(callCtx, req) timer.Stop() if err != nil { if response != nil && response.Body != nil { response.Body.Close() } cancel() return nil, err } return &cancelReadCloser{ReadCloser: response.Body, cancel: cancel}, nil } // GenAiCompatResponsesStream 实现 Client:以流式直通 OCI `/actions/v1/responses`, // 建立成功(2xx)返回 SSE body(调用方负责 Close);建立失败返回 SDK ServiceError, // 与既有渠道切换/熔断错误分类兼容。请求体须由调用方置 stream:true。 // 总超时置 0(SSE 读 body 不能有总时限);wait 以定时取消模拟等待响应头预算, // 响应头到达即解除,此后流的生命周期完全由 ctx(下游客户端断开)决定。 func (c *RealClient) GenAiCompatResponsesStream(ctx context.Context, cred Credentials, region string, body []byte, wait time.Duration) (io.ReadCloser, error) { ic, err := c.genAiInferenceClient(cred, region) if err != nil { return nil, err } client := ic.BaseClient common.UpdateEndpointTemplateForOptions(&client) common.SetMissingTemplateParams(&client) client.HTTPClient = dispatcherWithTimeout(client.HTTPClient, 0) request, err := newCompatResponsesRequest(ctx, cred, body) if err != nil { return nil, err } return callWithHeaderBudget(ctx, client, request, wait) } // readCompatBody 读取上游响应体并施加上限;读 limit+1 判超报错—— // 静默截断的 JSON/音频配 200 会被下游当完整成功记账。 func readCompatBody(body io.Reader, limit int64, tag string) ([]byte, error) { payload, err := io.ReadAll(io.LimitReader(body, limit+1)) if err != nil { return nil, fmt.Errorf("read %s body: %w", tag, err) } if int64(len(payload)) > limit { return nil, fmt.Errorf("%s body exceeds %d bytes", tag, limit) } return payload, nil }