mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-11-03 22:45:32 +08:00
fix(llm): scope Azure query filtering and preserve endpoint parameters
Follow up PR #677: only remove api-version on Azure unified v1 endpoints, retain versions for other compatible gateways, and prefer explicit endpoint parameters over SDK defaults. Add regression coverage for query policies and tool-free finalization.
This commit is contained in:
parent
4de1e4ff0e
commit
07613e30ca
@ -59,19 +59,23 @@ final class OpenAiCompatibleHttpInterceptor implements Interceptor {
|
||||
rewritten.path("messages").size(),
|
||||
rewritten.path("tools").size(),
|
||||
rewritten.path("stream").asBoolean());
|
||||
// fix:https://github.com/mateaix/mateclaw/issues/640
|
||||
// The endpoint (provider baseUrl + completions path) is the authoritative URL.
|
||||
// Carry over SDK-attached query params selectively: openai-java classifies
|
||||
// *.openai.azure.com hosts whose baseUrl does not end in /openai/v1 as
|
||||
// AZURE_LEGACY and appends a default `api-version` query — which the unified
|
||||
// /openai/v1 surface rejects with 400 "API version not supported". Keep
|
||||
// `api-version` only when the endpoint itself is a legacy deployments URL,
|
||||
// where Azure requires it.
|
||||
// Only Azure's unified v1 surface rejects the legacy SDK api-version.
|
||||
// Keep explicit endpoint query values authoritative and avoid duplicating
|
||||
// parameters already present there when copying SDK query parameters.
|
||||
HttpUrl parsedEndpoint = HttpUrl.get(endpoint);
|
||||
boolean legacyDeploymentsEndpoint = parsedEndpoint.encodedPath().contains("/deployments/");
|
||||
String host = parsedEndpoint.host();
|
||||
boolean azureUnifiedEndpoint =
|
||||
(host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com"))
|
||||
&& parsedEndpoint.encodedPath().startsWith("/openai/v1/");
|
||||
HttpUrl.Builder url = parsedEndpoint.newBuilder();
|
||||
if (azureUnifiedEndpoint) {
|
||||
for (String name : parsedEndpoint.queryParameterNames()) {
|
||||
if ("api-version".equalsIgnoreCase(name)) url.removeAllQueryParameters(name);
|
||||
}
|
||||
}
|
||||
for (String name : original.url().queryParameterNames()) {
|
||||
if ("api-version".equalsIgnoreCase(name) && !legacyDeploymentsEndpoint) continue;
|
||||
if (azureUnifiedEndpoint && "api-version".equalsIgnoreCase(name)) continue;
|
||||
if (parsedEndpoint.queryParameterNames().contains(name)) continue;
|
||||
for (String value : original.url().queryParameterValues(name)) {
|
||||
url.addQueryParameter(name, value);
|
||||
}
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
package vip.mate.llm.chatmodel;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import okhttp3.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import vip.mate.llm.model.ModelProviderEntity;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class OpenAiAzureWirePolicyTest {
|
||||
private static final ObjectMapper JSON = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void stripsOrphanToolOptionsAndEmptyToolsWithoutMutatingSource() throws Exception {
|
||||
for (String tools : List.of("", ",\"tools\":[]")) {
|
||||
ObjectNode source = (ObjectNode) JSON.readTree("{\"model\":\"gpt-4o\",\"messages\":[],"
|
||||
+ "\"tool_choice\":\"none\",\"parallel_tool_calls\":false" + tools + "}");
|
||||
var out = OpenAiRequestRewriter.rewrite(source, provider(), false);
|
||||
assertFalse(out.has("tools"));
|
||||
assertFalse(out.has("tool_choice"));
|
||||
assertFalse(out.has("parallel_tool_calls"));
|
||||
assertTrue(source.has("tool_choice"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesToolDisableGuardWhenToolsExist() throws Exception {
|
||||
ObjectNode source = (ObjectNode) JSON.readTree("""
|
||||
{"model":"gpt-4o","messages":[],"tools":[{"type":"function","function":{"name":"lookup"}}],
|
||||
"tool_choice":"none","parallel_tool_calls":false}
|
||||
""");
|
||||
assertEquals(source, OpenAiRequestRewriter.rewrite(source, provider(), false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsOptionsForInjectedKimiSearchTool() throws Exception {
|
||||
ObjectNode source = (ObjectNode) JSON.readTree("{\"model\":\"kimi-k2\",\"messages\":[],\"tool_choice\":\"none\"}");
|
||||
var out = OpenAiRequestRewriter.rewrite(source, provider(), true);
|
||||
assertEquals("none", out.path("tool_choice").asText());
|
||||
assertEquals(1, out.path("tools").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedAzureDropsSdkVersionButPreservesOtherParameters() throws Exception {
|
||||
var url = wireUrl("https://demo.openai.azure.com/openai/v1/chat/completions",
|
||||
"https://demo.openai.azure.com/openai/deployments/gpt/chat/completions?api-version=2025-01-01&trace=a&trace=b");
|
||||
assertNull(url.queryParameter("api-version"));
|
||||
assertEquals(List.of("a", "b"), url.queryParameterValues("trace"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyAzureKeepsVersion() throws Exception {
|
||||
var url = wireUrl("https://demo.openai.azure.com/openai/deployments/gpt/chat/completions",
|
||||
"https://demo.openai.azure.com/openai/deployments/gpt/chat/completions?api-version=2025-01-01");
|
||||
assertEquals("2025-01-01", url.queryParameter("api-version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyConfiguredVersionWinsWithoutDuplicate() throws Exception {
|
||||
var url = wireUrl("https://demo.openai.azure.com/openai/deployments/gpt/chat/completions?api-version=custom",
|
||||
"https://demo.openai.azure.com/openai/deployments/gpt/chat/completions?api-version=sdk");
|
||||
assertEquals(List.of("custom"), url.queryParameterValues("api-version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonAzureGatewayKeepsVersionParameter() throws Exception {
|
||||
var url = wireUrl("https://gateway.example/v1/chat/completions",
|
||||
"https://gateway.example/v1/chat/completions?api-version=custom");
|
||||
assertEquals("custom", url.queryParameter("api-version"));
|
||||
}
|
||||
|
||||
private static HttpUrl wireUrl(String endpoint, String original) throws Exception {
|
||||
AtomicReference<HttpUrl> sent = new AtomicReference<>();
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.addInterceptor(new OpenAiCompatibleHttpInterceptor(provider(), endpoint, Map.of(), false, Duration.ofSeconds(3)))
|
||||
.addInterceptor(chain -> {
|
||||
sent.set(chain.request().url());
|
||||
return new Response.Builder().request(chain.request()).protocol(Protocol.HTTP_1_1).code(200)
|
||||
.message("OK").body(ResponseBody.create("{}", MediaType.get("application/json"))).build();
|
||||
}).build();
|
||||
Request request = new Request.Builder().url(original)
|
||||
.post(RequestBody.create("{\"model\":\"gpt-4o\",\"messages\":[]}", MediaType.get("application/json"))).build();
|
||||
try (Response ignored = client.newCall(request).execute()) { return sent.get(); }
|
||||
}
|
||||
|
||||
private static ModelProviderEntity provider() {
|
||||
var provider = new ModelProviderEntity();
|
||||
provider.setProviderId("azure-openai");
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user