@@ -936,6 +936,113 @@ func TestClaudeExecutor_GeneratesNewUserIDByDefault(t *testing.T) {
936936 }
937937}
938938
939+ func TestClaudeExecutor_ExecuteOpenAINonStreamRejectsEmptyClaudeStream (t * testing.T ) {
940+ _ , err := executeOpenAIChatCompletionThroughClaude (t , "" )
941+ if err == nil {
942+ t .Fatal ("Execute error = nil, want empty stream error" )
943+ }
944+ assertStatusErr (t , err , http .StatusBadGateway )
945+ if ! strings .Contains (err .Error (), "empty stream response" ) {
946+ t .Fatalf ("Execute error = %q, want empty stream response" , err .Error ())
947+ }
948+ }
949+
950+ func TestClaudeExecutor_ExecuteOpenAINonStreamRejectsClaudeErrorEvent (t * testing.T ) {
951+ body := `data: {"type":"error","error":{"type":"overloaded_error","message":"upstream overloaded"}}` + "\n "
952+ _ , err := executeOpenAIChatCompletionThroughClaude (t , body )
953+ if err == nil {
954+ t .Fatal ("Execute error = nil, want upstream error event" )
955+ }
956+ assertStatusErr (t , err , http .StatusBadGateway )
957+ if ! strings .Contains (err .Error (), "upstream overloaded" ) {
958+ t .Fatalf ("Execute error = %q, want upstream overloaded" , err .Error ())
959+ }
960+ }
961+
962+ func TestClaudeExecutor_ExecuteOpenAINonStreamRejectsIncompleteClaudeStream (t * testing.T ) {
963+ body := strings .Join ([]string {
964+ `data: {"type":"message_start","message":{"id":"msg_123","model":"claude-3-5-sonnet-20241022"}}` ,
965+ `data: {"type":"message_stop"}` ,
966+ `` ,
967+ }, "\n " )
968+
969+ _ , err := executeOpenAIChatCompletionThroughClaude (t , body )
970+ if err == nil {
971+ t .Fatal ("Execute error = nil, want incomplete stream error" )
972+ }
973+ assertStatusErr (t , err , http .StatusBadGateway )
974+ if ! strings .Contains (err .Error (), "ended before message completion" ) {
975+ t .Fatalf ("Execute error = %q, want incomplete stream error" , err .Error ())
976+ }
977+ }
978+
979+ func TestClaudeExecutor_ExecuteOpenAINonStreamConvertsValidClaudeStream (t * testing.T ) {
980+ body := strings .Join ([]string {
981+ `event: message_start` ,
982+ `data: {"type":"message_start","message":{"id":"msg_123","model":"claude-3-5-sonnet-20241022"}}` ,
983+ `event: content_block_delta` ,
984+ `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}}` ,
985+ `event: message_delta` ,
986+ `data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"input_tokens":2,"output_tokens":1}}` ,
987+ `event: message_stop` ,
988+ `data: {"type":"message_stop"}` ,
989+ `` ,
990+ }, "\n " )
991+
992+ resp , err := executeOpenAIChatCompletionThroughClaude (t , body )
993+ if err != nil {
994+ t .Fatalf ("Execute error: %v" , err )
995+ }
996+ if got := gjson .GetBytes (resp .Payload , "id" ).String (); got != "msg_123" {
997+ t .Fatalf ("response id = %q, want msg_123; payload=%s" , got , string (resp .Payload ))
998+ }
999+ if got := gjson .GetBytes (resp .Payload , "model" ).String (); got != "claude-3-5-sonnet-20241022" {
1000+ t .Fatalf ("response model = %q, want claude-3-5-sonnet-20241022" , got )
1001+ }
1002+ if got := gjson .GetBytes (resp .Payload , "choices.0.message.content" ).String (); got != "ok" {
1003+ t .Fatalf ("response content = %q, want ok" , got )
1004+ }
1005+ if got := gjson .GetBytes (resp .Payload , "usage.total_tokens" ).Int (); got != 3 {
1006+ t .Fatalf ("usage.total_tokens = %d, want 3" , got )
1007+ }
1008+ }
1009+
1010+ func executeOpenAIChatCompletionThroughClaude (t * testing.T , upstreamBody string ) (cliproxyexecutor.Response , error ) {
1011+ t .Helper ()
1012+
1013+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
1014+ w .Header ().Set ("Content-Type" , "text/event-stream" )
1015+ _ , _ = w .Write ([]byte (upstreamBody ))
1016+ }))
1017+ defer server .Close ()
1018+
1019+ executor := NewClaudeExecutor (& config.Config {})
1020+ auth := & cliproxyauth.Auth {Attributes : map [string ]string {
1021+ "api_key" : "key-123" ,
1022+ "base_url" : server .URL ,
1023+ }}
1024+ payload := []byte (`{"model":"claude-3-5-sonnet-20241022","messages":[{"role":"user","content":"hi"}]}` )
1025+
1026+ return executor .Execute (context .Background (), auth , cliproxyexecutor.Request {
1027+ Model : "claude-3-5-sonnet-20241022" ,
1028+ Payload : payload ,
1029+ }, cliproxyexecutor.Options {
1030+ SourceFormat : sdktranslator .FromString ("openai" ),
1031+ })
1032+ }
1033+
1034+ func assertStatusErr (t * testing.T , err error , want int ) {
1035+ t .Helper ()
1036+
1037+ status , ok := err .(interface { StatusCode () int })
1038+ if ! ok {
1039+ t .Fatalf ("error %T does not expose StatusCode" , err )
1040+ }
1041+ if got := status .StatusCode (); got != want {
1042+ t .Fatalf ("StatusCode() = %d, want %d" , got , want )
1043+ }
1044+ }
1045+
9391046func TestStripClaudeToolPrefixFromResponse_NestedToolReference (t * testing.T ) {
9401047 input := []byte (`{"content":[{"type":"tool_result","tool_use_id":"toolu_123","content":[{"type":"tool_reference","tool_name":"proxy_mcp__nia__manage_resource"}]}]}` )
9411048 out := stripClaudeToolPrefixFromResponse (input , "proxy_" )
0 commit comments