Implementing a WebSocket Client for the iFlytek Spark Large Language Model in Java
Integrating with the iFlytek Spark LLM requires establishing a secure WebSocket connection, generating cryptographic request signatures, and managing streaming JSON responses. The following implementation demonstrates a complete Java client using OkHttp for transport and standard cryptographic utilities for authentication.
Cryptographic Authentication and Endpoint Generation
The API requires HMAC-SHA256 signing of the request line, timestamp, and host header. The resulting signature is embedded into query parameters before upgrading the HTTP request to a WebSocket connection.
public static String generateAuthenticatedUri(String baseUrl, String accessKey, String secretKey) throws Exception {
URL endpoint = new URL(baseUrl);
String timestamp = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US)
.format(Calendar.getInstance(TimeZone.getTimeZone("GMT")).getTime());
String signingString = String.format("host: %s\ndate: %s\nGET %s HTTP/1.1",
endpoint.getHost(), timestamp, endpoint.getPath());
Mac hmac = Mac.getInstance("hmacsha256");
hmac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "hmacsha256"));
byte[] rawSignature = hmac.doFinal(signingString.getBytes(StandardCharsets.UTF_8));
String encodedSignature = Base64.getEncoder().encodeToString(rawSignature);
String authHeader = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"",
accessKey, "hmac-sha256", "host date request-line", encodedSignature);
HttpUrl authenticatedUrl = Objects.requireNonNull(HttpUrl.parse("https://" + endpoint.getHost() + endpoint.getPath()))
.newBuilder()
.addQueryParameter("authorization", Base64.getEncoder().encodeToString(authHeader.getBytes(StandardCharsets.UTF_8)))
.addQueryParameter("date", timestamp)
.addQueryParameter("host", endpoint.getHost())
.build();
return authenticatedUrl.toString().replace("https://", "wss://");
}
Client Architecture and State Management
The handler class extends WebSocketListener to capture connection lifecycle events. State variables track conversation history, stream buffers, and thread synchronization flags. Data models map directly to the expected JSON schema.
public class SparkLlmHandler extends WebSocketListener {
private static final Gson PARSER = new Gson();
private static final List<ChatMessage> conversationLog = new ArrayList<>();
private static final StringBuilder replyAccumulator = new StringBuilder();
private static volatile boolean awaitingUserInput = true;
private static volatile String pendingQuery = "";
private volatile boolean connectionReadyToClose = false;
public static class ChatMessage {
public String role;
public String content;
}
public static class ApiEnvelope {
public ResponseHeader header;
public ResponsePayload payload;
}
public static class ResponseHeader { public int code; public int status; public String sid; }
public static class ResponsePayload { public ChoiceGroup choices; }
public static class ChoiceGroup { public List<TokenItem> text; }
public static class TokenItem { public String role; public String content; }
}
Interactive Execution Loop
The primary thread manages console input, triggers the WebSocket handshake, and coordinates synchronization between the main loop and the asynchronous listener.
public static void main(String[] args) throws Exception {
OkHttpClient httpClient = new OkHttpClient.Builder().build();
while (true) {
if (awaitingUserInput) {
System.out.print("User: ");
Scanner console = new Scanner(System.in);
pendingQuery = console.nextLine();
awaitingUserInput = false;
String authUri = generateAuthenticatedUri(
"https://spark-api.xf-yun.com/v3.5/chat",
"YOUR_API_KEY",
"YOUR_API_SECRET"
);
Request wsRequest = new Request.Builder().url(authUri).build();
replyAccumulator.setLength(0);
httpClient.newWebSocket(wsRequest, new SparkLlmHandler());
} else {
Thread.sleep(150);
}
}
}
WebSocket Lifecycle and Payload Construction
Upon successful cnonection, the onOpen callback spawns a worker thread to assemble and transmit the JSON payload. The payload structure combines application metadata, inference parameters, and the full conversation context.
@Override
public void onOpen(WebSocket ws, Response res) {
super.onOpen(ws, res);
System.out.print("Model: ");
new Thread(() -> {
try {
JSONObject requestEnvelope = new JSONObject();
JSONObject meta = new JSONObject();
meta.put("app_id", "YOUR_APP_ID");
meta.put("uid", UUID.randomUUID().toString().substring(0, 10));
JSONObject config = new JSONObject();
config.put("domain", "generalv3.5");
config.put("temperature", 0.6);
config.put("max_tokens", 4096);
requestEnvelope.put("parameter", new JSONObject().put("chat", config));
JSONArray messageArray = new JSONArray();
for (ChatMessage msg : conversationLog) {
messageArray.put(new JSONObject().put("role", msg.role).put("content", msg.content));
}
ChatMessage currentUserMsg = new ChatMessage();
currentUserMsg.role = "user";
currentUserMsg.content = pendingQuery;
messageArray.put(new JSONObject().put("role", currentUserMsg.role).put("content", currentUserMsg.content));
conversationLog.add(currentUserMsg);
requestEnvelope.put("header", meta);
requestEnvelope.put("payload", new JSONObject().put("message", new JSONObject().put("text", messageArray)));
ws.send(requestEnvelope.toString());
while (!connectionReadyToClose) {
Thread.sleep(200);
}
ws.close(1000, "Session complete");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
Stream Processing and Context Retention
The onMessage handler parses incoming JSON chunks, validates error codes, and prints streaming tokens. When the API signals completion (status 2), the accumulated response is stored in the history buffer, and control returns to the main input loop.
@Override
public void onMessage(WebSocket ws, String data) {
ApiEnvelope response = PARSER.fromJson(data, ApiEnvelope.class);
if (response.header.code != 0) {
System.err.println("API Error: " + response.header.code + " | SID: " + response.header.sid);
ws.close(1000, "");
return;
}
for (TokenItem token : response.payload.choices.text) {
System.out.print(token.content);
replyAccumulator.append(token.content);
}
if (response.header.status == 2) {
System.out.println("\n[End of Stream]");
ChatMessage assistantMsg = new ChatMessage();
assistantMsg.role = "assistant";
assistantMsg.content = replyAccumulator.toString();
if (conversationLog.size() >= 20) {
conversationLog.remove(0);
}
conversationLog.add(assistantMsg);
connectionReadyToClose = true;
awaitingUserInput = true;
}
}
Connection failures are caputred via onFailure, which logs the HTTP status code and terminates the process if the upgrade handshake fails.
@Override
public void onFailure(WebSocket ws, Throwable t, Response res) {
super.onFailure(ws, t, res);
if (res != null) {
System.err.println("Connection failed with code: " + res.code());
try {
System.err.println("Response body: " + res.body().string());
} catch (IOException ignored) {}
if (res.code() != 101) {
System.exit(1);
}
}
}