| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086 |
- package service
- import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "io"
- "net"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/config"
- coderws "github.com/coder/websocket"
- "github.com/stretchr/testify/require"
- "github.com/tidwall/gjson"
- )
- func TestIsOpenAIWSClientDisconnectError(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- err error
- want bool
- }{
- {name: "nil", err: nil, want: false},
- {name: "io_eof", err: io.EOF, want: true},
- {name: "net_closed", err: net.ErrClosed, want: true},
- {name: "context_canceled", err: context.Canceled, want: true},
- {name: "ws_normal_closure", err: coderws.CloseError{Code: coderws.StatusNormalClosure}, want: true},
- {name: "ws_going_away", err: coderws.CloseError{Code: coderws.StatusGoingAway}, want: true},
- {name: "ws_no_status", err: coderws.CloseError{Code: coderws.StatusNoStatusRcvd}, want: true},
- {name: "ws_abnormal_1006", err: coderws.CloseError{Code: coderws.StatusAbnormalClosure}, want: true},
- {name: "ws_policy_violation", err: coderws.CloseError{Code: coderws.StatusPolicyViolation}, want: false},
- {name: "wrapped_eof_message", err: errors.New("failed to get reader: failed to read frame header: EOF"), want: true},
- {name: "connection_reset_by_peer", err: errors.New("failed to read frame header: read tcp 127.0.0.1:1234->127.0.0.1:5678: read: connection reset by peer"), want: true},
- {name: "windows_connection_reset", err: errors.New("failed to get reader: failed to read frame header: read tcp 127.0.0.1:1234->127.0.0.1:5678: wsarecv: An existing connection was forcibly closed by the remote host."), want: true},
- {name: "broken_pipe", err: errors.New("write tcp 127.0.0.1:1234->127.0.0.1:5678: write: broken pipe"), want: true},
- }
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- require.Equal(t, tt.want, isOpenAIWSClientDisconnectError(tt.err))
- })
- }
- }
- func TestIsOpenAIWSIngressPreviousResponseNotFound(t *testing.T) {
- t.Parallel()
- require.False(t, isOpenAIWSIngressPreviousResponseNotFound(nil))
- require.False(t, isOpenAIWSIngressPreviousResponseNotFound(errors.New("plain error")))
- require.False(t, isOpenAIWSIngressPreviousResponseNotFound(
- wrapOpenAIWSIngressTurnError("read_upstream", errors.New("upstream read failed"), false),
- ))
- require.False(t, isOpenAIWSIngressPreviousResponseNotFound(
- wrapOpenAIWSIngressTurnError(openAIWSIngressStagePreviousResponseNotFound, errors.New("previous response not found"), true),
- ))
- require.True(t, isOpenAIWSIngressPreviousResponseNotFound(
- wrapOpenAIWSIngressTurnError(openAIWSIngressStagePreviousResponseNotFound, errors.New("previous response not found"), false),
- ))
- }
- func TestOpenAIWSIngressPreviousResponseRecoveryEnabled(t *testing.T) {
- t.Parallel()
- var nilService *OpenAIGatewayService
- require.True(t, nilService.openAIWSIngressPreviousResponseRecoveryEnabled(), "nil service should default to enabled")
- svcWithNilCfg := &OpenAIGatewayService{}
- require.True(t, svcWithNilCfg.openAIWSIngressPreviousResponseRecoveryEnabled(), "nil config should default to enabled")
- svc := &OpenAIGatewayService{
- cfg: &config.Config{},
- }
- require.False(t, svc.openAIWSIngressPreviousResponseRecoveryEnabled(), "explicit config default should be false")
- svc.cfg.Gateway.OpenAIWS.IngressPreviousResponseRecoveryEnabled = true
- require.True(t, svc.openAIWSIngressPreviousResponseRecoveryEnabled())
- }
- func TestDropPreviousResponseIDFromRawPayload(t *testing.T) {
- t.Parallel()
- t.Run("empty_payload", func(t *testing.T) {
- updated, removed, err := dropPreviousResponseIDFromRawPayload(nil)
- require.NoError(t, err)
- require.False(t, removed)
- require.Empty(t, updated)
- })
- t.Run("payload_without_previous_response_id", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","model":"gpt-5.1"}`)
- updated, removed, err := dropPreviousResponseIDFromRawPayload(payload)
- require.NoError(t, err)
- require.False(t, removed)
- require.Equal(t, string(payload), string(updated))
- })
- t.Run("normal_delete_success", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","model":"gpt-5.1","previous_response_id":"resp_abc"}`)
- updated, removed, err := dropPreviousResponseIDFromRawPayload(payload)
- require.NoError(t, err)
- require.True(t, removed)
- require.False(t, gjson.GetBytes(updated, "previous_response_id").Exists())
- })
- t.Run("duplicate_keys_are_removed", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","previous_response_id":"resp_a","input":[],"previous_response_id":"resp_b"}`)
- updated, removed, err := dropPreviousResponseIDFromRawPayload(payload)
- require.NoError(t, err)
- require.True(t, removed)
- require.False(t, gjson.GetBytes(updated, "previous_response_id").Exists())
- })
- t.Run("nil_delete_fn_uses_default_delete_logic", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","model":"gpt-5.1","previous_response_id":"resp_abc"}`)
- updated, removed, err := dropPreviousResponseIDFromRawPayloadWithDeleteFn(payload, nil)
- require.NoError(t, err)
- require.True(t, removed)
- require.False(t, gjson.GetBytes(updated, "previous_response_id").Exists())
- })
- t.Run("delete_error", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","model":"gpt-5.1","previous_response_id":"resp_abc"}`)
- updated, removed, err := dropPreviousResponseIDFromRawPayloadWithDeleteFn(payload, func(_ []byte, _ string) ([]byte, error) {
- return nil, errors.New("delete failed")
- })
- require.Error(t, err)
- require.False(t, removed)
- require.Equal(t, string(payload), string(updated))
- })
- t.Run("malformed_json_is_still_best_effort_deleted", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","previous_response_id":"resp_abc"`)
- require.True(t, gjson.GetBytes(payload, "previous_response_id").Exists())
- updated, removed, err := dropPreviousResponseIDFromRawPayload(payload)
- require.NoError(t, err)
- require.True(t, removed)
- require.False(t, gjson.GetBytes(updated, "previous_response_id").Exists())
- })
- }
- func TestStripCodexSparkImageGenerationToolFromRawPayload(t *testing.T) {
- t.Run("strips_image_generation_for_spark", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","model":"gpt-5.3-codex-spark","tools":[{"type":"function","name":"shell"},{"type":"image_generation","output_format":"png"}]}`)
- updated, changed, err := stripCodexSparkImageGenerationToolFromRawPayload(payload, "gpt-5.3-codex-spark")
- require.NoError(t, err)
- require.True(t, changed)
- require.False(t, gjson.GetBytes(updated, `tools.#(type=="image_generation")`).Exists())
- require.True(t, gjson.GetBytes(updated, `tools.#(type=="function")`).Exists())
- })
- t.Run("strips_namespace_tools_for_spark", func(t *testing.T) {
- payload := []byte(`{
- "type":"response.create",
- "model":"gpt-5.3-codex-spark",
- "input":[
- {"type":"message","role":"user","content":"hello"},
- {"type":"additional_tools","tools":[{"type":"namespace","name":"image_gen"}]}
- ],
- "tool_choice":{"type":"namespace","name":"image_gen"}
- }`)
- updated, changed, err := stripCodexSparkImageGenerationToolFromRawPayload(payload, "gpt-5.3-codex-spark")
- require.NoError(t, err)
- require.True(t, changed)
- require.False(t, IsImageGenerationIntent(openAIResponsesEndpoint, "gpt-5.3-codex-spark", updated))
- require.Equal(t, "hello", gjson.GetBytes(updated, "input.0.content").String())
- require.False(t, gjson.GetBytes(updated, "tool_choice").Exists())
- })
- t.Run("keeps_image_generation_for_non_spark", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","model":"gpt-5.3-codex","tools":[{"type":"image_generation","output_format":"png"}]}`)
- updated, changed, err := stripCodexSparkImageGenerationToolFromRawPayload(payload, "gpt-5.3-codex")
- require.NoError(t, err)
- require.False(t, changed)
- require.Equal(t, string(payload), string(updated))
- })
- t.Run("noop_when_no_image_tool", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","model":"gpt-5.3-codex-spark","tools":[{"type":"function","name":"shell"}]}`)
- updated, changed, err := stripCodexSparkImageGenerationToolFromRawPayload(payload, "gpt-5.3-codex-spark")
- require.NoError(t, err)
- require.False(t, changed)
- require.Equal(t, string(payload), string(updated))
- })
- }
- func TestStripOpenAIImageGenerationToolsFromRawPayload(t *testing.T) {
- t.Run("flat image tool", func(t *testing.T) {
- payload := []byte(`{
- "type":"response.create",
- "model":"gpt-5.4",
- "tools":[
- {"type":"function","name":"shell"},
- {"type":"image_generation","output_format":"png"}
- ],
- "tool_choice":{"type":"image_generation"}
- }`)
- updated, changed, err := stripOpenAIImageGenerationToolsFromRawPayload(payload)
- require.NoError(t, err)
- require.True(t, changed)
- require.False(t, gjson.GetBytes(updated, `tools.#(type=="image_generation")`).Exists())
- require.True(t, gjson.GetBytes(updated, `tools.#(type=="function")`).Exists())
- require.False(t, gjson.GetBytes(updated, "tool_choice").Exists())
- })
- t.Run("namespace and Responses Lite tools", func(t *testing.T) {
- payload := []byte(`{
- "type":"response.create",
- "model":"gpt-5.5",
- "tools":[
- {"type":"namespace","name":"image_gen","tools":[{"type":"function","name":"imagegen"}]},
- {"type":"namespace","name":"code_tools","tools":[{"type":"function","name":"run"}]}
- ],
- "input":[
- {"type":"message","role":"user","content":"hello"},
- {"type":"additional_tools","tools":[{"type":"namespace","name":"image_gen"}]}
- ],
- "tool_choice":{"type":"namespace","name":"image_gen"}
- }`)
- updated, changed, err := stripOpenAIImageGenerationToolsFromRawPayload(payload)
- require.NoError(t, err)
- require.True(t, changed)
- require.False(t, IsImageGenerationIntent(openAIResponsesEndpoint, "gpt-5.5", updated))
- require.True(t, gjson.GetBytes(updated, `tools.#(name=="code_tools")`).Exists())
- require.Equal(t, "hello", gjson.GetBytes(updated, "input.0.content").String())
- require.False(t, gjson.GetBytes(updated, "tool_choice").Exists())
- })
- t.Run("non-image namespace is unchanged", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","model":"gpt-5.5","tools":[{"type":"namespace","name":"code_tools"}]}`)
- updated, changed, err := stripOpenAIImageGenerationToolsFromRawPayload(payload)
- require.NoError(t, err)
- require.False(t, changed)
- require.Equal(t, payload, updated)
- })
- }
- func TestAlignStoreDisabledPreviousResponseID(t *testing.T) {
- t.Parallel()
- t.Run("empty_payload", func(t *testing.T) {
- updated, changed, err := alignStoreDisabledPreviousResponseID(nil, "resp_target")
- require.NoError(t, err)
- require.False(t, changed)
- require.Empty(t, updated)
- })
- t.Run("empty_expected", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","previous_response_id":"resp_old"}`)
- updated, changed, err := alignStoreDisabledPreviousResponseID(payload, "")
- require.NoError(t, err)
- require.False(t, changed)
- require.Equal(t, string(payload), string(updated))
- })
- t.Run("missing_previous_response_id", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","model":"gpt-5.1"}`)
- updated, changed, err := alignStoreDisabledPreviousResponseID(payload, "resp_target")
- require.NoError(t, err)
- require.False(t, changed)
- require.Equal(t, string(payload), string(updated))
- })
- t.Run("already_aligned", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","previous_response_id":"resp_target"}`)
- updated, changed, err := alignStoreDisabledPreviousResponseID(payload, "resp_target")
- require.NoError(t, err)
- require.False(t, changed)
- require.Equal(t, "resp_target", gjson.GetBytes(updated, "previous_response_id").String())
- })
- t.Run("mismatch_rewrites_to_expected", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","previous_response_id":"resp_old","input":[]}`)
- updated, changed, err := alignStoreDisabledPreviousResponseID(payload, "resp_target")
- require.NoError(t, err)
- require.True(t, changed)
- require.Equal(t, "resp_target", gjson.GetBytes(updated, "previous_response_id").String())
- })
- t.Run("duplicate_keys_rewrites_to_single_expected", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","previous_response_id":"resp_old_1","input":[],"previous_response_id":"resp_old_2"}`)
- updated, changed, err := alignStoreDisabledPreviousResponseID(payload, "resp_target")
- require.NoError(t, err)
- require.True(t, changed)
- require.Equal(t, "resp_target", gjson.GetBytes(updated, "previous_response_id").String())
- })
- }
- func TestSetPreviousResponseIDToRawPayload(t *testing.T) {
- t.Parallel()
- t.Run("empty_payload", func(t *testing.T) {
- updated, err := setPreviousResponseIDToRawPayload(nil, "resp_target")
- require.NoError(t, err)
- require.Empty(t, updated)
- })
- t.Run("empty_previous_response_id", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","model":"gpt-5.1"}`)
- updated, err := setPreviousResponseIDToRawPayload(payload, "")
- require.NoError(t, err)
- require.Equal(t, string(payload), string(updated))
- })
- t.Run("set_previous_response_id_when_missing", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","model":"gpt-5.1"}`)
- updated, err := setPreviousResponseIDToRawPayload(payload, "resp_target")
- require.NoError(t, err)
- require.Equal(t, "resp_target", gjson.GetBytes(updated, "previous_response_id").String())
- require.Equal(t, "gpt-5.1", gjson.GetBytes(updated, "model").String())
- })
- t.Run("overwrite_existing_previous_response_id", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","model":"gpt-5.1","previous_response_id":"resp_old"}`)
- updated, err := setPreviousResponseIDToRawPayload(payload, "resp_new")
- require.NoError(t, err)
- require.Equal(t, "resp_new", gjson.GetBytes(updated, "previous_response_id").String())
- })
- }
- func TestShouldInferIngressFunctionCallOutputPreviousResponseID(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- storeDisabled bool
- turn int
- signals ToolContinuationSignals
- currentPreviousResponse string
- expectedPrevious string
- want bool
- }{
- {
- name: "infer_when_all_conditions_match",
- storeDisabled: true,
- turn: 2,
- signals: ToolContinuationSignals{HasFunctionCallOutput: true},
- expectedPrevious: "resp_1",
- want: true,
- },
- {
- name: "skip_when_store_enabled",
- storeDisabled: false,
- turn: 2,
- signals: ToolContinuationSignals{HasFunctionCallOutput: true},
- expectedPrevious: "resp_1",
- want: false,
- },
- {
- name: "skip_on_first_turn",
- storeDisabled: true,
- turn: 1,
- signals: ToolContinuationSignals{HasFunctionCallOutput: true},
- expectedPrevious: "resp_1",
- want: false,
- },
- {
- name: "skip_without_function_call_output",
- storeDisabled: true,
- turn: 2,
- signals: ToolContinuationSignals{},
- expectedPrevious: "resp_1",
- want: false,
- },
- {
- name: "skip_when_request_already_has_previous_response_id",
- storeDisabled: true,
- turn: 2,
- signals: ToolContinuationSignals{HasFunctionCallOutput: true},
- currentPreviousResponse: "resp_client",
- expectedPrevious: "resp_1",
- want: false,
- },
- {
- name: "skip_when_last_turn_response_id_missing",
- storeDisabled: true,
- turn: 2,
- signals: ToolContinuationSignals{HasFunctionCallOutput: true},
- expectedPrevious: "",
- want: false,
- },
- {
- name: "trim_whitespace_before_judgement",
- storeDisabled: true,
- turn: 2,
- signals: ToolContinuationSignals{HasFunctionCallOutput: true},
- expectedPrevious: " resp_2 ",
- want: true,
- },
- {
- name: "skip_when_tool_call_context_already_present",
- storeDisabled: true,
- turn: 2,
- signals: ToolContinuationSignals{HasFunctionCallOutput: true, HasToolCallContext: true},
- expectedPrevious: "resp_2",
- want: false,
- },
- {
- name: "infer_when_only_item_reference_covers_call_ids",
- storeDisabled: true,
- turn: 2,
- signals: ToolContinuationSignals{HasFunctionCallOutput: true, HasItemReferenceForAllCallIDs: true},
- expectedPrevious: "resp_2",
- want: true,
- },
- {
- name: "skip_when_function_call_output_missing_call_id",
- storeDisabled: true,
- turn: 2,
- signals: ToolContinuationSignals{HasFunctionCallOutput: true, HasFunctionCallOutputMissingCallID: true},
- expectedPrevious: "resp_2",
- want: false,
- },
- }
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- got := shouldInferIngressFunctionCallOutputPreviousResponseID(
- tt.storeDisabled,
- tt.turn,
- tt.signals,
- tt.currentPreviousResponse,
- tt.expectedPrevious,
- )
- require.Equal(t, tt.want, got)
- })
- }
- }
- func TestOpenAIWSInputIsPrefixExtended(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- previous []byte
- current []byte
- want bool
- expectErr bool
- }{
- {
- name: "both_missing_input",
- previous: []byte(`{"type":"response.create","model":"gpt-5.1"}`),
- current: []byte(`{"type":"response.create","model":"gpt-5.1","previous_response_id":"resp_1"}`),
- want: true,
- },
- {
- name: "previous_missing_current_empty_array",
- previous: []byte(`{"type":"response.create","model":"gpt-5.1"}`),
- current: []byte(`{"type":"response.create","model":"gpt-5.1","input":[]}`),
- want: true,
- },
- {
- name: "previous_missing_current_non_empty_array",
- previous: []byte(`{"type":"response.create","model":"gpt-5.1"}`),
- current: []byte(`{"type":"response.create","model":"gpt-5.1","input":[{"type":"input_text","text":"hello"}]}`),
- want: false,
- },
- {
- name: "array_prefix_match",
- previous: []byte(`{"input":[{"type":"input_text","text":"hello"}]}`),
- current: []byte(`{"input":[{"text":"hello","type":"input_text"},{"type":"input_text","text":"world"}]}`),
- want: true,
- },
- {
- name: "array_prefix_mismatch",
- previous: []byte(`{"input":[{"type":"input_text","text":"hello"}]}`),
- current: []byte(`{"input":[{"type":"input_text","text":"different"}]}`),
- want: false,
- },
- {
- name: "current_shorter_than_previous",
- previous: []byte(`{"input":[{"type":"input_text","text":"a"},{"type":"input_text","text":"b"}]}`),
- current: []byte(`{"input":[{"type":"input_text","text":"a"}]}`),
- want: false,
- },
- {
- name: "previous_has_input_current_missing",
- previous: []byte(`{"input":[{"type":"input_text","text":"a"}]}`),
- current: []byte(`{"model":"gpt-5.1"}`),
- want: false,
- },
- {
- name: "input_string_treated_as_single_item",
- previous: []byte(`{"input":"hello"}`),
- current: []byte(`{"input":"hello"}`),
- want: true,
- },
- {
- name: "current_invalid_input_json",
- previous: []byte(`{"input":[]}`),
- current: []byte(`{"input":[}`),
- expectErr: true,
- },
- {
- name: "invalid_input_json",
- previous: []byte(`{"input":[}`),
- current: []byte(`{"input":[]}`),
- expectErr: true,
- },
- }
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- got, err := openAIWSInputIsPrefixExtended(tt.previous, tt.current)
- if tt.expectErr {
- require.Error(t, err)
- return
- }
- require.NoError(t, err)
- require.Equal(t, tt.want, got)
- })
- }
- }
- func TestNormalizeOpenAIWSJSONForCompare(t *testing.T) {
- t.Parallel()
- normalized, err := normalizeOpenAIWSJSONForCompare([]byte(`{"b":2,"a":1}`))
- require.NoError(t, err)
- require.Equal(t, `{"a":1,"b":2}`, string(normalized))
- _, err = normalizeOpenAIWSJSONForCompare([]byte(" "))
- require.Error(t, err)
- _, err = normalizeOpenAIWSJSONForCompare([]byte(`{"a":`))
- require.Error(t, err)
- }
- func TestNormalizeOpenAIWSJSONForCompareOrRaw(t *testing.T) {
- t.Parallel()
- require.Equal(t, `{"a":1,"b":2}`, string(normalizeOpenAIWSJSONForCompareOrRaw([]byte(`{"b":2,"a":1}`))))
- require.Equal(t, `{"a":`, string(normalizeOpenAIWSJSONForCompareOrRaw([]byte(`{"a":`))))
- }
- func TestNormalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID(t *testing.T) {
- t.Parallel()
- normalized, err := normalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID(
- []byte(`{"model":"gpt-5.1","input":[1],"previous_response_id":"resp_x","client_metadata":{"request_start_ms":"1"},"stream_options":{"include_usage":true},"generate":false,"metadata":{"b":2,"a":1}}`),
- )
- require.NoError(t, err)
- require.False(t, gjson.GetBytes(normalized, "input").Exists())
- require.False(t, gjson.GetBytes(normalized, "previous_response_id").Exists())
- require.False(t, gjson.GetBytes(normalized, "client_metadata").Exists())
- require.False(t, gjson.GetBytes(normalized, "stream_options").Exists())
- require.False(t, gjson.GetBytes(normalized, "generate").Exists())
- require.Equal(t, float64(1), gjson.GetBytes(normalized, "metadata.a").Float())
- normalized, err = normalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID(
- []byte(`{"model":"gpt-5.1","generate":true}`),
- )
- require.NoError(t, err)
- require.True(t, gjson.GetBytes(normalized, "generate").Bool())
- _, err = normalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID(nil)
- require.Error(t, err)
- _, err = normalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID([]byte(`[]`))
- require.Error(t, err)
- }
- func TestOpenAIWSExtractNormalizedInputSequence(t *testing.T) {
- t.Parallel()
- t.Run("empty_payload", func(t *testing.T) {
- items, exists, err := openAIWSExtractNormalizedInputSequence(nil)
- require.NoError(t, err)
- require.False(t, exists)
- require.Nil(t, items)
- })
- t.Run("input_missing", func(t *testing.T) {
- items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"type":"response.create"}`))
- require.NoError(t, err)
- require.False(t, exists)
- require.Nil(t, items)
- })
- t.Run("input_array", func(t *testing.T) {
- items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"input":[{"type":"input_text","text":"hello"}]}`))
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 1)
- })
- t.Run("input_object", func(t *testing.T) {
- items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"input":{"type":"input_text","text":"hello"}}`))
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 1)
- })
- t.Run("input_string", func(t *testing.T) {
- items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"input":"hello"}`))
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 1)
- require.Equal(t, `"hello"`, string(items[0]))
- })
- t.Run("input_number", func(t *testing.T) {
- items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"input":42}`))
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 1)
- require.Equal(t, "42", string(items[0]))
- })
- t.Run("input_bool", func(t *testing.T) {
- items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"input":true}`))
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 1)
- require.Equal(t, "true", string(items[0]))
- })
- t.Run("input_null", func(t *testing.T) {
- items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"input":null}`))
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 1)
- require.Equal(t, "null", string(items[0]))
- })
- t.Run("input_invalid_array_json", func(t *testing.T) {
- items, exists, err := openAIWSExtractNormalizedInputSequence([]byte(`{"input":[}`))
- require.Error(t, err)
- require.True(t, exists)
- require.Nil(t, items)
- })
- }
- func TestShouldKeepIngressPreviousResponseID(t *testing.T) {
- t.Parallel()
- previousPayload := []byte(`{
- "type":"response.create",
- "model":"gpt-5.1",
- "store":false,
- "tools":[{"type":"function","name":"tool_a"}],
- "input":[{"type":"input_text","text":"hello"}]
- }`)
- currentStrictPayload := []byte(`{
- "type":"response.create",
- "model":"gpt-5.1",
- "store":false,
- "tools":[{"name":"tool_a","type":"function"}],
- "previous_response_id":"resp_turn_1",
- "input":[{"text":"hello","type":"input_text"},{"type":"input_text","text":"world"}]
- }`)
- t.Run("strict_incremental_keep", func(t *testing.T) {
- keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, currentStrictPayload, "resp_turn_1", false)
- require.NoError(t, err)
- require.True(t, keep)
- require.Equal(t, "strict_incremental_ok", reason)
- })
- t.Run("codex_prewarm_to_business_keep", func(t *testing.T) {
- prewarmPayload := []byte(`{
- "type":"response.create",
- "model":"gpt-5.1",
- "store":false,
- "generate":false,
- "client_metadata":{"x-codex-ws-stream-request-start-ms":"100"},
- "stream_options":{"include_usage":true},
- "input":[{"type":"input_text","text":"hello"}]
- }`)
- businessPayload := []byte(`{
- "type":"response.create",
- "model":"gpt-5.1",
- "store":false,
- "client_metadata":{"x-codex-ws-stream-request-start-ms":"200"},
- "previous_response_id":"resp_prewarm",
- "input":[{"type":"input_text","text":"hello"}]
- }`)
- keep, reason, err := shouldKeepIngressPreviousResponseID(
- prewarmPayload,
- businessPayload,
- "resp_prewarm",
- false,
- )
- require.NoError(t, err)
- require.True(t, keep)
- require.Equal(t, "strict_incremental_ok", reason)
- })
- t.Run("missing_previous_response_id", func(t *testing.T) {
- payload := []byte(`{"type":"response.create","model":"gpt-5.1","input":[]}`)
- keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, payload, "resp_turn_1", false)
- require.NoError(t, err)
- require.False(t, keep)
- require.Equal(t, "missing_previous_response_id", reason)
- })
- t.Run("missing_last_turn_response_id", func(t *testing.T) {
- keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, currentStrictPayload, "", false)
- require.NoError(t, err)
- require.False(t, keep)
- require.Equal(t, "missing_last_turn_response_id", reason)
- })
- t.Run("previous_response_id_mismatch", func(t *testing.T) {
- keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, currentStrictPayload, "resp_turn_other", false)
- require.NoError(t, err)
- require.False(t, keep)
- require.Equal(t, "previous_response_id_mismatch", reason)
- })
- t.Run("missing_previous_turn_payload", func(t *testing.T) {
- keep, reason, err := shouldKeepIngressPreviousResponseID(nil, currentStrictPayload, "resp_turn_1", false)
- require.NoError(t, err)
- require.False(t, keep)
- require.Equal(t, "missing_previous_turn_payload", reason)
- })
- t.Run("non_input_changed", func(t *testing.T) {
- payload := []byte(`{
- "type":"response.create",
- "model":"gpt-5.1-mini",
- "store":false,
- "tools":[{"type":"function","name":"tool_a"}],
- "previous_response_id":"resp_turn_1",
- "input":[{"type":"input_text","text":"hello"},{"type":"input_text","text":"world"}]
- }`)
- keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, payload, "resp_turn_1", false)
- require.NoError(t, err)
- require.False(t, keep)
- require.Equal(t, "non_input_changed", reason)
- })
- t.Run("delta_input_keeps_previous_response_id", func(t *testing.T) {
- payload := []byte(`{
- "type":"response.create",
- "model":"gpt-5.1",
- "store":false,
- "tools":[{"type":"function","name":"tool_a"}],
- "previous_response_id":"resp_turn_1",
- "input":[{"type":"input_text","text":"different"}]
- }`)
- keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, payload, "resp_turn_1", false)
- require.NoError(t, err)
- require.True(t, keep)
- require.Equal(t, "strict_incremental_ok", reason)
- })
- t.Run("function_call_output_keeps_previous_response_id", func(t *testing.T) {
- payload := []byte(`{
- "type":"response.create",
- "model":"gpt-5.1",
- "store":false,
- "previous_response_id":"resp_external",
- "input":[{"type":"function_call_output","call_id":"call_1","output":"ok"}]
- }`)
- keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, payload, "resp_turn_1", true)
- require.NoError(t, err)
- require.True(t, keep)
- require.Equal(t, "has_function_call_output", reason)
- })
- t.Run("non_input_compare_error", func(t *testing.T) {
- keep, reason, err := shouldKeepIngressPreviousResponseID([]byte(`[]`), currentStrictPayload, "resp_turn_1", false)
- require.Error(t, err)
- require.False(t, keep)
- require.Equal(t, "non_input_compare_error", reason)
- })
- t.Run("current_payload_compare_error", func(t *testing.T) {
- keep, reason, err := shouldKeepIngressPreviousResponseID(previousPayload, []byte(`{"previous_response_id":"resp_turn_1","input":[}`), "resp_turn_1", false)
- require.Error(t, err)
- require.False(t, keep)
- require.Equal(t, "non_input_compare_error", reason)
- })
- }
- func TestBuildOpenAIWSReplayInputSequence(t *testing.T) {
- t.Parallel()
- lastFull := []json.RawMessage{
- json.RawMessage(`{"type":"input_text","text":"hello"}`),
- }
- t.Run("no_previous_response_id_use_current", func(t *testing.T) {
- items, exists, err := buildOpenAIWSReplayInputSequence(
- lastFull,
- true,
- []byte(`{"input":[{"type":"input_text","text":"new"}]}`),
- false,
- )
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 1)
- require.Equal(t, "new", gjson.GetBytes(items[0], "text").String())
- })
- t.Run("no_previous_response_id_custom_tool_history_does_not_accumulate", func(t *testing.T) {
- previousFull := []json.RawMessage{
- json.RawMessage(`{"type":"input_text","text":"stale"}`),
- json.RawMessage(`{"type":"custom_tool_call","id":"stale_item","call_id":"stale_call","name":"exec","input":"stale"}`),
- }
- currentPayload := []byte(`{"input":[
- {"type":"custom_tool_call","id":"item_1","call_id":"call_1","name":"exec","input":"pwd"},
- {"type":"custom_tool_call_output","call_id":"call_1","output":"/tmp"},
- {"type":"input_text","text":"continue"}
- ]}`)
- for range 3 {
- items, exists, err := buildOpenAIWSReplayInputSequence(
- previousFull,
- true,
- currentPayload,
- false,
- )
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 3)
- require.Equal(t, "custom_tool_call", gjson.GetBytes(items[0], "type").String())
- require.Equal(t, "call_1", gjson.GetBytes(items[0], "call_id").String())
- require.Equal(t, "custom_tool_call_output", gjson.GetBytes(items[1], "type").String())
- require.Equal(t, "call_1", gjson.GetBytes(items[1], "call_id").String())
- previousFull = append(items, json.RawMessage(`{"type":"custom_tool_call","id":"replayed_item","call_id":"replayed_call","name":"exec","input":"ignored"}`))
- }
- })
- t.Run("previous_response_id_delta_append", func(t *testing.T) {
- items, exists, err := buildOpenAIWSReplayInputSequence(
- lastFull,
- true,
- []byte(`{"previous_response_id":"resp_1","input":[{"type":"input_text","text":"world"}]}`),
- true,
- )
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 2)
- require.Equal(t, "hello", gjson.GetBytes(items[0], "text").String())
- require.Equal(t, "world", gjson.GetBytes(items[1], "text").String())
- })
- t.Run("previous_response_id_filters_orphan_historical_custom_tool_call", func(t *testing.T) {
- previousFull := []json.RawMessage{
- json.RawMessage(`{"type":"input_text","text":"hello"}`),
- json.RawMessage(`{"type":"custom_tool_call","id":"item_orphan","call_id":"call_orphan","name":"exec","input":"pwd"}`),
- }
- items, exists, err := buildOpenAIWSReplayInputSequence(
- previousFull,
- true,
- []byte(`{"previous_response_id":"resp_1","input":[{"role":"user","content":"continue"}]}`),
- true,
- )
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 2)
- require.Equal(t, "hello", gjson.GetBytes(items[0], "text").String())
- require.Equal(t, "user", gjson.GetBytes(items[1], "role").String())
- })
- t.Run("previous_response_id_preserves_paired_historical_function_call", func(t *testing.T) {
- previousFull := []json.RawMessage{
- json.RawMessage(`{"type":"function_call","id":"item_1","call_id":"call_1","name":"lookup","arguments":"{}"}`),
- json.RawMessage(`{"type":"function_call_output","call_id":"call_1","output":"ok"}`),
- }
- items, exists, err := buildOpenAIWSReplayInputSequence(
- previousFull,
- true,
- []byte(`{"previous_response_id":"resp_1","input":[{"role":"user","content":"continue"}]}`),
- true,
- )
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 3)
- require.Equal(t, "function_call", gjson.GetBytes(items[0], "type").String())
- require.Equal(t, "function_call_output", gjson.GetBytes(items[1], "type").String())
- })
- t.Run("previous_response_id_preserves_paired_historical_custom_tool_call", func(t *testing.T) {
- previousFull := []json.RawMessage{
- json.RawMessage(`{"type":"custom_tool_call","id":"item_1","call_id":"call_1","name":"exec","input":"pwd"}`),
- json.RawMessage(`{"type":"custom_tool_call_output","call_id":"call_1","output":"/tmp"}`),
- }
- items, exists, err := buildOpenAIWSReplayInputSequence(
- previousFull,
- true,
- []byte(`{"previous_response_id":"resp_1","input":[{"role":"user","content":"continue"}]}`),
- true,
- )
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 3)
- require.Equal(t, "custom_tool_call", gjson.GetBytes(items[0], "type").String())
- require.Equal(t, "custom_tool_call_output", gjson.GetBytes(items[1], "type").String())
- })
- t.Run("item_reference_does_not_complete_historical_call", func(t *testing.T) {
- previousFull := []json.RawMessage{
- json.RawMessage(`{"type":"custom_tool_call","id":"item_1","call_id":"call_1","name":"exec","input":"pwd"}`),
- }
- items, exists, err := buildOpenAIWSReplayInputSequence(
- previousFull,
- true,
- []byte(`{"previous_response_id":"resp_1","input":[{"type":"item_reference","id":"call_1"},{"role":"user","content":"continue"}]}`),
- true,
- )
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 2)
- require.Equal(t, "item_reference", gjson.GetBytes(items[0], "type").String())
- require.Equal(t, "user", gjson.GetBytes(items[1], "role").String())
- })
- t.Run("previous_response_id_preserves_current_orphan_custom_tool_call", func(t *testing.T) {
- items, exists, err := buildOpenAIWSReplayInputSequence(
- lastFull,
- true,
- []byte(`{"previous_response_id":"resp_1","input":[{"type":"custom_tool_call","id":"item_live","call_id":"call_live","name":"exec","input":"pwd"}]}`),
- true,
- )
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 2)
- require.Equal(t, "custom_tool_call", gjson.GetBytes(items[1], "type").String())
- require.Equal(t, "call_live", gjson.GetBytes(items[1], "call_id").String())
- })
- t.Run("previous_response_id_full_input_replace", func(t *testing.T) {
- items, exists, err := buildOpenAIWSReplayInputSequence(
- lastFull,
- true,
- []byte(`{"previous_response_id":"resp_1","input":[{"type":"input_text","text":"hello"},{"type":"input_text","text":"world"}]}`),
- true,
- )
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 2)
- require.Equal(t, "hello", gjson.GetBytes(items[0], "text").String())
- require.Equal(t, "world", gjson.GetBytes(items[1], "text").String())
- })
- }
- func TestOpenAIWSRawPayloadHasToolCallOutput(t *testing.T) {
- t.Parallel()
- for _, typ := range []string{
- "function_call_output",
- "tool_search_output",
- "custom_tool_call_output",
- "mcp_tool_call_output",
- } {
- typ := typ
- t.Run(typ, func(t *testing.T) {
- t.Parallel()
- payload := []byte(`{"input":[{"type":"` + typ + `","call_id":"call_1","output":"ok"}]}`)
- require.True(t, openAIWSRawPayloadHasToolCallOutput(payload))
- })
- }
- t.Run("object_input", func(t *testing.T) {
- t.Parallel()
- payload := []byte(`{"input":{"type":"tool_search_output","call_id":"call_1","output":"ok"}}`)
- require.True(t, openAIWSRawPayloadHasToolCallOutput(payload))
- })
- t.Run("non_tool_output", func(t *testing.T) {
- t.Parallel()
- payload := []byte(`{"input":[{"type":"input_text","text":"hello"}]}`)
- require.False(t, openAIWSRawPayloadHasToolCallOutput(payload))
- })
- }
- func TestSetOpenAIWSPayloadInputSequence(t *testing.T) {
- t.Parallel()
- t.Run("set_items", func(t *testing.T) {
- original := []byte(`{"type":"response.create","previous_response_id":"resp_1"}`)
- items := []json.RawMessage{
- json.RawMessage(`{"type":"input_text","text":"hello"}`),
- json.RawMessage(`{"type":"input_text","text":"world"}`),
- }
- updated, err := setOpenAIWSPayloadInputSequence(original, items, true)
- require.NoError(t, err)
- require.Equal(t, "hello", gjson.GetBytes(updated, "input.0.text").String())
- require.Equal(t, "world", gjson.GetBytes(updated, "input.1.text").String())
- })
- t.Run("preserve_empty_array_not_null", func(t *testing.T) {
- original := []byte(`{"type":"response.create","previous_response_id":"resp_1"}`)
- updated, err := setOpenAIWSPayloadInputSequence(original, nil, true)
- require.NoError(t, err)
- require.True(t, gjson.GetBytes(updated, "input").IsArray())
- require.Len(t, gjson.GetBytes(updated, "input").Array(), 0)
- require.False(t, gjson.GetBytes(updated, "input").Type == gjson.Null)
- })
- }
- func TestCombineOpenAIWSReplayItems(t *testing.T) {
- t.Parallel()
- t.Run("empty_delta_returns_history", func(t *testing.T) {
- history := []json.RawMessage{json.RawMessage(`{"a":1}`)}
- require.Nil(t, combineOpenAIWSReplayItems(nil, nil))
- combined := combineOpenAIWSReplayItems(history, nil)
- require.Len(t, combined, 1)
- })
- t.Run("new_header_shares_bodies", func(t *testing.T) {
- history := []json.RawMessage{json.RawMessage(`{"a":1}`)}
- delta := []json.RawMessage{json.RawMessage(`{"b":2}`)}
- combined := combineOpenAIWSReplayItems(history, delta)
- require.Len(t, combined, 2)
- // 头数组必须是新建的:对 combined 追加不影响 history。
- require.NotSame(t, &history[0], &combined[0])
- // 正文共享:不发生字节级深拷贝。
- require.Same(t, &history[0][0], &combined[0][0])
- require.Same(t, &delta[0][0], &combined[1][0])
- })
- }
- func TestOpenAIWSReplaySequenceSharesBodies(t *testing.T) {
- t.Parallel()
- t.Run("extract_shares_payload_backing_array", func(t *testing.T) {
- payload := []byte(`{"input":[{"type":"input_text","text":"hello"},{"type":"input_text","text":"world"}]}`)
- items, exists, err := openAIWSExtractNormalizedInputSequence(payload)
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 2)
- for _, item := range items {
- start := bytes.Index(payload, []byte(item))
- require.GreaterOrEqual(t, start, 0)
- require.Same(t, &payload[start], &item[0], "extract 应零拷贝共享 payload 底层数组")
- }
- })
- t.Run("build_transfers_current_items_ownership", func(t *testing.T) {
- payload := []byte(`{"input":[{"type":"input_text","text":"hello"}]}`)
- items, exists, err := buildOpenAIWSReplayInputSequence(nil, false, payload, false)
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 1)
- start := bytes.Index(payload, []byte(items[0]))
- require.GreaterOrEqual(t, start, 0)
- require.Same(t, &payload[start], &items[0][0])
- })
- t.Run("build_merge_shares_history_bodies", func(t *testing.T) {
- history := []json.RawMessage{json.RawMessage(`{"type":"input_text","text":"hello"}`)}
- items, exists, err := buildOpenAIWSReplayInputSequence(
- history,
- true,
- []byte(`{"previous_response_id":"resp_1","input":[{"type":"input_text","text":"world"}]}`),
- true,
- )
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 2)
- require.Same(t, &history[0][0], &items[0][0], "历史正文应共享而非深拷贝")
- })
- t.Run("build_prefix_hit_transfers_current_items", func(t *testing.T) {
- history := []json.RawMessage{json.RawMessage(`{"type":"input_text","text":"hello"}`)}
- payload := []byte(`{"previous_response_id":"resp_1","input":[{"type":"input_text","text":"hello"},{"type":"input_text","text":"world"}]}`)
- items, exists, err := buildOpenAIWSReplayInputSequence(history, true, payload, true)
- require.NoError(t, err)
- require.True(t, exists)
- require.Len(t, items, 2)
- start := bytes.Index(payload, []byte(items[1]))
- require.GreaterOrEqual(t, start, 0)
- require.Same(t, &payload[start], &items[1][0], "prefix 命中应转移当前 items 所有权并共享 payload 底层数组")
- })
- }
|