Skip to content

Commit 004b226

Browse files
committed
Merge remote-tracking branch 'origin/fix/unstable-computehash' into fix/unstable-computehash
2 parents 0392f38 + a0f5e43 commit 004b226

19 files changed

Lines changed: 956 additions & 72 deletions

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ target/
2727
build/
2828
!**/src/main/**/build/
2929
!**/src/test/**/build/
30-
3130
### VS Code ###
3231
.vscode/
3332
.claude/

agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/DashScopeMessageConverter.java

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,12 @@ private DashScopeMessage convertToMultimodalContent(Msg msg) {
168168
private DashScopeMessage convertToolRoleMessage(Msg msg) {
169169
ToolResultBlock toolResult = msg.getFirstContentBlock(ToolResultBlock.class);
170170
if (toolResult != null) {
171-
String toolResultText = toolResultConverter.apply(toolResult.getOutput());
172-
List<DashScopeContentPart> content = new ArrayList<>();
173-
content.add(DashScopeContentPart.text(toolResultText));
171+
List<DashScopeContentPart> content =
172+
hasMediaContent(toolResult.getOutput())
173+
? convertContentBlocks(toolResult.getOutput())
174+
: List.of(
175+
DashScopeContentPart.text(
176+
toolResultConverter.apply(toolResult.getOutput())));
174177

175178
return DashScopeMessage.builder()
176179
.role("tool")
@@ -181,9 +184,10 @@ private DashScopeMessage convertToolRoleMessage(Msg msg) {
181184
}
182185

183186
// Fallback: no ToolResultBlock found, use text content
184-
List<DashScopeContentPart> content = new ArrayList<>();
185-
content.add(DashScopeContentPart.text(extractTextContent(msg)));
186-
return DashScopeMessage.builder().role("tool").content(content).build();
187+
return DashScopeMessage.builder()
188+
.role("tool")
189+
.content(List.of(DashScopeContentPart.text(extractTextContent(msg))))
190+
.build();
187191
}
188192

189193
/**
@@ -259,4 +263,67 @@ private void applyCacheControlFromMetadata(Msg msg, DashScopeMessage result) {
259263
result.setCacheControl(DashScopeChatFormatter.getEphemeralCacheControl());
260264
}
261265
}
266+
267+
/**
268+
* Check if blocks contain media content (image, audio, video).
269+
*
270+
* @param blocks the list of content blocks to check
271+
* @return true if any block is ImageBlock, AudioBlock, or VideoBlock
272+
*/
273+
private boolean hasMediaContent(List<ContentBlock> blocks) {
274+
if (blocks == null) {
275+
return false;
276+
}
277+
for (ContentBlock block : blocks) {
278+
if (block instanceof ImageBlock
279+
|| block instanceof AudioBlock
280+
|| block instanceof VideoBlock) {
281+
return true;
282+
}
283+
}
284+
return false;
285+
}
286+
287+
/**
288+
* Convert content blocks to DashScope content parts for multimodal messages.
289+
*
290+
* @param blocks the list of content blocks to convert
291+
* @return the converted list of DashScopeContentPart
292+
*/
293+
private List<DashScopeContentPart> convertContentBlocks(List<ContentBlock> blocks) {
294+
List<DashScopeContentPart> content = new ArrayList<>();
295+
for (ContentBlock block : blocks) {
296+
if (block instanceof TextBlock tb) {
297+
content.add(DashScopeContentPart.text(tb.getText()));
298+
} else if (block instanceof ImageBlock ib) {
299+
try {
300+
content.add(mediaConverter.convertImageBlockToContentPart(ib));
301+
} catch (Exception e) {
302+
log.warn("Failed to process ImageBlock in tool result: {}", e.getMessage());
303+
content.add(
304+
DashScopeContentPart.text(
305+
"[Image - processing failed: " + e.getMessage() + "]"));
306+
}
307+
} else if (block instanceof AudioBlock ab) {
308+
try {
309+
content.add(mediaConverter.convertAudioBlockToContentPart(ab));
310+
} catch (Exception e) {
311+
log.warn("Failed to process AudioBlock in tool result: {}", e.getMessage());
312+
content.add(
313+
DashScopeContentPart.text(
314+
"[Audio - processing failed: " + e.getMessage() + "]"));
315+
}
316+
} else if (block instanceof VideoBlock vb) {
317+
try {
318+
content.add(mediaConverter.convertVideoBlockToContentPart(vb));
319+
} catch (Exception e) {
320+
log.warn("Failed to process VideoBlock in tool result: {}", e.getMessage());
321+
content.add(
322+
DashScopeContentPart.text(
323+
"[Video - processing failed: " + e.getMessage() + "]"));
324+
}
325+
}
326+
}
327+
return content;
328+
}
262329
}

agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIResponse.java

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,18 @@ public class OpenAIResponse {
100100
@JsonProperty("error")
101101
private OpenAIError error;
102102

103+
/** Error code for non-standard error responses. */
104+
@JsonProperty("code")
105+
private String code;
106+
107+
/** Error message for non-standard error responses. */
108+
@JsonProperty("message")
109+
private String message;
110+
111+
/** Status for non-standard error responses. */
112+
@JsonProperty("status")
113+
private String status;
114+
103115
public OpenAIResponse() {}
104116

105117
public String getId() {
@@ -166,13 +178,84 @@ public void setError(OpenAIError error) {
166178
this.error = error;
167179
}
168180

181+
public String getCode() {
182+
return code;
183+
}
184+
185+
public void setCode(String code) {
186+
this.code = code;
187+
}
188+
189+
public String getMessage() {
190+
return message;
191+
}
192+
193+
public void setMessage(String message) {
194+
this.message = message;
195+
}
196+
197+
public String getStatus() {
198+
return status;
199+
}
200+
201+
public void setStatus(String status) {
202+
this.status = status;
203+
}
204+
169205
/**
170206
* Check if this response represents an error.
171207
*
208+
* Support the detection of standard OpenAI error structures
209+
* and non-standard code/status error structures using a blacklist strategy.
210+
*
172211
* @return true if the response contains an error
173212
*/
174213
public boolean isError() {
175-
return error != null;
214+
// Standard OpenAI error format
215+
if (error != null) {
216+
return true;
217+
}
218+
219+
// Double check using the status field
220+
if ("error".equalsIgnoreCase(status) || "failed".equalsIgnoreCase(status)) {
221+
return true;
222+
}
223+
224+
// Blacklist strategy for custom error codes
225+
if (code != null) {
226+
try {
227+
int numericCode = Integer.parseInt(code);
228+
if (numericCode >= 400 && numericCode <= 599) {
229+
return true;
230+
}
231+
} catch (NumberFormatException e) {
232+
// If code is not numeric (e.g., "ok", "success", "invalid_key"),
233+
// we rely on the 'status' or 'error' fields checked above.
234+
// Do not blindly assume it's an error.
235+
}
236+
}
237+
238+
return false;
239+
}
240+
241+
/**
242+
* Get the effective error message, handling both standard and non-standard formats.
243+
*/
244+
public String getEffectiveErrorMessage() {
245+
if (error != null && error.getMessage() != null) {
246+
return error.getMessage();
247+
}
248+
return message != null ? message : "Unknown error";
249+
}
250+
251+
/**
252+
* Get the effective error code, handling both standard and non-standard formats.
253+
*/
254+
public String getEffectiveErrorCode() {
255+
if (error != null && error.getCode() != null) {
256+
return error.getCode();
257+
}
258+
return code != null ? code : "unknown_error";
176259
}
177260

178261
/**

agentscope-core/src/main/java/io/agentscope/core/model/OpenAIClient.java

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
package io.agentscope.core.model;
1717

1818
import io.agentscope.core.Version;
19-
import io.agentscope.core.formatter.openai.dto.OpenAIError;
2019
import io.agentscope.core.formatter.openai.dto.OpenAIRequest;
2120
import io.agentscope.core.formatter.openai.dto.OpenAIResponse;
2221
import io.agentscope.core.model.exception.OpenAIException;
@@ -96,6 +95,7 @@ public OpenAIClient() {
9695
}
9796

9897
private static final Pattern VERSION_PATTERN = Pattern.compile(".*/v\\d+$");
98+
private static final Pattern RATE_LIMIT_PATTERN = Pattern.compile(".*\\b429\\b.*");
9999

100100
/**
101101
* Normalize the base URL by removing trailing slashes.
@@ -333,22 +333,11 @@ public OpenAIResponse call(
333333
}
334334

335335
if (response.isError()) {
336-
OpenAIError error = response.getError();
337-
if (error == null) {
338-
throw new OpenAIException(
339-
"OpenAI API returned error but error details are null",
340-
400,
341-
"unknown_error",
342-
responseBody);
343-
}
344-
String errorMessage =
345-
error.getMessage() != null ? error.getMessage() : "Unknown error";
346-
String errorCode = error.getCode() != null ? error.getCode() : "unknown_error";
336+
String errorMessage = response.getEffectiveErrorMessage();
337+
String errorCode = response.getEffectiveErrorCode();
338+
int statusCode = resolveErrorStatusCode(httpResponse.getStatusCode(), errorCode);
347339
throw OpenAIException.create(
348-
httpResponse.getStatusCode(),
349-
"OpenAI API error: " + errorMessage,
350-
errorCode,
351-
responseBody);
340+
statusCode, "OpenAI API error: " + errorMessage, errorCode, responseBody);
352341
}
353342

354343
return response;
@@ -415,22 +404,16 @@ public Flux<OpenAIResponse> stream(
415404
if (response != null) {
416405
// Check for error in streaming response chunk
417406
if (response.isError()) {
418-
OpenAIError error = response.getError();
419-
String errorMessage =
420-
error != null && error.getMessage() != null
421-
? error.getMessage()
422-
: "Unknown error in streaming response";
423-
String errorCode =
424-
error != null && error.getCode() != null
425-
? error.getCode()
426-
: null;
407+
String errorMessage = response.getEffectiveErrorMessage();
408+
String errorCode = response.getEffectiveErrorCode();
409+
int statusCode = resolveErrorStatusCode(200, errorCode);
427410
sink.error(
428411
OpenAIException.create(
429-
400,
412+
statusCode,
430413
"OpenAI API error in streaming response: "
431414
+ errorMessage,
432415
errorCode,
433-
null));
416+
data));
434417
return;
435418
}
436419
sink.next(response);
@@ -458,6 +441,43 @@ public Flux<OpenAIResponse> stream(
458441
}
459442
}
460443

444+
/**
445+
* Resolve the actual HTTP error status code when the API returns 200 OK
446+
* but contains an error payload.
447+
*
448+
* @param httpStatusCode the original HTTP status code
449+
* @param errorCode the error code extracted from the response body
450+
* @return a valid HTTP error status code (4xx or 5xx)
451+
*/
452+
private int resolveErrorStatusCode(int httpStatusCode, String errorCode) {
453+
if (httpStatusCode >= 400) {
454+
return httpStatusCode;
455+
}
456+
457+
// Handling HTTP 200 with Body containing errors
458+
if (errorCode != null) {
459+
// Use a regex to ensure "429" appears as a standalone word
460+
// For example, "HTTP 429 Too Many Requests"
461+
// And compatible with OpenAI's standard strings
462+
if (RATE_LIMIT_PATTERN.matcher(errorCode).matches()
463+
|| "rate_limit_exceeded".equalsIgnoreCase(errorCode)) {
464+
return 429;
465+
}
466+
467+
try {
468+
int parsedCode = Integer.parseInt(errorCode);
469+
if (parsedCode >= 400 && parsedCode <= 599) {
470+
return parsedCode;
471+
}
472+
} catch (NumberFormatException e) {
473+
// Ignore error codes of non numeric types
474+
}
475+
}
476+
477+
// Extraction failed, return to default
478+
return 400;
479+
}
480+
461481
/**
462482
* Parse a single SSE data line to OpenAIResponse.
463483
*

agentscope-core/src/main/java/io/agentscope/core/tool/AgentTool.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,19 @@ public interface AgentTool {
7474
*/
7575
Map<String, Object> getParameters();
7676

77+
/**
78+
* Gets strict mode configuration for this tool schema.
79+
*
80+
* <p>When strict mode is enabled, compatible model providers are expected to enforce stricter
81+
* adherence to the tool parameter schema. Returning {@code null} means no explicit strict mode
82+
* preference is provided.
83+
*
84+
* @return strict mode value ({@code true}/{@code false}) or {@code null} when unspecified
85+
*/
86+
default Boolean getStrict() {
87+
return null;
88+
}
89+
7790
/**
7891
* Gets the optional output schema for this tool in JSON Schema format.
7992
*

0 commit comments

Comments
 (0)