fix: support Atlas multimodal generation and media preview

This commit is contained in:
ageerle
2026-09-11 10:49:29 +08:00
parent 4766d2e628
commit cca3090577
11 changed files with 498 additions and 13 deletions

View File

@@ -16,6 +16,12 @@ public final class ChatModelCredentialPolicy {
public static final String DEEPSEEK_API_HOST = "https://api.deepseek.com";
public static final String PPIO_PROVIDER = "ppio";
public static final String PPIO_API_HOST = "https://api.ppio.com/openai/v1";
public static final String ATLAS_PROVIDER = "atlas";
private static final Set<String> ATLAS_API_HOSTS = Set.of(
"https://api.atlascloud.ai", "https://api.atlascloud.ai/",
"https://api.atlascloud.ai/v1", "https://api.atlascloud.ai/v1/",
"https://api.atlascloud.ai/api/v1", "https://api.atlascloud.ai/api/v1/"
);
private static final Set<String> PPIO_API_HOSTS = Set.of(
PPIO_API_HOST, PPIO_API_HOST + "/",
@@ -69,6 +75,10 @@ public final class ChatModelCredentialPolicy {
String apiHost,
String apiKey) {
requireSecureApiHost(apiHost);
if (ATLAS_PROVIDER.equals(providerCode)) {
requireAtlasConfiguration(providerCode, modelName, apiHost, apiKey);
return;
}
if (CustomApiCredentialPolicy.isCustomProvider(providerCode)) {
CustomApiCredentialPolicy.requireConfiguration(providerCode, modelName, apiHost, apiKey);
return;
@@ -121,6 +131,8 @@ public final class ChatModelCredentialPolicy {
expectedReference = "env:DEEPSEEK_API_KEY";
} else if (PPIO_PROVIDER.equals(providerCode)) {
expectedReference = "env:PPIO_API_KEY";
} else if (ATLAS_PROVIDER.equals(providerCode)) {
expectedReference = "env:ATLAS_API_KEY";
} else {
throw new IllegalArgumentException("Model credential provider is not allowlisted");
}
@@ -137,11 +149,29 @@ public final class ChatModelCredentialPolicy {
CustomApiCredentialPolicy.requireConfiguration(providerCode, modelName, apiHost, apiKey);
} else if (PPIO_PROVIDER.equals(providerCode)) {
requirePpioConfiguration(providerCode, modelName, apiHost, apiKey);
} else if (ATLAS_PROVIDER.equals(providerCode)) {
requireAtlasConfiguration(providerCode, modelName, apiHost, apiKey);
} else {
requireDeepSeekConfiguration(providerCode, modelName, apiHost, apiKey);
}
}
/** Atlas credentials may only be sent to its official media API origin. */
public static void requireAtlasConfiguration(String providerCode, String modelName,
String apiHost, String apiKey) {
if (!ATLAS_PROVIDER.equals(providerCode)) {
throw new IllegalArgumentException("Atlas model provider is not trusted");
}
if (modelName == null || !modelName.matches("[A-Za-z0-9][A-Za-z0-9._/-]{0,254}")) {
throw new IllegalArgumentException("Atlas model ID is invalid");
}
requireSecureApiHost(apiHost);
if (!ATLAS_API_HOSTS.contains(apiHost)) {
throw new IllegalArgumentException("Atlas API host is not allowlisted");
}
requireProviderReference(providerCode, apiKey);
}
/** Validates PPIO and returns the base URL expected by the OpenAI Chat Completions client. */
public static String requirePpioConfiguration(String providerCode, String modelName,
String apiHost, String apiKey) {

View File

@@ -8,7 +8,7 @@ import java.util.regex.Pattern;
public final class ChatModelSecretReference {
public static final String ENV_REFERENCE_REGEXP =
"^env:(?:(?:DEEPSEEK|PPIO)_API_KEY|" + CustomApiCredentialPolicy.REFERENCE_PATTERN + ")$";
"^env:(?:(?:DEEPSEEK|PPIO|ATLAS)_API_KEY|" + CustomApiCredentialPolicy.REFERENCE_PATTERN + ")$";
private static final String ENV_PREFIX = "env:";
private static final Pattern ENV_REFERENCE = Pattern.compile(ENV_REFERENCE_REGEXP);

View File

@@ -0,0 +1,50 @@
package org.ruoyi.common.chat.security;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@Tag("dev")
class AtlasCredentialPolicyTest {
private static final String MODEL = "bytedance/seed-audio-1.0";
private static final String HOST = "https://api.atlascloud.ai/v1";
private static final String REF = "env:ATLAS_API_KEY";
@Test
void acceptsOfficialEndpointFormsAtSaveAndUse() {
for (String suffix : new String[]{"", "/", "/v1", "/v1/", "/api/v1", "/api/v1/"}) {
String host = "https://api.atlascloud.ai" + suffix;
assertDoesNotThrow(() -> ChatModelCredentialPolicy.requirePersistableConfiguration("atlas", MODEL, host, REF));
assertEquals("test-secret", ChatModelCredentialPolicy.resolveApiKeyForUse("atlas", "atlas", MODEL, host, REF,
name -> "ATLAS_API_KEY".equals(name) ? "test-secret" : null));
}
}
@Test
void rejectsUntrustedEndpointsBeforeReadingEnvironment() {
for (String host : new String[]{"http://api.atlascloud.ai/v1", "https://evil.example/v1",
"https://api.atlascloud.ai.evil.example/v1", "https://api.atlascloud.ai:8443/v1",
"https://user@api.atlascloud.ai/v1", "https://api.atlascloud.ai/v1?x=1", "https://api.atlascloud.ai/other"}) {
assertThrows(IllegalArgumentException.class,
() -> ChatModelCredentialPolicy.requirePersistableConfiguration("atlas", MODEL, host, REF));
assertThrows(IllegalArgumentException.class,
() -> ChatModelCredentialPolicy.resolveApiKeyForUse("atlas", "atlas", MODEL, host, REF,
name -> { fail("Must validate endpoint before resolving secret"); return null; }));
}
}
@Test
void rejectsWrongProviderReferenceAndMissingSecret() {
assertThrows(IllegalArgumentException.class, () -> ChatModelCredentialPolicy.resolveApiKeyForUse(
"openai", "atlas", MODEL, HOST, REF, name -> "secret"));
for (String key : new String[]{"env:PPIO_API_KEY", "plaintext", "", null}) {
assertThrows(IllegalArgumentException.class,
() -> ChatModelCredentialPolicy.requirePersistableConfiguration("atlas", MODEL, HOST, key));
}
assertThrows(IllegalStateException.class, () -> ChatModelCredentialPolicy.resolveApiKeyForUse(
"atlas", "atlas", MODEL, HOST, REF, name -> null));
assertThrows(IllegalArgumentException.class,
() -> ChatModelCredentialPolicy.requirePersistableConfiguration("atlas", "bad model", HOST, REF));
}
}