From 72f21989bbc1f2ced3e5dd64bb8180a2609a93c2 Mon Sep 17 00:00:00 2001 From: tomsun28 Date: Thu, 7 Aug 2025 14:09:26 +0800 Subject: [PATCH] chore: update jackson version, add custom headers (#30) --- .gitignore | 1 + core/pom.xml | 2 +- .../java/ai/z/openapi/AbstractAiClient.java | 14 ++ .../java/ai/z/openapi/api/chat/ChatApi.java | 36 ++++ .../java/ai/z/openapi/core/Constants.java | 5 + .../ai/z/openapi/core/config/ZaiConfig.java | 14 ++ ...eptor.java => HttpRequestInterceptor.java} | 20 +- .../z/openapi/service/chat/ChatService.java | 11 ++ .../openapi/service/chat/ChatServiceImpl.java | 35 +++- .../service/file/UploadFilePurpose.java | 17 ++ .../service/model/ChatRequestWithHeaders.java | 32 +++ .../main/java/ai/z/openapi/utils/OkHttps.java | 4 +- .../assistant/AssistantServiceTest.java | 100 +++------- .../service/audio/AudioServiceTest.java | 2 +- .../openapi/service/chat/ChatServiceTest.java | 183 ++++++++++++++++++ ...ChatServiceWithHeadersIntegrationTest.java | 120 ++++++++++++ .../openapi/service/file/FileServiceTest.java | 25 +-- .../service/image/ImageServiceTest.java | 7 +- .../service/videos/VideosServiceTest.java | 29 +-- core/src/test/resources/asr.mp3 | Bin 0 -> 29996 bytes .../ai.z.openapi.samples/AgentExample.java | 2 +- .../AgentVideoExample.java | 2 +- .../ChatAsyncCompletionExample.java | 4 +- .../ChatCompletionExample.java | 4 +- ...hatCompletionWithCustomHeadersExample.java | 90 +++++++++ .../ClientConfigurationExample.java | 28 ++- .../CogVideoX3Example.java | 4 +- .../CogVideoXExample.java | 4 +- .../GLM41VThinkingExample.java | 2 +- .../GLM4VPlusExample.java | 2 +- .../ViduAspectVideoExample.java | 2 +- .../ViduImageToVideoExample.java | 2 +- .../ViduStartEndVideoExample.java | 2 +- .../ViduTextToVideoExample.java | 4 +- .../WebSearchExample.java | 4 +- 35 files changed, 670 insertions(+), 143 deletions(-) rename core/src/main/java/ai/z/openapi/core/token/{AuthenticationInterceptor.java => HttpRequestInterceptor.java} (65%) create mode 100644 core/src/main/java/ai/z/openapi/service/file/UploadFilePurpose.java create mode 100644 core/src/main/java/ai/z/openapi/service/model/ChatRequestWithHeaders.java create mode 100644 core/src/test/java/ai/z/openapi/service/chat/ChatServiceWithHeadersIntegrationTest.java create mode 100644 core/src/test/resources/asr.mp3 create mode 100644 samples/src/main/ai.z.openapi.samples/ChatCompletionWithCustomHeadersExample.java diff --git a/.gitignore b/.gitignore index 792704b..fe42722 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/** !**/src/test/** +.DS_Store ### STS ### .apt_generated diff --git a/core/pom.xml b/core/pom.xml index cb6d4e3..45159ca 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -49,7 +49,7 @@ UTF-8 2.0.11 3.14.9 - 2.11.3 + 2.18.3 2.9.0 3.1.8 4.2.2 diff --git a/core/src/main/java/ai/z/openapi/AbstractAiClient.java b/core/src/main/java/ai/z/openapi/AbstractAiClient.java index 59dfce9..ae240d4 100644 --- a/core/src/main/java/ai/z/openapi/AbstractAiClient.java +++ b/core/src/main/java/ai/z/openapi/AbstractAiClient.java @@ -42,6 +42,7 @@ import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; import retrofit2.converter.jackson.JacksonConverterFactory; import java.lang.reflect.InvocationTargetException; +import java.util.Map; import java.util.concurrent.TimeUnit; /** @@ -477,6 +478,19 @@ public abstract class AbstractAiClient extends AbstractClientBaseService { return self(); } + /** + * Config the custom headers + * @param customHeaders custom headers + * @return this Builder instance for method chaining + */ + public B customHeaders(Map customHeaders) { + if (customHeaders == null || customHeaders.isEmpty()) { + throw new IllegalArgumentException("Custom headers cannot be null or empty"); + } + config.setCustomHeaders(customHeaders); + return self(); + } + /** * Disables token caching, forcing the client to use API keys for direct requests. * This is the default behavior. diff --git a/core/src/main/java/ai/z/openapi/api/chat/ChatApi.java b/core/src/main/java/ai/z/openapi/api/chat/ChatApi.java index ab80b4b..0359bea 100644 --- a/core/src/main/java/ai/z/openapi/api/chat/ChatApi.java +++ b/core/src/main/java/ai/z/openapi/api/chat/ChatApi.java @@ -7,10 +7,13 @@ import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.GET; +import retrofit2.http.HeaderMap; import retrofit2.http.POST; import retrofit2.http.Path; import retrofit2.http.Streaming; +import java.util.Map; + /** * Chat Completions API for advanced GLM-4 series models Provides synchronous, * asynchronous, and streaming chat completion capabilities Supports complex reasoning, @@ -36,6 +39,23 @@ public interface ChatApi { @POST("chat/completions") Call createChatCompletionStream(@Body ChatCompletionCreateParams request); + /** + * Create a streaming chat completion with custom headers support Returns response + * content incrementally through Server-Sent Events (SSE) for immediate user feedback + * Optimized for interactive applications requiring low latency and progressive + * content delivery Supports all GLM-4 models with configurable streaming parameters + * and custom HTTP headers + * @param request Chat completion parameters including model selection, messages, and + * streaming settings + * @param headers Custom HTTP headers to be added to the request + * @return Streaming response body with incremental content, usage statistics, and + * completion indicators + */ + @Streaming + @POST("chat/completions") + Call createChatCompletionStream(@Body ChatCompletionCreateParams request, + @HeaderMap Map headers); + /** * Create an asynchronous chat completion for long-running tasks Submits the request * and returns immediately with a task ID for later result retrieval Ideal for complex @@ -64,6 +84,22 @@ public interface ChatApi { @POST("chat/completions") Single createChatCompletion(@Body ChatCompletionCreateParams request); + /** + * Create a synchronous chat completion with custom headers support Waits for the + * GLM-4 model to complete execution and returns the final result with custom HTTP + * headers Supports complex reasoning, tool calling, function execution, and + * multi-modal understanding Features advanced capabilities like web search + * integration, code interpretation, and image analysis + * @param request Chat completion parameters including model selection, conversation + * messages, generation settings, tools configuration, and response format + * @param headers Custom HTTP headers to be added to the request + * @return Complete chat completion response with generated content, usage statistics, + * tool call results, and reasoning traces + */ + @POST("chat/completions") + Single createChatCompletion(@Body ChatCompletionCreateParams request, + @HeaderMap Map headers); + /** * Query the result of an asynchronous chat completion task Retrieves the completion * result or current status using the task ID from async request Provides detailed diff --git a/core/src/main/java/ai/z/openapi/core/Constants.java b/core/src/main/java/ai/z/openapi/core/Constants.java index 34aeef6..886a103 100644 --- a/core/src/main/java/ai/z/openapi/core/Constants.java +++ b/core/src/main/java/ai/z/openapi/core/Constants.java @@ -260,6 +260,11 @@ public final class Constants { */ public static final String ModelCogVideoX2 = "cogvideox-2"; + /** + * CogVideoX-3 model + */ + public static final String ModelCogVideoX3 = "cogvideox-3"; + /** * Vidu Q1 Text model - High-performance video generation from text input. Supports * general and anime styles. diff --git a/core/src/main/java/ai/z/openapi/core/config/ZaiConfig.java b/core/src/main/java/ai/z/openapi/core/config/ZaiConfig.java index ad6b8f0..b2f7d08 100644 --- a/core/src/main/java/ai/z/openapi/core/config/ZaiConfig.java +++ b/core/src/main/java/ai/z/openapi/core/config/ZaiConfig.java @@ -5,6 +5,7 @@ import lombok.Builder; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.Map; import java.util.concurrent.TimeUnit; import static ai.z.openapi.core.Constants.Z_AI_BASE_URL; @@ -63,6 +64,11 @@ public class ZaiConfig { */ private String apiSecret; + /** + * Custom Http Request Headers + */ + private Map customHeaders; + /** * JWT token expiration time in milliseconds (default: 30 minutes). */ @@ -408,4 +414,12 @@ public class ZaiConfig { return source_channel; } + /** + * Get custom headers + * @return + */ + public Map getCustomHeaders() { + return customHeaders; + } + } diff --git a/core/src/main/java/ai/z/openapi/core/token/AuthenticationInterceptor.java b/core/src/main/java/ai/z/openapi/core/token/HttpRequestInterceptor.java similarity index 65% rename from core/src/main/java/ai/z/openapi/core/token/AuthenticationInterceptor.java rename to core/src/main/java/ai/z/openapi/core/token/HttpRequestInterceptor.java index 322da34..ba3f550 100644 --- a/core/src/main/java/ai/z/openapi/core/token/AuthenticationInterceptor.java +++ b/core/src/main/java/ai/z/openapi/core/token/HttpRequestInterceptor.java @@ -7,16 +7,17 @@ import okhttp3.Request; import okhttp3.Response; import java.io.IOException; +import java.util.Map; import java.util.Objects; /** * OkHttp Interceptor that adds an authorization token header */ -public class AuthenticationInterceptor implements Interceptor { +public class HttpRequestInterceptor implements Interceptor { private final ZaiConfig config; - public AuthenticationInterceptor(ZaiConfig config) { + public HttpRequestInterceptor(ZaiConfig config) { Objects.requireNonNull(config.getApiKey(), "Z.ai token required"); this.config = config; } @@ -31,17 +32,22 @@ public class AuthenticationInterceptor implements Interceptor { TokenManager tokenManager = GlobalTokenManager.getTokenManagerV4(); accessToken = tokenManager.getToken(this.config); } - String source_channel = "java-sdk"; + String source_channel = "z-ai-sdk-java"; if (StringUtils.isNotEmpty(config.getSource_channel())) { source_channel = config.getSource_channel(); } - Request request = chain.request() + Request.Builder request = chain.request() .newBuilder() .header("Authorization", "Bearer " + accessToken) .header("x-source-channel", source_channel) - .header("Accept-Language", "en-US,en") - .build(); - return chain.proceed(request); + .header("Zai-SDK-Ver", "0.0.2") + .header("Accept-Language", "en-US,en"); + if (Objects.nonNull(config.getCustomHeaders())) { + for (Map.Entry entry : config.getCustomHeaders().entrySet()) { + request.addHeader(entry.getKey(), entry.getValue()); + } + } + return chain.proceed(request.build()); } } diff --git a/core/src/main/java/ai/z/openapi/service/chat/ChatService.java b/core/src/main/java/ai/z/openapi/service/chat/ChatService.java index f382290..cf1ed62 100644 --- a/core/src/main/java/ai/z/openapi/service/chat/ChatService.java +++ b/core/src/main/java/ai/z/openapi/service/chat/ChatService.java @@ -5,6 +5,8 @@ import ai.z.openapi.service.model.ChatCompletionResponse; import ai.z.openapi.service.model.AsyncResultRetrieveParams; import ai.z.openapi.service.model.QueryModelResultResponse; +import java.util.Map; + /** * Chat completion service interface */ @@ -32,4 +34,13 @@ public interface ChatService { */ QueryModelResultResponse retrieveAsyncResult(AsyncResultRetrieveParams request); + /** + * Creates a chat completion with custom headers support. This method allows passing + * custom HTTP headers along with the chat completion request. + * @param request the chat completion request parameters + * @param customHeaders custom HTTP headers to be added to the request + * @return ChatCompletionResponse containing the completion result + */ + ChatCompletionResponse createChatCompletion(ChatCompletionCreateParams request, Map customHeaders); + } \ No newline at end of file diff --git a/core/src/main/java/ai/z/openapi/service/chat/ChatServiceImpl.java b/core/src/main/java/ai/z/openapi/service/chat/ChatServiceImpl.java index 273c747..6c29a2d 100644 --- a/core/src/main/java/ai/z/openapi/service/chat/ChatServiceImpl.java +++ b/core/src/main/java/ai/z/openapi/service/chat/ChatServiceImpl.java @@ -7,11 +7,12 @@ import ai.z.openapi.service.model.ChatCompletionResponse; import ai.z.openapi.service.model.AsyncResultRetrieveParams; import ai.z.openapi.service.model.QueryModelResultResponse; import ai.z.openapi.service.model.ModelData; +import ai.z.openapi.service.model.ChatRequestWithHeaders; import ai.z.openapi.utils.FlowableRequestSupplier; import ai.z.openapi.utils.RequestSupplier; -import ai.z.openapi.utils.StringUtils; import okhttp3.ResponseBody; +import java.util.Map; import java.util.Objects; /** @@ -64,6 +65,38 @@ public class ChatServiceImpl implements ChatService { return this.zAiClient.executeRequest(request, supplier, ChatCompletionResponse.class); } + @Override + public ChatCompletionResponse createChatCompletion(ChatCompletionCreateParams request, + Map customHeaders) { + if (Objects.isNull(customHeaders)) { + throw new IllegalArgumentException("customHeaders can not be null"); + } + validateParams(request); + if (Objects.nonNull(request.getStream()) && request.getStream()) { + return streamChatCompletionWithHeaders(request, customHeaders); + } + else { + return syncChatCompletionWithHeaders(request, customHeaders); + } + } + + private ChatCompletionResponse streamChatCompletionWithHeaders(ChatCompletionCreateParams request, + Map customHeaders) { + ChatRequestWithHeaders requestWithHeaders = new ChatRequestWithHeaders(request, customHeaders); + FlowableRequestSupplier> supplier = (wrapper) -> chatApi + .createChatCompletionStream(wrapper.getRequest(), wrapper.getCustomHeaders()); + return this.zAiClient.streamRequest(requestWithHeaders, supplier, ChatCompletionResponse.class, + ModelData.class); + } + + private ChatCompletionResponse syncChatCompletionWithHeaders(ChatCompletionCreateParams request, + Map customHeaders) { + ChatRequestWithHeaders requestWithHeaders = new ChatRequestWithHeaders(request, customHeaders); + RequestSupplier supplier = (wrapper) -> chatApi + .createChatCompletion(wrapper.getRequest(), wrapper.getCustomHeaders()); + return this.zAiClient.executeRequest(requestWithHeaders, supplier, ChatCompletionResponse.class); + } + private void validateParams(ChatCompletionCreateParams request) { if (request == null) { throw new IllegalArgumentException("request cannot be null"); diff --git a/core/src/main/java/ai/z/openapi/service/file/UploadFilePurpose.java b/core/src/main/java/ai/z/openapi/service/file/UploadFilePurpose.java new file mode 100644 index 0000000..c674394 --- /dev/null +++ b/core/src/main/java/ai/z/openapi/service/file/UploadFilePurpose.java @@ -0,0 +1,17 @@ +package ai.z.openapi.service.file; + +public enum UploadFilePurpose { + + BATCH("batch"), FILE_EXTRACT("file-extract"), CODE_INTERPRETER("code-interpreter"), AGENT("agent"); + + private final String value; + + UploadFilePurpose(final String value) { + this.value = value; + } + + public String value() { + return value; + } + +} diff --git a/core/src/main/java/ai/z/openapi/service/model/ChatRequestWithHeaders.java b/core/src/main/java/ai/z/openapi/service/model/ChatRequestWithHeaders.java new file mode 100644 index 0000000..ccb3045 --- /dev/null +++ b/core/src/main/java/ai/z/openapi/service/model/ChatRequestWithHeaders.java @@ -0,0 +1,32 @@ +package ai.z.openapi.service.model; + +import ai.z.openapi.core.model.ClientRequest; +import ai.z.openapi.service.CommonRequest; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * Chat request wrapper that includes custom headers support. This class wraps the + * original ChatCompletionCreateParams and adds custom headers functionality. + */ +@EqualsAndHashCode(callSuper = true) +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatRequestWithHeaders extends CommonRequest implements ClientRequest { + + /** + * The original chat completion request parameters + */ + private ChatCompletionCreateParams request; + + /** + * Custom headers to be added to the HTTP request + */ + private Map customHeaders; + +} \ No newline at end of file diff --git a/core/src/main/java/ai/z/openapi/utils/OkHttps.java b/core/src/main/java/ai/z/openapi/utils/OkHttps.java index 4d48348..37b253a 100644 --- a/core/src/main/java/ai/z/openapi/utils/OkHttps.java +++ b/core/src/main/java/ai/z/openapi/utils/OkHttps.java @@ -1,7 +1,7 @@ package ai.z.openapi.utils; import ai.z.openapi.core.config.ZaiConfig; -import ai.z.openapi.core.token.AuthenticationInterceptor; +import ai.z.openapi.core.token.HttpRequestInterceptor; import okhttp3.ConnectionPool; import okhttp3.OkHttpClient; @@ -42,7 +42,7 @@ public final class OkHttps { throw new IllegalArgumentException("Configuration cannot be null"); } - OkHttpClient.Builder builder = new OkHttpClient.Builder().addInterceptor(new AuthenticationInterceptor(config)); + OkHttpClient.Builder builder = new OkHttpClient.Builder().addInterceptor(new HttpRequestInterceptor(config)); // Configure timeouts configureTimeouts(builder, config); diff --git a/core/src/test/java/ai/z/openapi/service/assistant/AssistantServiceTest.java b/core/src/test/java/ai/z/openapi/service/assistant/AssistantServiceTest.java index e6eea8b..eaf8f71 100644 --- a/core/src/test/java/ai/z/openapi/service/assistant/AssistantServiceTest.java +++ b/core/src/test/java/ai/z/openapi/service/assistant/AssistantServiceTest.java @@ -61,44 +61,6 @@ public class AssistantServiceTest { "AssistantService should be an instance of AssistantServiceImpl"); } - // As of 2025-07-23, no models support synchronous completion - // @Test - // @DisplayName("Test Synchronous Assistant Completion - Basic Functionality") - // @EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$") - // void testSyncAssistantCompletion() throws JsonProcessingException { - // // Prepare test data - // MessageTextContent textContent = MessageTextContent.builder() - // .text("Hello, please introduce yourself") - // .type("text") - // .build(); - - // ConversationMessage message = ConversationMessage.builder() - // .role("user") - // .content(Collections.singletonList(textContent)) - // .build(); - - // String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis()); - - // AssistantParameters request = AssistantParameters.builder() - // .model(Constants.ModelChatGLM4Assistant) - // .assistantId(TEST_ASSISTANT_ID) - // .stream(false) - // .messages(Collections.singletonList(message)) - // .requestId(requestId) - // .build(); - - // // Execute test - // AssistantApiResponse response = assistantService.assistantCompletion(request); - - // // Verify results - // assertNotNull(response, "Response should not be null"); - // assertTrue(response.isSuccess(), "Response should be successful"); - // assertNotNull(response.getData(), "Response data should not be null"); - // assertNull(response.getError(), "Response error should be null"); - // logger.info("Synchronous assistant completion response: {}", - // mapper.writeValueAsString(response)); - // } - @Test @DisplayName("Test Stream Assistant Completion") @EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$") @@ -188,13 +150,17 @@ public class AssistantServiceTest { @EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$") void testQueryConversationUsage() { // Prepare test data - ConversationParameters request = ConversationParameters.builder().assistantId(TEST_ASSISTANT_ID).build(); + ConversationParameters request = ConversationParameters.builder() + .assistantId(TEST_ASSISTANT_ID) + .page(1) + .pageSize(5) + .build(); // Execute test ConversationUsageListResponse response = assistantService.queryConversationUsage(request); // Verify results - assertNotNull(response, "Response should not be null"); + assertNotNull(response.getData(), "Response should not be null"); logger.info("Query conversation usage response: {}", response); } @@ -216,7 +182,7 @@ public class AssistantServiceTest { .messages(Collections.singletonList(message)) .build(); - AssistantApiResponse response = assistantService.assistantCompletion(request); + AssistantApiResponse response = assistantService.assistantCompletionStream(request); // Should handle error gracefully assertNotNull(response, "Response should not be null even for invalid assistant ID"); @@ -258,45 +224,29 @@ public class AssistantServiceTest { AssistantParameters request = AssistantParameters.builder() .assistantId(TEST_ASSISTANT_ID) - .stream(false) + .stream(true) .messages(messages) .requestId(requestId) .build(); - AssistantApiResponse response = assistantService.assistantCompletion(request); - + AssistantApiResponse response = assistantService.assistantCompletionStream(request); + response.getFlowable().doOnNext(accumulator -> { + if (accumulator.getChoices() != null && !accumulator.getChoices().isEmpty()) { + MessageContent delta = accumulator.getChoices().get(0).getDelta(); + if (delta != null) { + try { + logger.info("MessageContent: {}", mapper.writeValueAsString(delta)); + } + catch (JsonProcessingException e) { + logger.error("Error processing message content", e); + } + } + } + }) + .doOnComplete(() -> logger.info("Stream response completed, received messages")) + .doOnError(throwable -> logger.error("Stream error: {}", throwable.getMessage())) + .blockingSubscribe(); assertNotNull(response, "Multi-turn conversation response should not be null"); - logger.info("Multi-turn conversation response: {}", mapper.writeValueAsString(response)); - } - - @Test - @DisplayName("Test Assistant with Translation Parameters") - @EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$") - void testAssistantWithTranslation() throws JsonProcessingException { - // This test is based on the existing - // TestAssistantClientApiService.testTranslateAssistantCompletion - MessageTextContent textContent = MessageTextContent.builder().text("Hello there").type("text").build(); - - ConversationMessage message = ConversationMessage.builder() - .role("user") - .content(Collections.singletonList(textContent)) - .build(); - - String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis()); - - AssistantParameters request = AssistantParameters.builder() - .assistantId("9996ijk789lmn012o345p999") // Translation assistant ID - .stream(false) - .messages(Collections.singletonList(message)) - .requestId(requestId) - .build(); - - // Execute test - AssistantApiResponse response = assistantService.assistantCompletion(request); - - // Verify results - assertNotNull(response, "Translation response should not be null"); - logger.info("Translation assistant response: {}", mapper.writeValueAsString(response)); } } diff --git a/core/src/test/java/ai/z/openapi/service/audio/AudioServiceTest.java b/core/src/test/java/ai/z/openapi/service/audio/AudioServiceTest.java index f527c67..d70d574 100644 --- a/core/src/test/java/ai/z/openapi/service/audio/AudioServiceTest.java +++ b/core/src/test/java/ai/z/openapi/service/audio/AudioServiceTest.java @@ -303,7 +303,7 @@ public class AudioServiceTest { @DisplayName("Should transcribe different audio formats successfully") @EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$") void shouldTranscribeDifferentAudioFormatsSuccessfully() throws JsonProcessingException { - String[] audioFiles = { "asr.wav", "asr.webm" }; + String[] audioFiles = { "asr.wav", "asr.mp3" }; for (String audioFile : audioFiles) { String requestId = String.format(REQUEST_ID_TEMPLATE + "-%s", System.currentTimeMillis(), audioFile); diff --git a/core/src/test/java/ai/z/openapi/service/chat/ChatServiceTest.java b/core/src/test/java/ai/z/openapi/service/chat/ChatServiceTest.java index 088a20d..3cb32a6 100644 --- a/core/src/test/java/ai/z/openapi/service/chat/ChatServiceTest.java +++ b/core/src/test/java/ai/z/openapi/service/chat/ChatServiceTest.java @@ -508,4 +508,187 @@ public class ChatServiceTest { logger.info("CodeGeex code completion test completed"); } + @Test + @DisplayName("Test Synchronous Chat Completion with Custom Headers") + @EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$") + void testSyncChatCompletionWithCustomHeaders() throws JsonProcessingException { + // Prepare test data + List messages = new ArrayList<>(); + ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "Hello, please introduce yourself"); + messages.add(userMessage); + + String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis()); + + ChatCompletionCreateParams request = ChatCompletionCreateParams.builder() + .model(Constants.ModelChatGLM4) + .stream(Boolean.FALSE) + .messages(messages) + .requestId(requestId) + .build(); + + // Prepare custom headers + Map customHeaders = new HashMap<>(); + customHeaders.put("X-Custom-User-ID", "test-user-123"); + customHeaders.put("X-Request-Source", "junit-test"); + customHeaders.put("Session-Id", "session-" + System.currentTimeMillis()); + + // Execute test + ChatCompletionResponse response = chatService.createChatCompletion(request, customHeaders); + + // Verify results + assertNotNull(response, "Response should not be null"); + assertTrue(response.isSuccess(), "Response should be successful"); + assertNotNull(response.getData(), "Response data should not be null"); + assertEquals(requestId, response.getData().getRequestId(), "Request ID should match"); + assertNotNull(response.getData().getChoices(), "Response data should not be null"); + assertFalse(response.getData().getChoices().isEmpty(), "Response data should not be empty"); + assertNull(response.getError(), "Response error should be null"); + logger.info("Synchronous chat completion with custom headers response: {}", + mapper.writeValueAsString(response)); + } + + @Test + @DisplayName("Test Stream Chat Completion with Custom Headers") + @EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$") + void testStreamChatCompletionWithCustomHeaders() throws JsonProcessingException { + // Prepare test data + List messages = new ArrayList<>(); + ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), + "Please write a short poem about spring"); + messages.add(userMessage); + + String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis()); + + ChatCompletionCreateParams request = ChatCompletionCreateParams.builder() + .model(Constants.ModelChatGLM4) + .stream(Boolean.TRUE) + .messages(messages) + .requestId(requestId) + .temperature(0.7F) + .maxTokens(100) + .build(); + + // Prepare custom headers + Map customHeaders = new HashMap<>(); + customHeaders.put("X-Custom-User-ID", "stream-test-user-456"); + customHeaders.put("X-Request-Source", "junit-stream-test"); + customHeaders.put("X-Stream-Mode", "enabled"); + customHeaders.put("Content-Type", "application/json"); + + // Execute test + ChatCompletionResponse response = chatService.createChatCompletion(request, customHeaders); + + // Verify results + assertNotNull(response, "Response should not be null"); + assertTrue(response.isSuccess(), "Response should be successful"); + + // Test stream data processing + AtomicInteger messageCount = new AtomicInteger(0); + AtomicBoolean isFirst = new AtomicBoolean(true); + + response.getFlowable().doOnNext(modelData -> { + if (isFirst.getAndSet(false)) { + logger.info("Starting to receive stream response with custom headers:"); + } + if (modelData.getChoices() != null && !modelData.getChoices().isEmpty()) { + Choice choice = modelData.getChoices().get(0); + if (choice.getDelta() != null && choice.getDelta().getContent() != null) { + logger.info("Received content: {}", choice.getDelta().getContent()); + messageCount.incrementAndGet(); + } + } + }) + .doOnComplete(() -> logger.info( + "Stream response with custom headers completed, received {} messages in total", messageCount.get())) + .blockingSubscribe(); + + assertTrue(messageCount.get() > 0, "Should receive at least one message"); + + logger.info("Stream chat completion with custom headers test completed"); + } + + @Test + @DisplayName("Test Custom Headers Validation - Null Headers") + void testCustomHeadersValidation_NullHeaders() { + // Prepare test data + List messages = new ArrayList<>(); + ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "Hello"); + messages.add(userMessage); + + ChatCompletionCreateParams request = ChatCompletionCreateParams.builder() + .model(Constants.ModelChatGLM4) + .stream(Boolean.FALSE) + .messages(messages) + .build(); + + // Test with null custom headers + assertThrows(IllegalArgumentException.class, () -> { + chatService.createChatCompletion(request, null); + }, "Null custom headers should throw IllegalArgumentException"); + } + + @Test + @DisplayName("Test Custom Headers with Empty Map") + @EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$") + void testCustomHeadersWithEmptyMap() throws JsonProcessingException { + // Prepare test data + List messages = new ArrayList<>(); + ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "Hello"); + messages.add(userMessage); + + String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis()); + + ChatCompletionCreateParams request = ChatCompletionCreateParams.builder() + .model(Constants.ModelChatGLM4) + .stream(Boolean.FALSE) + .messages(messages) + .requestId(requestId) + .build(); + + // Test with empty custom headers map + Map emptyHeaders = new HashMap<>(); + ChatCompletionResponse response = chatService.createChatCompletion(request, emptyHeaders); + + // Verify results + assertNotNull(response, "Response should not be null"); + assertTrue(response.isSuccess(), "Response should be successful"); + assertNotNull(response.getData(), "Response data should not be null"); + logger.info("Chat completion with empty custom headers response: {}", mapper.writeValueAsString(response)); + } + + @Test + @DisplayName("Test Custom Headers with Special Characters") + @EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$") + void testCustomHeadersWithSpecialCharacters() throws JsonProcessingException { + // Prepare test data + List messages = new ArrayList<>(); + ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "Hello"); + messages.add(userMessage); + + String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis()); + + ChatCompletionCreateParams request = ChatCompletionCreateParams.builder() + .model(Constants.ModelChatGLM4) + .stream(Boolean.FALSE) + .messages(messages) + .requestId(requestId) + .build(); + + // Prepare custom headers with special characters + Map customHeaders = new HashMap<>(); + customHeaders.put("X-User-Agent", "ZAI-SDK/1.0 (Java; Test)"); + customHeaders.put("X-Client-Version", "v1.2.3-beta"); + customHeaders.put("X-Request-Timestamp", String.valueOf(System.currentTimeMillis())); + customHeaders.put("X-Trace-ID", "trace-" + UUID.randomUUID()); + + // Execute test + ChatCompletionResponse response = chatService.createChatCompletion(request, customHeaders); + + // Verify results + assertNotNull(response, "Response should not be null"); + assertTrue(response.isSuccess(), "Response should be successful"); + assertNotNull(response.getData(), "Response data should not be null"); + logger.info("Chat completion with special character headers response: {}", mapper.writeValueAsString(response)); + } + } diff --git a/core/src/test/java/ai/z/openapi/service/chat/ChatServiceWithHeadersIntegrationTest.java b/core/src/test/java/ai/z/openapi/service/chat/ChatServiceWithHeadersIntegrationTest.java new file mode 100644 index 0000000..ffc8d91 --- /dev/null +++ b/core/src/test/java/ai/z/openapi/service/chat/ChatServiceWithHeadersIntegrationTest.java @@ -0,0 +1,120 @@ +package ai.z.openapi.service.chat; + +import ai.z.openapi.service.model.ChatCompletionCreateParams; +import ai.z.openapi.service.model.ChatMessage; +import ai.z.openapi.service.model.ChatMessageRole; +import ai.z.openapi.service.model.ChatRequestWithHeaders; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit test for ChatRequestWithHeaders wrapper functionality. Tests the wrapper class + * used for custom headers in chat requests. + */ +class ChatServiceWithHeadersIntegrationTest { + + @Test + void testChatRequestWithHeaders_StreamingRequest() { + // Arrange + ChatCompletionCreateParams request = ChatCompletionCreateParams.builder() + .model("glm-4-flash") + .messages(Arrays + .asList(ChatMessage.builder().role(ChatMessageRole.USER.value()).content("Hello, world!").build())) + .stream(true) + .build(); + + Map customHeaders = new HashMap<>(); + customHeaders.put("X-Custom-User-ID", "user123"); + customHeaders.put("X-Request-Source", "test"); + + // Act + ChatRequestWithHeaders wrapper = new ChatRequestWithHeaders(request, customHeaders); + + // Assert + assertNotNull(wrapper); + assertNotNull(wrapper.getRequest()); + assertNotNull(wrapper.getCustomHeaders()); + assertEquals(request, wrapper.getRequest()); + assertEquals(customHeaders, wrapper.getCustomHeaders()); + assertEquals(2, wrapper.getCustomHeaders().size()); + assertEquals("user123", wrapper.getCustomHeaders().get("X-Custom-User-ID")); + assertEquals("test", wrapper.getCustomHeaders().get("X-Request-Source")); + } + + @Test + void testChatRequestWithHeaders_NonStreamingRequest() { + // Arrange + ChatCompletionCreateParams request = ChatCompletionCreateParams.builder() + .model("glm-4-flash") + .messages(Arrays + .asList(ChatMessage.builder().role(ChatMessageRole.USER.value()).content("What is AI?").build())) + .stream(false) // Non-streaming request + .build(); + + Map customHeaders = new HashMap<>(); + customHeaders.put("X-Custom-User-ID", "user456"); + customHeaders.put("X-Request-Source", "test-sync"); + + // Act + ChatRequestWithHeaders wrapper = new ChatRequestWithHeaders(request, customHeaders); + + // Assert + assertNotNull(wrapper); + assertNotNull(wrapper.getRequest()); + assertNotNull(wrapper.getCustomHeaders()); + assertEquals(request, wrapper.getRequest()); + assertEquals(customHeaders, wrapper.getCustomHeaders()); + assertFalse(wrapper.getRequest().getStream()); // Verify non-streaming + assertEquals("user456", wrapper.getCustomHeaders().get("X-Custom-User-ID")); + assertEquals("test-sync", wrapper.getCustomHeaders().get("X-Request-Source")); + } + + @Test + void testChatRequestWithHeaders_NullHeaders() { + // Arrange + ChatCompletionCreateParams request = ChatCompletionCreateParams.builder() + .model("glm-4-flash") + .messages(Arrays + .asList(ChatMessage.builder().role(ChatMessageRole.USER.value()).content("Test message").build())) + .stream(false) + .build(); + + // Act + ChatRequestWithHeaders wrapper = new ChatRequestWithHeaders(request, null); + + // Assert + assertNotNull(wrapper); + assertNotNull(wrapper.getRequest()); + assertEquals(request, wrapper.getRequest()); + // Custom headers are converted to empty map when null + assertNull(wrapper.getCustomHeaders()); + } + + @Test + void testChatRequestWithHeaders_ToString() { + // Arrange + ChatCompletionCreateParams request = ChatCompletionCreateParams.builder() + .model("glm-4-flash") + .messages(Arrays + .asList(ChatMessage.builder().role(ChatMessageRole.USER.value()).content("Test message").build())) + .stream(false) + .build(); + + Map headers = new HashMap<>(); + headers.put("X-Test", "value"); + + // Act + ChatRequestWithHeaders wrapper = new ChatRequestWithHeaders(request, headers); + String toStringResult = wrapper.toString(); + + // Assert + assertNotNull(toStringResult); + assertTrue(toStringResult.contains("ChatRequestWithHeaders")); + } + +} \ No newline at end of file diff --git a/core/src/test/java/ai/z/openapi/service/file/FileServiceTest.java b/core/src/test/java/ai/z/openapi/service/file/FileServiceTest.java index 53e94cb..a18dd46 100644 --- a/core/src/test/java/ai/z/openapi/service/file/FileServiceTest.java +++ b/core/src/test/java/ai/z/openapi/service/file/FileServiceTest.java @@ -37,13 +37,6 @@ public class FileServiceTest { // Request ID template private static final String REQUEST_ID_TEMPLATE = "file-test-%d"; - // Test file purposes - private static final String PURPOSE_FINE_TUNE = "fine-tune"; - - private static final String PURPOSE_ASSISTANTS = "assistants"; - - private static final String PURPOSE_BATCH = "batch"; - @BeforeEach void setUp() { ZaiConfig zaiConfig = new ZaiConfig(); @@ -74,7 +67,7 @@ public class FileServiceTest { FileUploadParams request = FileUploadParams.builder() .filePath(tempFile.toString()) - .purpose(PURPOSE_ASSISTANTS) + .purpose(UploadFilePurpose.AGENT.value()) .requestId(requestId) .build(); @@ -87,7 +80,7 @@ public class FileServiceTest { assertNotNull(response.getData(), "Response data should not be null"); assertNotNull(response.getData().getId(), "File ID should not be null"); assertEquals("file", response.getData().getObject(), "Object type should be 'file'"); - assertEquals(PURPOSE_ASSISTANTS, response.getData().getPurpose(), "Purpose should match"); + assertEquals(UploadFilePurpose.AGENT.value(), response.getData().getPurpose(), "Purpose should match"); assertNotNull(response.getData().getFilename(), "Filename should not be null"); assertNotNull(response.getData().getBytes(), "File size should not be null"); assertTrue(response.getData().getBytes() > 0, "File size should be greater than 0"); @@ -119,7 +112,7 @@ public class FileServiceTest { FileUploadParams request = FileUploadParams.builder() .filePath(tempFile.toString()) - .purpose(PURPOSE_FINE_TUNE) + .purpose(UploadFilePurpose.FILE_EXTRACT.value()) .requestId(requestId) .extraJson(extraJson) .build(); @@ -131,7 +124,8 @@ public class FileServiceTest { assertNotNull(response, "Response should not be null"); assertTrue(response.isSuccess(), "Response should be successful"); assertNotNull(response.getData(), "Response data should not be null"); - assertEquals(PURPOSE_FINE_TUNE, response.getData().getPurpose(), "Purpose should match"); + assertEquals(UploadFilePurpose.FILE_EXTRACT.value(), response.getData().getPurpose(), + "Purpose should match"); assertNull(response.getError(), "Response error should be null"); logger.info("File upload with extra JSON response: {}", mapper.writeValueAsString(response)); @@ -149,7 +143,7 @@ public class FileServiceTest { FileUploadParams request = FileUploadParams.builder() .filePath("/non/existent/file.txt") - .purpose(PURPOSE_ASSISTANTS) + .purpose(UploadFilePurpose.AGENT.value()) .requestId(requestId) .build(); @@ -187,7 +181,7 @@ public class FileServiceTest { String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis()); FileListParams request = FileListParams.builder() - .purpose(PURPOSE_ASSISTANTS) + .purpose(UploadFilePurpose.AGENT.value()) .limit(5) .order("asc") .requestId(requestId) @@ -205,7 +199,8 @@ public class FileServiceTest { // Verify purpose filter if files exist if (response.getData().getData() != null && !response.getData().getData().isEmpty()) { response.getData().getData().forEach(file -> { - assertEquals(PURPOSE_ASSISTANTS, file.getPurpose(), "All files should have the specified purpose"); + assertEquals(UploadFilePurpose.AGENT.value(), file.getPurpose(), + "All files should have the specified purpose"); }); } @@ -269,7 +264,7 @@ public class FileServiceTest { FileUploadParams uploadRequest = FileUploadParams.builder() .filePath(tempFile.toString()) - .purpose(PURPOSE_ASSISTANTS) + .purpose(UploadFilePurpose.FILE_EXTRACT.value()) .requestId(requestId) .build(); diff --git a/core/src/test/java/ai/z/openapi/service/image/ImageServiceTest.java b/core/src/test/java/ai/z/openapi/service/image/ImageServiceTest.java index f905018..d19a549 100644 --- a/core/src/test/java/ai/z/openapi/service/image/ImageServiceTest.java +++ b/core/src/test/java/ai/z/openapi/service/image/ImageServiceTest.java @@ -110,13 +110,13 @@ public class ImageServiceTest { @DisplayName("Test Different Image Sizes") @EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$") void testDifferentImageSizes() throws JsonProcessingException { - String[] sizes = { "256x256", "512x512", "1024x1024" }; + String[] sizes = { "512x512", "1024x1024" }; for (String size : sizes) { String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis()); CreateImageRequest request = CreateImageRequest.builder() - .model(Constants.ModelCogView3Plus) + .model(Constants.ModelCogView4) .prompt("A simple geometric pattern") .size(size) .requestId(requestId) @@ -124,7 +124,8 @@ public class ImageServiceTest { ImageResponse response = imageService.createImage(request); - assertNotNull(response, "Response should not be null for size: " + size); + assertNotNull(response.getData(), "Response should not be null for size: " + size); + assertEquals(200, response.getCode()); logger.info("Size {} response: {}", size, mapper.writeValueAsString(response)); } } diff --git a/core/src/test/java/ai/z/openapi/service/videos/VideosServiceTest.java b/core/src/test/java/ai/z/openapi/service/videos/VideosServiceTest.java index 24ed764..c7f843f 100644 --- a/core/src/test/java/ai/z/openapi/service/videos/VideosServiceTest.java +++ b/core/src/test/java/ai/z/openapi/service/videos/VideosServiceTest.java @@ -1,6 +1,7 @@ package ai.z.openapi.service.videos; import ai.z.openapi.ZaiClient; +import ai.z.openapi.core.Constants; import ai.z.openapi.core.config.ZaiConfig; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -35,11 +36,6 @@ public class VideosServiceTest { // Request ID template private static final String REQUEST_ID_TEMPLATE = "video-test-%d"; - // Video model constants - private static final String MODEL_COGVIDEOX = "cogvideox"; - - private static final String MODEL_COGVIDEO3 = "cogvideo-3"; - @BeforeEach void setUp() { ZaiConfig zaiConfig = new ZaiConfig(); @@ -67,7 +63,7 @@ public class VideosServiceTest { String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis()); VideoCreateParams request = VideoCreateParams.builder() - .model(MODEL_COGVIDEOX) + .model(Constants.ModelCogVideoX3) .prompt("A beautiful sunset over the ocean with waves gently crashing on the shore") .requestId(requestId) .withAudio(Boolean.TRUE) @@ -80,6 +76,7 @@ public class VideosServiceTest { // Verify results assertNotNull(response, "Response should not be null"); + assertEquals(200, response.getCode()); assertTrue(response.isSuccess(), "Response should be successful"); assertNotNull(response.getData(), "Response data should not be null"); assertNotNull(response.getData().getId(), "Response data ID should not be null"); @@ -95,7 +92,7 @@ public class VideosServiceTest { String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis()); VideoCreateParams request = VideoCreateParams.builder() - .model(MODEL_COGVIDEOX) + .model(Constants.ModelCogVideoX3) .prompt("A person walking in a beautiful garden") .requestId(requestId) .build(); @@ -114,6 +111,7 @@ public class VideosServiceTest { // Verify result response assertNotNull(resultResponse, "Result response should not be null"); + assertEquals(200, resultResponse.getCode()); assertNotNull(resultResponse.getData(), "Result response data should not be null"); assertNotNull(resultResponse.getData().getId(), "Result response task ID should not be null"); logger.info("Video generation result: taskId={}, response={}", taskId, @@ -138,7 +136,7 @@ public class VideosServiceTest { } @ParameterizedTest - @ValueSource(strings = { MODEL_COGVIDEOX, MODEL_COGVIDEO3 }) + @ValueSource(strings = { Constants.ModelCogVideoX2, Constants.ModelCogVideoX3 }) @DisplayName("Test Different Video Models") @EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$") void testDifferentModels(String model) throws JsonProcessingException { @@ -153,6 +151,7 @@ public class VideosServiceTest { VideosResponse response = videosService.videoGenerations(request); assertNotNull(response, "Response should not be null"); + assertEquals(200, response.getCode()); logger.info("Model {} response: {}", model, mapper.writeValueAsString(response)); } @@ -177,7 +176,7 @@ public class VideosServiceTest { @Test @DisplayName("Test Parameter Validation - Empty Prompt") void testValidation_EmptyPrompt() { - VideoCreateParams request = VideoCreateParams.builder().model(MODEL_COGVIDEOX).prompt("").build(); + VideoCreateParams request = VideoCreateParams.builder().model(Constants.ModelCogVideoX3).prompt("").build(); assertThrows(IllegalArgumentException.class, () -> { videosService.videoGenerations(request); @@ -202,17 +201,18 @@ public class VideosServiceTest { Base64.Encoder encoder = Base64.getEncoder(); String imageUrl = encoder.encodeToString(bytes); VideoCreateParams request = VideoCreateParams.builder() - .model(MODEL_COGVIDEOX) + .model(Constants.ModelCogVideoX3) .prompt("Transform this image into a dynamic video scene") .imageUrl(imageUrl) .requestId(requestId) .withAudio(Boolean.FALSE) - .duration(3) + .duration(5) .build(); VideosResponse response = videosService.videoGenerations(request); assertNotNull(response, "Response should not be null"); + assertEquals(200, response.getCode()); logger.info("Video generation with image response: {}", mapper.writeValueAsString(response)); } @@ -223,19 +223,20 @@ public class VideosServiceTest { String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis()); VideoCreateParams request = VideoCreateParams.builder() - .model(MODEL_COGVIDEOX) + .model(Constants.ModelCogVideoX3) .prompt("A futuristic city with flying cars and neon lights") .requestId(requestId) - .quality("high") + .quality("speed") .withAudio(Boolean.TRUE) .size("1280x720") - .duration(10) + .duration(5) .fps(30) .build(); VideosResponse response = videosService.videoGenerations(request); assertNotNull(response, "Response should not be null"); + assertEquals(200, response.getCode()); logger.info("Video generation with custom settings response: {}", mapper.writeValueAsString(response)); } diff --git a/core/src/test/resources/asr.mp3 b/core/src/test/resources/asr.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ff9b04d44227fdf4f88e3dbd691cf086f5daea78 GIT binary patch literal 29996 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�tQjL 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 customHeaders = new HashMap<>(); + customHeaders.put("X-Custom-User-ID", "user123"); + customHeaders.put("X-Request-Source", "mobile-app"); + customHeaders.put("Session-Id", "session-abc-123"); + + try { + // Execute request with custom headers + // This works for both streaming and non-streaming requests + ChatCompletionResponse response = client.chat().createChatCompletion(request, customHeaders); + + // Example for non-streaming request with custom headers + ChatCompletionCreateParams nonStreamingRequest = ChatCompletionCreateParams.builder() + .model(Constants.ModelChatGLM4_5) + .messages(Arrays.asList( + ChatMessage.builder() + .role(ChatMessageRole.USER.value()) + .content("What is artificial intelligence?") + .build() + )) + .stream(false) // Explicitly set to false for non-streaming + .temperature(0.7f) + .maxTokens(1024) + .build(); + + ChatCompletionResponse nonStreamingResponse = client.chat() + .createChatCompletion(nonStreamingRequest, customHeaders); + + System.out.println("Non-streaming response: " + nonStreamingResponse.getData()); + + if (response.isSuccess() && response.getFlowable() != null) { + System.out.println("Streaming response with custom headers:"); + response.getFlowable().subscribe( + data -> { + // Handle streaming chunk + if (data.getChoices() != null && !data.getChoices().isEmpty()) { + String content = data.getChoices().get(0).getDelta().getContent(); + if (content != null) { + System.out.print(content); + } + } + }, + error -> System.err.println("\nStream error: " + error.getMessage()), + () -> System.out.println("\nStream completed") + ); + } else { + System.err.println("Error: " + response.getMsg()); + } + } catch (Exception e) { + System.err.println("Exception occurred: " + e.getMessage()); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/samples/src/main/ai.z.openapi.samples/ClientConfigurationExample.java b/samples/src/main/ai.z.openapi.samples/ClientConfigurationExample.java index 905c474..4abe954 100644 --- a/samples/src/main/ai.z.openapi.samples/ClientConfigurationExample.java +++ b/samples/src/main/ai.z.openapi.samples/ClientConfigurationExample.java @@ -1,6 +1,10 @@ package ai.z.openapi.samples; import ai.z.openapi.ZaiClient; +import ai.z.openapi.ZhipuAiClient; + +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.TimeUnit; /** @@ -12,14 +16,17 @@ public class ClientConfigurationExample { public static void main(String[] args) { System.out.println("=== Basic Configuration Example ==="); - ZaiClient basicClient = ZaiClient.builder().build(); + ZaiClient basicClient = ZaiClient.builder().apiKey("xxx.xxx").build(); System.out.println("✓ Basic client created successfully"); // Complete configuration example System.out.println("\n=== Complete Configuration Example ==="); + Map customHeaders = new HashMap<>(); + customHeaders.put("Session-Id", "custom-session-id-xx"); ZaiClient advancedClient = ZaiClient.builder() - .apiKey("your.api.key") + .apiKey("your.api_key") .baseUrl("https://api.z.ai/api/paas/v4/") + .customHeaders(customHeaders) .enableTokenCache() .tokenExpire(3600000) // 1 hour .connectionPool(10, 5, TimeUnit.MINUTES) @@ -28,14 +35,25 @@ public class ClientConfigurationExample { // ZHIPU platform specific client System.out.println("\n=== ZHIPU Platform Specific Configuration ==="); - ZaiClient zhipuClient = ZaiClient.ofZHIPU("your.api.key").build(); + ZaiClient zhipuClient = ZaiClient.ofZHIPU("your.api_key").build(); System.out.println("✓ ZHIPU platform client created successfully"); - + + ZhipuAiClient zhipuAiClient = ZhipuAiClient.builder() + .apiKey("your.api_key") + .baseUrl("https://api.z.ai/api/paas/v4/") + .customHeaders(customHeaders) + .enableTokenCache() + .tokenExpire(3600000) // 1 hour + .connectionPool(10, 5, TimeUnit.MINUTES) + .build(); + System.out.println("✓ ZHIPU platform client created successfully"); + // Custom configuration example System.out.println("\n=== Custom Configuration Example ==="); ZaiClient customClient = ZaiClient.builder() - .apiKey("your.api.key") + .apiKey("your.api_key") .baseUrl("https://custom.api.endpoint/") + .customHeaders(customHeaders) .enableTokenCache() .tokenExpire(7200000) .connectionPool(20, 10, TimeUnit.MINUTES) diff --git a/samples/src/main/ai.z.openapi.samples/CogVideoX3Example.java b/samples/src/main/ai.z.openapi.samples/CogVideoX3Example.java index 77ec566..d41a4be 100644 --- a/samples/src/main/ai.z.openapi.samples/CogVideoX3Example.java +++ b/samples/src/main/ai.z.openapi.samples/CogVideoX3Example.java @@ -16,13 +16,13 @@ public class CogVideoX3Example { public static void main(String[] args) { // Create client, recommended to set API Key via environment variable - // export ZAI_API_KEY=your.api.key + // export ZAI_API_KEY=your.api_key // for Z.ai use the `ZaiClient`, for Zhipu AI use the ZhipuAiClient ZaiClient client = ZaiClient.builder().build(); // Or set API Key via code // ZaiClient client = ZaiClient.builder() - // .apiKey("your.api.key") + // .apiKey("your.api_key") // .build(); // Video generation examples diff --git a/samples/src/main/ai.z.openapi.samples/CogVideoXExample.java b/samples/src/main/ai.z.openapi.samples/CogVideoXExample.java index 6a532a2..913aec5 100644 --- a/samples/src/main/ai.z.openapi.samples/CogVideoXExample.java +++ b/samples/src/main/ai.z.openapi.samples/CogVideoXExample.java @@ -13,13 +13,13 @@ public class CogVideoXExample { public static void main(String[] args) { // Create client, recommended to set API Key via environment variable - // export ZAI_API_KEY=your.api.key + // export ZAI_API_KEY=your.api_key // for Z.ai use the `ZaiClient`, for Zhipu AI use the ZhipuAiClient ZaiClient client = ZaiClient.builder().build(); // Or set API Key via code // ZaiClient client = ZaiClient.builder() - // .apiKey("your.api.key") + // .apiKey("your.api_key") // .build(); // Basic Video Generation diff --git a/samples/src/main/ai.z.openapi.samples/GLM41VThinkingExample.java b/samples/src/main/ai.z.openapi.samples/GLM41VThinkingExample.java index 7c8c635..70280cd 100644 --- a/samples/src/main/ai.z.openapi.samples/GLM41VThinkingExample.java +++ b/samples/src/main/ai.z.openapi.samples/GLM41VThinkingExample.java @@ -16,7 +16,7 @@ public class GLM41VThinkingExample { // for Z.ai use the `ZaiClient`, for Zhipu AI use the ZhipuAiClient ZaiClient client = ZaiClient.builder() - .apiKey("your.api.key") + .apiKey("your.api_key") .build(); ChatCompletionCreateParams request = ChatCompletionCreateParams.builder() diff --git a/samples/src/main/ai.z.openapi.samples/GLM4VPlusExample.java b/samples/src/main/ai.z.openapi.samples/GLM4VPlusExample.java index f40c3bc..b0ac710 100644 --- a/samples/src/main/ai.z.openapi.samples/GLM4VPlusExample.java +++ b/samples/src/main/ai.z.openapi.samples/GLM4VPlusExample.java @@ -15,7 +15,7 @@ public class GLM4VPlusExample { public static void main(String[] args) { // for Z.ai use the `ZaiClient`, for Zhipu AI use the ZhipuAiClient - ZaiClient client = ZaiClient.builder().apiKey("your.api.key").build(); + ZaiClient client = ZaiClient.builder().apiKey("your.api_key").build(); ChatCompletionCreateParams request = ChatCompletionCreateParams.builder() .model("glm-4v-plus-0111") diff --git a/samples/src/main/ai.z.openapi.samples/ViduAspectVideoExample.java b/samples/src/main/ai.z.openapi.samples/ViduAspectVideoExample.java index 52a9c12..5d09478 100644 --- a/samples/src/main/ai.z.openapi.samples/ViduAspectVideoExample.java +++ b/samples/src/main/ai.z.openapi.samples/ViduAspectVideoExample.java @@ -15,7 +15,7 @@ public class ViduAspectVideoExample { public static void main(String[] args) { // Create client, recommended to set API Key via environment variable - // export ZAI_API_KEY=your.api.key + // export ZAI_API_KEY=your.api_key // for Z.ai use the `ZaiClient`, for Zhipu AI use the ZhipuAiClient ZaiClient client = ZaiClient.builder().build(); diff --git a/samples/src/main/ai.z.openapi.samples/ViduImageToVideoExample.java b/samples/src/main/ai.z.openapi.samples/ViduImageToVideoExample.java index c08a23f..ad6ec8a 100644 --- a/samples/src/main/ai.z.openapi.samples/ViduImageToVideoExample.java +++ b/samples/src/main/ai.z.openapi.samples/ViduImageToVideoExample.java @@ -13,7 +13,7 @@ public class ViduImageToVideoExample { public static void main(String[] args) { // Create client, recommended to set API Key via environment variable - // export ZAI_API_KEY=your.api.key + // export ZAI_API_KEY=your.api_key // for Z.ai use the `ZaiClient`, for Zhipu AI use the ZhipuAiClient ZaiClient client = ZaiClient.builder().build(); diff --git a/samples/src/main/ai.z.openapi.samples/ViduStartEndVideoExample.java b/samples/src/main/ai.z.openapi.samples/ViduStartEndVideoExample.java index 3bdf339..006fc8e 100644 --- a/samples/src/main/ai.z.openapi.samples/ViduStartEndVideoExample.java +++ b/samples/src/main/ai.z.openapi.samples/ViduStartEndVideoExample.java @@ -15,7 +15,7 @@ public class ViduStartEndVideoExample { public static void main(String[] args) { // Create client, recommended to set API Key via environment variable - // export ZAI_API_KEY=your.api.key + // export ZAI_API_KEY=your.api_key // for Z.ai use the `ZaiClient`, for Zhipu AI use the ZhipuAiClient ZaiClient client = ZaiClient.builder().build(); diff --git a/samples/src/main/ai.z.openapi.samples/ViduTextToVideoExample.java b/samples/src/main/ai.z.openapi.samples/ViduTextToVideoExample.java index 0d9ca4c..2a219ff 100644 --- a/samples/src/main/ai.z.openapi.samples/ViduTextToVideoExample.java +++ b/samples/src/main/ai.z.openapi.samples/ViduTextToVideoExample.java @@ -13,13 +13,13 @@ public class ViduTextToVideoExample { public static void main(String[] args) { // Create client, recommended to set API Key via environment variable - // export ZAI_API_KEY=your.api.key + // export ZAI_API_KEY=your.api_key // for Z.ai use the `ZaiClient`, for Zhipu AI use the ZhipuAiClient ZaiClient client = ZaiClient.builder().build(); // Or set API Key via code // ZaiClient client = ZaiClient.builder() - // .apiKey("your.api.key") + // .apiKey("your.api_key") // .build(); // Example: Generate video from text using Vidu diff --git a/samples/src/main/ai.z.openapi.samples/WebSearchExample.java b/samples/src/main/ai.z.openapi.samples/WebSearchExample.java index ee8088c..8db3ede 100644 --- a/samples/src/main/ai.z.openapi.samples/WebSearchExample.java +++ b/samples/src/main/ai.z.openapi.samples/WebSearchExample.java @@ -23,13 +23,13 @@ public class WebSearchExample { public static void main(String[] args) { // Create client, recommended to set API Key via environment variable - // export ZAI_API_KEY=your.api.key + // export ZAI_API_KEY=your.api_key // for Z.ai use the `ZaiClient`, for Zhipu AI use the ZhipuAiClient ZaiClient client = ZaiClient.builder().build(); // Or set API Key via code // ZaiClient client = ZaiClient.builder() - // .apiKey("your.api.key") + // .apiKey("your.api_key") // .build(); // Example 1: Basic Web Search