| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- //go:build unit
- package service
- import (
- "bytes"
- "context"
- "errors"
- "io"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/pkg/openai_compat"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- "github.com/tidwall/gjson"
- )
- func forceChatMessagesFallbackAccount() *Account {
- account := rawChatCompletionsTestAccount()
- account.Extra = map[string]any{
- openai_compat.ExtraKeyResponsesMode: string(openai_compat.ResponsesSupportModeForceChatCompletions),
- }
- return account
- }
- // errTailReader yields the given data, then returns err instead of io.EOF,
- // simulating an upstream connection that breaks mid-stream.
- type errTailReader struct {
- data []byte
- off int
- err error
- }
- func (r *errTailReader) Read(p []byte) (int, error) {
- if r.off < len(r.data) {
- n := copy(p, r.data[r.off:])
- r.off += n
- return n, nil
- }
- return 0, r.err
- }
- func (r *errTailReader) Close() error { return nil }
- func TestForwardAsAnthropic_ForceChatCompletionsPreservesFinalModelReasoningEffort(t *testing.T) {
- gin.SetMode(gin.TestMode)
- tests := []struct {
- name string
- model string
- mapped string
- effortJSON string
- wantEffort string
- maxPolicy string
- }{
- {
- name: "policy caps converted effort",
- model: "gpt-5.6-luna",
- mapped: "gpt-5.6-luna",
- effortJSON: `,"output_config":{"effort":"max"}`,
- wantEffort: "medium",
- maxPolicy: "medium",
- },
- {
- name: "GPT56 max",
- model: "luna",
- mapped: "gpt-5.6-luna",
- effortJSON: `,"output_config":{"effort":"max"}`,
- wantEffort: "max",
- },
- {
- name: "old model max",
- model: "gpt-5.5",
- mapped: "gpt-5.5",
- effortJSON: `,"output_config":{"effort":"max"}`,
- wantEffort: "xhigh",
- },
- {
- name: "high remains high",
- model: "gpt-5.6-luna",
- mapped: "gpt-5.6-luna",
- effortJSON: `,"output_config":{"effort":"high"}`,
- wantEffort: "high",
- },
- {
- name: "omitted defaults medium",
- model: "gpt-5.6-luna",
- mapped: "gpt-5.6-luna",
- wantEffort: "medium",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- body := `{"model":"` + tt.model + `","max_tokens":16,"messages":[{"role":"user","content":"hello"}]` + tt.effortJSON + `,"stream":false}`
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader([]byte(body)))
- c.Request.Header.Set("Content-Type", "application/json")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"application/json"}},
- Body: io.NopCloser(strings.NewReader(
- `{"id":"chatcmpl_effort","object":"chat.completion","model":"` + tt.mapped + `","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":2,"total_tokens":5}}`,
- )),
- }}
- account := forceChatMessagesFallbackAccount()
- account.Credentials["model_mapping"] = map[string]any{tt.model: tt.mapped}
- svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
- ctx := context.Background()
- if tt.maxPolicy != "" {
- ctx = WithOpenAIReasoningEffortPolicy(ctx, tt.maxPolicy, nil, "")
- }
- result, err := svc.ForwardAsAnthropic(ctx, c, account, []byte(body), "", "")
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, tt.mapped, gjson.GetBytes(upstream.lastBody, "model").String())
- require.Equal(t, tt.wantEffort, gjson.GetBytes(upstream.lastBody, "reasoning_effort").String())
- require.NotNil(t, result.ReasoningEffort)
- require.Equal(t, tt.wantEffort, *result.ReasoningEffort)
- })
- }
- }
- func TestForwardAsAnthropic_ForceChatCompletionsNonStreaming(t *testing.T) {
- gin.SetMode(gin.TestMode)
- body := []byte(`{"model":"gpt-5.4","max_tokens":32,"messages":[{"role":"user","content":"hello"}],"stream":false}`)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
- c.Request.Header.Set("Content-Type", "application/json")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"application/json"}, "x-request-id": []string{"rid_msg_chat_json"}},
- Body: io.NopCloser(strings.NewReader(
- `{"id":"chatcmpl_json","object":"chat.completion","model":"gpt-5.4","service_tier":"priority","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":2,"total_tokens":5,"prompt_tokens_details":{"cached_tokens":1}}}`,
- )),
- }}
- svc := &OpenAIGatewayService{
- cfg: rawChatCompletionsTestConfig(),
- httpUpstream: upstream,
- }
- result, err := svc.ForwardAsAnthropic(context.Background(), c, forceChatMessagesFallbackAccount(), body, "", "")
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, "http://upstream.example/v1/chat/completions", upstream.lastReq.URL.String())
- require.Equal(t, "hello", gjson.GetBytes(upstream.lastBody, "messages.0.content").String())
- require.False(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
- require.True(t, gjson.GetBytes(upstream.lastBody, "stream_options").Exists() == false)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Equal(t, "assistant", gjson.Get(rec.Body.String(), "role").String())
- require.Equal(t, "ok", gjson.Get(rec.Body.String(), "content.0.text").String())
- require.Equal(t, 3, result.Usage.InputTokens)
- require.Equal(t, 2, result.Usage.OutputTokens)
- require.Equal(t, 1, result.Usage.CacheReadInputTokens)
- require.Nil(t, result.ServiceTier)
- require.Equal(t, "priority", result.UpstreamResponseServiceTier)
- require.False(t, result.Stream)
- }
- // Covers the fully-new streaming composition: text block is still open when
- // [DONE] arrives, so finalization must close it (content_block_stop) before
- // message_delta / message_stop.
- func TestForwardAsAnthropic_ForceChatCompletionsStreamingClosesOpenBlockOnDone(t *testing.T) {
- gin.SetMode(gin.TestMode)
- body := []byte(`{"model":"gpt-5.4","max_tokens":32,"messages":[{"role":"user","content":"hello"}],"stream":true}`)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
- c.Request.Header.Set("Content-Type", "application/json")
- upstreamBody := strings.Join([]string{
- `data: {"id":"chatcmpl_s","object":"chat.completion.chunk","model":"gpt-5.4","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}`,
- "",
- `data: {"id":"chatcmpl_s","object":"chat.completion.chunk","model":"gpt-5.4","choices":[{"index":0,"delta":{"content":"he"},"finish_reason":null}]}`,
- "",
- `data: {"id":"chatcmpl_s","object":"chat.completion.chunk","model":"gpt-5.4","choices":[{"index":0,"delta":{"content":"llo"},"finish_reason":null}]}`,
- "",
- `data: {"id":"chatcmpl_s","object":"chat.completion.chunk","model":"gpt-5.4","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}`,
- "",
- `data: {"id":"chatcmpl_s","object":"chat.completion.chunk","model":"gpt-5.4","choices":[],"usage":{"prompt_tokens":4,"completion_tokens":3,"total_tokens":7}}`,
- "",
- "data: [DONE]",
- "",
- }, "\n")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid_msg_chat_stream"}},
- Body: io.NopCloser(strings.NewReader(upstreamBody)),
- }}
- svc := &OpenAIGatewayService{
- cfg: rawChatCompletionsTestConfig(),
- httpUpstream: upstream,
- }
- result, err := svc.ForwardAsAnthropic(context.Background(), c, forceChatMessagesFallbackAccount(), body, "", "")
- require.NoError(t, err)
- require.NotNil(t, result)
- require.True(t, gjson.GetBytes(upstream.lastBody, "stream_options.include_usage").Bool())
- out := rec.Body.String()
- require.Contains(t, out, "event: message_start")
- require.Contains(t, out, `"text":"he"`)
- require.Contains(t, out, `"text":"llo"`)
- require.Contains(t, out, "event: content_block_stop")
- require.Contains(t, out, `"stop_reason":"end_turn"`)
- require.Contains(t, out, "event: message_stop")
- blockStop := strings.Index(out, "event: content_block_stop")
- msgDelta := strings.Index(out, `"stop_reason":"end_turn"`)
- msgStop := strings.Index(out, "event: message_stop")
- require.Greater(t, msgDelta, blockStop, "content_block_stop must precede message_delta")
- require.Greater(t, msgStop, msgDelta, "message_delta must precede message_stop")
- require.Equal(t, 4, result.Usage.InputTokens)
- require.Equal(t, 3, result.Usage.OutputTokens)
- require.True(t, result.Stream)
- require.NotNil(t, result.FirstTokenMs)
- }
- // Covers multi-chunk tool_call fragments aggregated by index and finalized as
- // an Anthropic tool_use block with stop_reason=tool_use.
- func TestForwardAsAnthropic_ForceChatCompletionsStreamingToolCallAggregation(t *testing.T) {
- gin.SetMode(gin.TestMode)
- body := []byte(`{"model":"gpt-5.4","max_tokens":32,"messages":[{"role":"user","content":"weather in sf?"}],"stream":true}`)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
- c.Request.Header.Set("Content-Type", "application/json")
- upstreamBody := strings.Join([]string{
- `data: {"id":"chatcmpl_t","object":"chat.completion.chunk","model":"gpt-5.4","choices":[{"index":0,"delta":{"role":"assistant","tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"get_weather","arguments":""}}]},"finish_reason":null}]}`,
- "",
- `data: {"id":"chatcmpl_t","object":"chat.completion.chunk","model":"gpt-5.4","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":"}}]},"finish_reason":null}]}`,
- "",
- `data: {"id":"chatcmpl_t","object":"chat.completion.chunk","model":"gpt-5.4","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"sf\"}"}}]},"finish_reason":null}]}`,
- "",
- `data: {"id":"chatcmpl_t","object":"chat.completion.chunk","model":"gpt-5.4","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}`,
- "",
- `data: {"id":"chatcmpl_t","object":"chat.completion.chunk","model":"gpt-5.4","choices":[],"usage":{"prompt_tokens":6,"completion_tokens":5,"total_tokens":11}}`,
- "",
- "data: [DONE]",
- "",
- }, "\n")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid_msg_chat_tool"}},
- Body: io.NopCloser(strings.NewReader(upstreamBody)),
- }}
- svc := &OpenAIGatewayService{
- cfg: rawChatCompletionsTestConfig(),
- httpUpstream: upstream,
- }
- result, err := svc.ForwardAsAnthropic(context.Background(), c, forceChatMessagesFallbackAccount(), body, "", "")
- require.NoError(t, err)
- require.NotNil(t, result)
- out := rec.Body.String()
- require.Contains(t, out, `"type":"tool_use"`)
- require.Contains(t, out, `"name":"get_weather"`)
- require.Contains(t, out, `"input_json_delta"`)
- require.Contains(t, out, `"stop_reason":"tool_use"`)
- require.Contains(t, out, "event: message_stop")
- require.Equal(t, 6, result.Usage.InputTokens)
- require.Equal(t, 5, result.Usage.OutputTokens)
- }
- // finish_reason=length must survive the double conversion (CC → Responses →
- // Anthropic) as stop_reason=max_tokens.
- func TestForwardAsAnthropic_ForceChatCompletionsStreamingLengthMapsToMaxTokens(t *testing.T) {
- gin.SetMode(gin.TestMode)
- body := []byte(`{"model":"gpt-5.4","max_tokens":8,"messages":[{"role":"user","content":"hello"}],"stream":true}`)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
- c.Request.Header.Set("Content-Type", "application/json")
- upstreamBody := strings.Join([]string{
- `data: {"id":"chatcmpl_l","object":"chat.completion.chunk","model":"gpt-5.4","choices":[{"index":0,"delta":{"role":"assistant","content":"truncat"},"finish_reason":null}]}`,
- "",
- `data: {"id":"chatcmpl_l","object":"chat.completion.chunk","model":"gpt-5.4","choices":[{"index":0,"delta":{},"finish_reason":"length"}],"usage":{"prompt_tokens":4,"completion_tokens":8,"total_tokens":12}}`,
- "",
- "data: [DONE]",
- "",
- }, "\n")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid_msg_chat_len"}},
- Body: io.NopCloser(strings.NewReader(upstreamBody)),
- }}
- svc := &OpenAIGatewayService{
- cfg: rawChatCompletionsTestConfig(),
- httpUpstream: upstream,
- }
- result, err := svc.ForwardAsAnthropic(context.Background(), c, forceChatMessagesFallbackAccount(), body, "", "")
- require.NoError(t, err)
- require.NotNil(t, result)
- out := rec.Body.String()
- require.Contains(t, out, `"stop_reason":"max_tokens"`)
- require.Contains(t, out, "event: message_stop")
- }
- // An upstream that ends immediately with [DONE] must still produce a fully
- // framed (message_start → message_delta → message_stop) Anthropic stream.
- func TestForwardAsAnthropic_ForceChatCompletionsEmptyStreamStillFramesMessage(t *testing.T) {
- gin.SetMode(gin.TestMode)
- body := []byte(`{"model":"gpt-5.4","max_tokens":8,"messages":[{"role":"user","content":"hello"}],"stream":true}`)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
- c.Request.Header.Set("Content-Type", "application/json")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid_msg_chat_empty"}},
- Body: io.NopCloser(strings.NewReader("data: [DONE]\n\n")),
- }}
- svc := &OpenAIGatewayService{
- cfg: rawChatCompletionsTestConfig(),
- httpUpstream: upstream,
- }
- result, err := svc.ForwardAsAnthropic(context.Background(), c, forceChatMessagesFallbackAccount(), body, "", "")
- require.NoError(t, err)
- require.NotNil(t, result)
- out := rec.Body.String()
- require.Contains(t, out, "event: message_start")
- require.Contains(t, out, "event: message_delta")
- require.Contains(t, out, "event: message_stop")
- }
- // Non-failover 4xx responses must go through the shared compat error handler:
- // status-specific Anthropic error type, upstream message preserved, and ops
- // upstream-error events recorded (previously this branch bypassed all three).
- func TestForwardAsAnthropic_ForceChatCompletionsNonFailover400UsesSharedErrorHandler(t *testing.T) {
- gin.SetMode(gin.TestMode)
- body := []byte(`{"model":"gpt-5.4","max_tokens":8,"messages":[{"role":"user","content":"hello"}],"stream":false}`)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
- c.Request.Header.Set("Content-Type", "application/json")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusBadRequest,
- Header: http.Header{"Content-Type": []string{"application/json"}, "x-request-id": []string{"rid_msg_chat_400"}},
- Body: io.NopCloser(strings.NewReader(`{"error":{"message":"invalid roles","type":"invalid_request_error"}}`)),
- }}
- svc := &OpenAIGatewayService{
- cfg: rawChatCompletionsTestConfig(),
- httpUpstream: upstream,
- }
- result, err := svc.ForwardAsAnthropic(context.Background(), c, forceChatMessagesFallbackAccount(), body, "", "")
- require.Error(t, err)
- require.Nil(t, result)
- require.Equal(t, http.StatusBadRequest, rec.Code)
- require.Equal(t, "error", gjson.Get(rec.Body.String(), "type").String())
- require.Equal(t, "invalid_request_error", gjson.Get(rec.Body.String(), "error.type").String())
- require.Equal(t, "invalid roles", gjson.Get(rec.Body.String(), "error.message").String())
- statusVal, ok := c.Get(OpsUpstreamStatusCodeKey)
- require.True(t, ok, "shared handler must record the upstream status for ops")
- require.Equal(t, http.StatusBadRequest, statusVal)
- eventsVal, ok := c.Get(OpsUpstreamErrorsKey)
- require.True(t, ok, "shared handler must append an ops upstream error event")
- events, castOK := eventsVal.([]*OpsUpstreamErrorEvent)
- require.True(t, castOK)
- require.Len(t, events, 1)
- require.Equal(t, http.StatusBadRequest, events[0].UpstreamStatusCode)
- require.Equal(t, "http_error", events[0].Kind)
- require.Equal(t, "invalid roles", events[0].Message)
- }
- // A broken upstream read mid-stream must surface an error and must NOT emit a
- // synthetic message_stop that would disguise the truncation as a completion.
- func TestForwardAsAnthropic_ForceChatCompletionsStreamReadErrorSkipsFinalize(t *testing.T) {
- gin.SetMode(gin.TestMode)
- body := []byte(`{"model":"gpt-5.4","max_tokens":8,"messages":[{"role":"user","content":"hello"}],"stream":true}`)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
- c.Request.Header.Set("Content-Type", "application/json")
- partial := strings.Join([]string{
- `data: {"id":"chatcmpl_e","object":"chat.completion.chunk","model":"gpt-5.4","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}`,
- "",
- `data: {"id":"chatcmpl_e","object":"chat.completion.chunk","model":"gpt-5.4","choices":[{"index":0,"delta":{"content":"he"},"finish_reason":null}]}`,
- "",
- "",
- }, "\n")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid_msg_chat_err"}},
- Body: &errTailReader{data: []byte(partial), err: errors.New("simulated upstream read failure")},
- }}
- svc := &OpenAIGatewayService{
- cfg: rawChatCompletionsTestConfig(),
- httpUpstream: upstream,
- }
- result, err := svc.ForwardAsAnthropic(context.Background(), c, forceChatMessagesFallbackAccount(), body, "", "")
- require.Error(t, err)
- require.Contains(t, err.Error(), "stream usage incomplete")
- require.NotNil(t, result)
- require.True(t, result.Stream)
- out := rec.Body.String()
- require.Contains(t, out, `"text":"he"`, "delta emitted before the failure must reach the client")
- require.NotContains(t, out, "event: message_stop", "no synthetic completion after a broken read")
- }
- // Gate regression: an API-key account whose upstream is confirmed to support
- // the Responses API must keep using /v1/responses, never the CC fallback.
- func TestForwardAsAnthropic_ResponsesSupportedAccountStillUsesResponsesEndpoint(t *testing.T) {
- gin.SetMode(gin.TestMode)
- body := []byte(`{"model":"gpt-5.4","max_tokens":16,"messages":[{"role":"user","content":"hello"}],"output_config":{"effort":"high"},"stream":false}`)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
- c.Request.Header.Set("Content-Type", "application/json")
- c.Request.Header.Set("User-Agent", "third-party-client/1.0.0")
- c.Request.Header.Set("originator", "opencode")
- upstreamBody := strings.Join([]string{
- `data: {"type":"response.completed","response":{"id":"resp_native","object":"response","model":"gpt-5.4","status":"completed","output":[{"type":"message","id":"msg_1","role":"assistant","status":"completed","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":5,"output_tokens":2,"total_tokens":7}}}`,
- "",
- "data: [DONE]",
- "",
- }, "\n")
- upstream := &httpUpstreamRecorder{resp: &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid_msg_native"}},
- Body: io.NopCloser(strings.NewReader(upstreamBody)),
- }}
- svc := &OpenAIGatewayService{
- cfg: rawChatCompletionsTestConfig(),
- httpUpstream: upstream,
- }
- account := rawChatCompletionsTestAccount()
- account.Extra = map[string]any{
- openai_compat.ExtraKeyResponsesMode: string(openai_compat.ResponsesSupportModeAuto),
- openai_compat.ExtraKeyResponsesSupported: true,
- }
- ctx := WithOpenAIReasoningEffortPolicy(context.Background(), "medium", nil, "")
- result, err := svc.ForwardAsAnthropic(ctx, c, account, body, "", "")
- require.NoError(t, err)
- require.NotNil(t, result)
- require.True(t, strings.HasSuffix(upstream.lastReq.URL.Path, "/responses"),
- "responses-capable account must stay on /v1/responses, got %s", upstream.lastReq.URL.String())
- require.True(t, gjson.GetBytes(upstream.lastBody, "input").Exists())
- require.Equal(t, "medium", gjson.GetBytes(upstream.lastBody, "reasoning.effort").String())
- require.NotNil(t, result.ReasoningEffort)
- require.Equal(t, "medium", *result.ReasoningEffort)
- require.False(t, gjson.GetBytes(upstream.lastBody, "messages").Exists())
- require.Equal(t, "third-party-client/1.0.0", upstream.lastReq.Header.Get("User-Agent"))
- require.Equal(t, "opencode", upstream.lastReq.Header.Get("originator"))
- require.Empty(t, upstream.lastReq.Header.Get("version"))
- require.Empty(t, upstream.lastReq.Header.Get("OpenAI-Beta"))
- require.Equal(t, "ok", gjson.Get(rec.Body.String(), "content.0.text").String())
- }
|