diff --git a/MCPForUnity/Editor/Services/AssetGen/AssetGenModelCatalog.cs b/MCPForUnity/Editor/Services/AssetGen/AssetGenModelCatalog.cs
index ef25af0da..0e6bcc840 100644
--- a/MCPForUnity/Editor/Services/AssetGen/AssetGenModelCatalog.cs
+++ b/MCPForUnity/Editor/Services/AssetGen/AssetGenModelCatalog.cs
@@ -68,6 +68,14 @@ public static class AssetGenModelCatalog
new ModelEntry { Id = "cassetteai/music-generator", Label = "CassetteAI Music", Provider = "fal", Kind = "audio", UseCase = "Background music", PriceLabel = "$0.02/min", MaxDurationSeconds = 180f,
DurationField = "duration", DefaultDurationSeconds = 10f, MinDurationSeconds = 1f },
new ModelEntry { Id = "fal-ai/lyria2", Label = "Google Lyria 2", Provider = "fal", Kind = "audio", UseCase = "Background music", PriceLabel = "$0.10/30s", MaxDurationSeconds = 30f },
+
+ // Audio — minimax (music generation, MiniMaxAudioAdapter.DefaultModel is first => the
+ // default). The endpoint produces a full track from the prompt, so there is no duration
+ // knob (prompt-only, DurationField null / MaxDurationSeconds 0).
+ new ModelEntry { Id = MiniMaxAudioAdapter.DefaultModel, Label = "MiniMax Music 3.0", Provider = "minimax", Kind = "audio", UseCase = "Background music" },
+ new ModelEntry { Id = "music-2.6", Label = "MiniMax Music 2.6", Provider = "minimax", Kind = "audio", UseCase = "Background music" },
+ new ModelEntry { Id = "music-3.0-free", Label = "MiniMax Music 3.0 (free)", Provider = "minimax", Kind = "audio", UseCase = "Background music" },
+ new ModelEntry { Id = "music-2.6-free", Label = "MiniMax Music 2.6 (free)", Provider = "minimax", Kind = "audio", UseCase = "Background music" },
};
/// Curated entries for a provider+kind, in curated order (default first). Never null.
diff --git a/MCPForUnity/Editor/Services/AssetGen/Providers/AssetGenProviders.cs b/MCPForUnity/Editor/Services/AssetGen/Providers/AssetGenProviders.cs
index 026de16b3..e127bfe8c 100644
--- a/MCPForUnity/Editor/Services/AssetGen/Providers/AssetGenProviders.cs
+++ b/MCPForUnity/Editor/Services/AssetGen/Providers/AssetGenProviders.cs
@@ -6,7 +6,7 @@ namespace MCPForUnity.Editor.Services.AssetGen.Providers
{
///
/// Factory + registry for asset-gen provider adapters. Resolves a provider id to its adapter
- /// (model: tripo/meshy; image: fal/openrouter; audio: fal; marketplace: sketchfab); unknown ids
+ /// (model: tripo/meshy; image: fal/openrouter; audio: fal/minimax; marketplace: sketchfab); unknown ids
/// throw . advertises providers and reports
/// Configured existence only — never a key value.
///
@@ -44,6 +44,8 @@ public static IAudioProviderAdapter Audio(string id)
{
case "fal":
return new FalAudioAdapter();
+ case "minimax":
+ return new MiniMaxAudioAdapter();
default:
throw new NotSupportedException($"Unknown audio provider '{id}'.");
}
@@ -71,6 +73,7 @@ public static IReadOnlyList List()
new ProviderInfo { Id = "openrouter", Kind = "image", Configured = IsConfigured("openrouter"), Capabilities = new[] { "text", "image" } },
// fal appears twice by design — once per kind (image + audio) — sharing the single "fal" key.
new ProviderInfo { Id = "fal", Kind = "audio", Configured = IsConfigured("fal"), Capabilities = new[] { "text", "music", "sfx" } },
+ new ProviderInfo { Id = "minimax", Kind = "audio", Configured = IsConfigured("minimax"), Capabilities = new[] { "text", "music" } },
};
}
diff --git a/MCPForUnity/Editor/Services/AssetGen/Providers/MiniMaxAudioAdapter.cs b/MCPForUnity/Editor/Services/AssetGen/Providers/MiniMaxAudioAdapter.cs
new file mode 100644
index 000000000..3bb2382dc
--- /dev/null
+++ b/MCPForUnity/Editor/Services/AssetGen/Providers/MiniMaxAudioAdapter.cs
@@ -0,0 +1,220 @@
+using System;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MCPForUnity.Editor.Security;
+using MCPForUnity.Editor.Services.AssetGen.Http;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace MCPForUnity.Editor.Services.AssetGen.Providers
+{
+ ///
+ /// MiniMax audio provider for background-music generation via the (synchronous)
+ /// /v1/music_generation endpoint. The prompt drives an instrumental track; the endpoint
+ /// returns the result inline in a single response (no task id / poll endpoint exists), so the
+ /// work happens in and returns it immediately —
+ /// the same shape as the OpenRouter image adapter. One adapter instance handles a single job.
+ ///
+ /// The region is selected via MCPFORUNITY_MINIMAX_REGION (global — default — or
+ /// cn) so both the global (api.minimax.io) and China (api.minimaxi.com)
+ /// hosts are reachable; the key is only ever attached to the resolved region host.
+ ///
+ public sealed class MiniMaxAudioAdapter : IAudioProviderAdapter
+ {
+ internal const string GlobalEndpoint = "https://api.minimax.io/v1/music_generation";
+ internal const string ChinaEndpoint = "https://api.minimaxi.com/v1/music_generation";
+ private const string GlobalHost = "api.minimax.io";
+ private const string ChinaHost = "api.minimaxi.com";
+ internal const string RegionEnvVar = "MCPFORUNITY_MINIMAX_REGION";
+
+ // Default background-music model. internal so the catalog references it directly (single
+ // source of truth, drift-guarded).
+ internal const string DefaultModel = "music-3.0";
+
+ // We request mp3 (an import-allowed extension) and pin the result extension to it, so a
+ // hosted result URL without a clean extension still imports correctly.
+ private const string AudioFormat = "mp3";
+
+ public string Id => "minimax";
+
+ // Set once in SubmitAsync (synchronous provider), read back in PollAsync.
+ private byte[] _inlineData;
+ private string _downloadUrl;
+ private string _resultExt;
+ private string _error;
+
+ public async Task SubmitAsync(AudioGenRequest req, string apiKey, IHttpTransport http, CancellationToken ct)
+ {
+ if (req == null) throw new ArgumentNullException(nameof(req));
+ if (http == null) throw new ArgumentNullException(nameof(http));
+
+ string model = string.IsNullOrEmpty(req.Model) ? DefaultModel : req.Model;
+ ResolveRegion(out string endpoint, out string host);
+ // The endpoint is adapter-constructed, but validate the host before attaching the key so
+ // a mis-set region can never send credentials to an unexpected host.
+ ProviderHttp.RequireHost(endpoint, host, apiKey, "minimax submit");
+
+ var spec = new HttpRequestSpec
+ {
+ Method = "POST",
+ Url = endpoint,
+ ContentType = "application/json",
+ Body = Encoding.UTF8.GetBytes(BuildBody(model, req).ToString(Formatting.None))
+ };
+ spec.Headers["Authorization"] = "Bearer " + apiKey;
+
+ HttpResult res = await http.SendAsync(spec, ct);
+ JObject json = ParseOk(res, apiKey);
+
+ // base_resp.status_code == 0 means the request itself succeeded; anything else is an
+ // application-level error (auth, quota, invalid params).
+ int statusCode = AsInt(json["base_resp"]?["status_code"], -1);
+ if (statusCode != 0)
+ {
+ string msg = json["base_resp"]?["status_msg"]?.ToString();
+ _error = SecretRedactor.Scrub(
+ $"MiniMax music generation failed (status_code={statusCode}): {msg ?? "unknown error"}", apiKey);
+ return "ready";
+ }
+
+ JToken data = json["data"];
+ // data.status: 1 = in_progress, 2 = completed. The endpoint is synchronous, so a
+ // non-completed status has no poll target to recover from — surface it as a failure.
+ int genStatus = AsInt(data?["status"], -1);
+ if (genStatus != 2)
+ {
+ _error = $"MiniMax music generation did not complete (status={genStatus}).";
+ return "ready";
+ }
+
+ string audio = data?["audio"]?.ToString();
+ if (string.IsNullOrEmpty(audio))
+ {
+ _error = "MiniMax completed but returned no audio.";
+ return "ready";
+ }
+
+ // output_format may be a hosted URL or a hex-encoded audio payload; handle both.
+ if (audio.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
+ || audio.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
+ {
+ _downloadUrl = audio;
+ }
+ else
+ {
+ byte[] bytes = TryDecodeHex(audio);
+ if (bytes == null || bytes.Length == 0)
+ {
+ _error = "MiniMax returned an unrecognized audio payload.";
+ return "ready";
+ }
+ _inlineData = bytes;
+ }
+ _resultExt = AudioFormat;
+ return "ready";
+ }
+
+ public Task PollAsync(string providerJobId, string apiKey, IHttpTransport http, CancellationToken ct)
+ {
+ var result = new ProviderPollResult { Progress = 1f };
+ if (!string.IsNullOrEmpty(_error) || (_inlineData == null && string.IsNullOrEmpty(_downloadUrl)))
+ {
+ result.State = ProviderPollState.Failed;
+ result.Error = _error ?? "MiniMax produced no audio.";
+ }
+ else
+ {
+ result.State = ProviderPollState.Succeeded;
+ result.InlineData = _inlineData;
+ result.DownloadUrl = _downloadUrl;
+ result.ResultExt = _resultExt;
+ }
+ return Task.FromResult(result);
+ }
+
+ // Prompt-driven instrumental background music. Duration is not a request parameter for this
+ // endpoint (the model produces a full track), so req.Duration is intentionally unused.
+ private static JObject BuildBody(string model, AudioGenRequest req)
+ {
+ return new JObject
+ {
+ ["model"] = model,
+ ["prompt"] = req.Prompt ?? string.Empty,
+ ["is_instrumental"] = true,
+ ["output_format"] = "url",
+ ["audio_setting"] = new JObject
+ {
+ ["sample_rate"] = 44100,
+ ["bitrate"] = 256000,
+ ["format"] = AudioFormat
+ }
+ };
+ }
+
+ private static void ResolveRegion(out string endpoint, out string host)
+ {
+ string region = (Environment.GetEnvironmentVariable(RegionEnvVar) ?? string.Empty).Trim().ToLowerInvariant();
+ if (region == "cn" || region == "cn_zh" || region == "china")
+ {
+ endpoint = ChinaEndpoint;
+ host = ChinaHost;
+ }
+ else
+ {
+ endpoint = GlobalEndpoint;
+ host = GlobalHost;
+ }
+ }
+
+ private static int AsInt(JToken token, int fallback)
+ {
+ if (token == null || token.Type == JTokenType.Null) return fallback;
+ if (token.Type == JTokenType.Integer) return (int)token;
+ return int.TryParse(token.ToString(), out int v) ? v : fallback;
+ }
+
+ private static byte[] TryDecodeHex(string hex)
+ {
+ if (string.IsNullOrEmpty(hex) || (hex.Length % 2) != 0) return null;
+ var bytes = new byte[hex.Length / 2];
+ for (int i = 0; i < bytes.Length; i++)
+ {
+ int hi = HexVal(hex[i * 2]);
+ int lo = HexVal(hex[i * 2 + 1]);
+ if (hi < 0 || lo < 0) return null;
+ bytes[i] = (byte)((hi << 4) | lo);
+ }
+ return bytes;
+ }
+
+ private static int HexVal(char c)
+ {
+ if (c >= '0' && c <= '9') return c - '0';
+ if (c >= 'a' && c <= 'f') return c - 'a' + 10;
+ if (c >= 'A' && c <= 'F') return c - 'A' + 10;
+ return -1;
+ }
+
+ private static JObject ParseOk(HttpResult res, string apiKey)
+ {
+ string text = ProviderHttp.BodyText(res);
+
+ JObject json = null;
+ if (!string.IsNullOrEmpty(text))
+ {
+ try { json = JObject.Parse(text); } catch { /* non-JSON */ }
+ }
+
+ bool ok = res?.Ok == true;
+ if (!ok)
+ {
+ string detail = json?["base_resp"]?["status_msg"]?.ToString()
+ ?? json?["error"]?.ToString()
+ ?? ProviderHttp.Truncate(text);
+ throw new Exception(SecretRedactor.Scrub($"MiniMax request failed (status={res?.Status}): {detail}", apiKey));
+ }
+ return json ?? new JObject();
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Services/AssetGen/Providers/MiniMaxAudioAdapter.cs.meta b/MCPForUnity/Editor/Services/AssetGen/Providers/MiniMaxAudioAdapter.cs.meta
new file mode 100644
index 000000000..812b7458b
--- /dev/null
+++ b/MCPForUnity/Editor/Services/AssetGen/Providers/MiniMaxAudioAdapter.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: edcf0554d7a94b0ab2d469529e08c2d1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenModelCatalogTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenModelCatalogTests.cs
index 9ef048e6c..bd448324b 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenModelCatalogTests.cs
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenModelCatalogTests.cs
@@ -80,6 +80,21 @@ public void DefaultModelId_MatchesAdapterConstants()
Assert.AreEqual(MeshyAdapter.DefaultModel, AssetGenModelCatalog.DefaultModelId("meshy", "model"));
Assert.AreEqual(FalAdapter.DefaultModel, AssetGenModelCatalog.DefaultModelId("fal", "image"));
Assert.AreEqual(FalAudioAdapter.DefaultModel, AssetGenModelCatalog.DefaultModelId("fal", "audio"));
+ Assert.AreEqual(MiniMaxAudioAdapter.DefaultModel, AssetGenModelCatalog.DefaultModelId("minimax", "audio"));
+ }
+
+ [Test]
+ public void Curated_HasMiniMaxMusicGenerationModels()
+ {
+ IReadOnlyList audio = AssetGenModelCatalog.ForProvider("minimax", "audio");
+
+ CollectionAssert.AreEqual(
+ new[] { "music-3.0", "music-2.6", "music-3.0-free", "music-2.6-free" },
+ audio.Select(e => e.Id).ToList());
+
+ // Music generation has no duration knob (the model returns a full track).
+ foreach (ModelEntry e in audio)
+ Assert.IsNull(e.DurationField, e.Id + " should be prompt-only (no duration control)");
}
[Test]
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenProvidersTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenProvidersTests.cs
index 2b8e0b84f..e79585a51 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenProvidersTests.cs
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/AssetGenProvidersTests.cs
@@ -35,6 +35,14 @@ public void Audio_Fal_ReturnsAdapter()
Assert.AreEqual("fal", adapter.Id);
}
+ [Test]
+ public void Audio_MiniMax_ReturnsAdapter()
+ {
+ IAudioProviderAdapter adapter = AssetGenProviders.Audio("minimax");
+ Assert.IsNotNull(adapter);
+ Assert.AreEqual("minimax", adapter.Id);
+ }
+
[Test]
public void Audio_Unimplemented_Throws()
{
@@ -49,6 +57,15 @@ public void List_IncludesFalAudioRow()
Assert.AreEqual("audio", row.Kind);
}
+ [Test]
+ public void List_IncludesMiniMaxAudioRow()
+ {
+ ProviderInfo row = FindByIdKind("minimax", "audio");
+ Assert.IsNotNull(row, "List() should advertise a minimax audio row");
+ Assert.AreEqual("audio", row.Kind);
+ CollectionAssert.Contains(row.Capabilities, "music");
+ }
+
[Test]
public void List_HasExactlyOneFalImage_AndOneFalAudio()
{
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/MiniMaxAudioAdapterTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/MiniMaxAudioAdapterTests.cs
new file mode 100644
index 000000000..d882cc055
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/MiniMaxAudioAdapterTests.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Text;
+using System.Threading;
+using MCPForUnity.Editor.Services.AssetGen.Http;
+using MCPForUnity.Editor.Services.AssetGen.Providers;
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+
+namespace MCPForUnityTests.Editor.AssetGen
+{
+ ///
+ /// Covers the MiniMax music-generation adapter: it posts the region endpoint with a Bearer key,
+ /// parses the synchronous response (base_resp.status_code / data.status / data.audio), returns a
+ /// hosted URL or a decoded hex payload, honours the region env override, and never leaks the key
+ /// to an unexpected host.
+ ///
+ public class MiniMaxAudioAdapterTests
+ {
+ private const string GlobalHost = "api.minimax.io";
+ private const string ChinaHost = "api.minimaxi.com";
+
+ private static HttpResult Json(string body) => new HttpResult { Status = 200, IsSuccess = true, Text = body };
+
+ private static HttpResult Completed(string audio) =>
+ Json("{\"data\":{\"status\":2,\"audio\":\"" + audio + "\"},\"base_resp\":{\"status_code\":0,\"status_msg\":\"success\"}}");
+
+ private static AudioGenRequest Req(string model = null) =>
+ new AudioGenRequest { Provider = "minimax", Model = model, Prompt = "calm ambient background" };
+
+ private static string SubmittedBody(FakeHttpTransport fake) =>
+ Encoding.UTF8.GetString(fake.RecordedRequests[0].Body);
+
+ [SetUp]
+ public void SetUp() => Environment.SetEnvironmentVariable(MiniMaxAudioAdapter.RegionEnvVar, null);
+
+ [TearDown]
+ public void TearDown() => Environment.SetEnvironmentVariable(MiniMaxAudioAdapter.RegionEnvVar, null);
+
+ [Test]
+ public void Submit_PostsGlobalEndpoint_WithBearerKey_JsonBody()
+ {
+ var fake = new FakeHttpTransport { Handler = _ => Completed("https://cdn.minimax.io/a.mp3") };
+ var adapter = new MiniMaxAudioAdapter();
+
+ string pid = adapter.SubmitAsync(Req(), "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult();
+
+ Assert.IsFalse(string.IsNullOrEmpty(pid), "submit must return a non-empty provider job id");
+ HttpRequestSpec sent = fake.RecordedRequests[0];
+ Assert.AreEqual("POST", sent.Method);
+ StringAssert.Contains(GlobalHost, sent.Url);
+ Assert.IsTrue(sent.Headers.ContainsKey("Authorization"));
+ StringAssert.StartsWith("Bearer ", sent.Headers["Authorization"]);
+ }
+
+ [Test]
+ public void Submit_DefaultModel_MatchesAdapterConstant()
+ {
+ var fake = new FakeHttpTransport { Handler = _ => Completed("https://cdn.minimax.io/a.mp3") };
+ var adapter = new MiniMaxAudioAdapter();
+
+ adapter.SubmitAsync(Req(), "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult();
+
+ JObject body = JObject.Parse(SubmittedBody(fake));
+ Assert.AreEqual(MiniMaxAudioAdapter.DefaultModel, (string)body["model"]);
+ }
+
+ [Test]
+ public void Submit_ModelOverride_IsSent()
+ {
+ var fake = new FakeHttpTransport { Handler = _ => Completed("https://cdn.minimax.io/a.mp3") };
+ var adapter = new MiniMaxAudioAdapter();
+
+ adapter.SubmitAsync(Req("music-2.6"), "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult();
+
+ JObject body = JObject.Parse(SubmittedBody(fake));
+ Assert.AreEqual("music-2.6", (string)body["model"]);
+ }
+
+ [Test]
+ public void Submit_CnRegion_PostsChinaEndpoint()
+ {
+ Environment.SetEnvironmentVariable(MiniMaxAudioAdapter.RegionEnvVar, "cn");
+ var fake = new FakeHttpTransport { Handler = _ => Completed("https://cdn.minimaxi.com/a.mp3") };
+ var adapter = new MiniMaxAudioAdapter();
+
+ adapter.SubmitAsync(Req(), "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult();
+
+ StringAssert.Contains(ChinaHost, fake.RecordedRequests[0].Url);
+ }
+
+ [Test]
+ public void Poll_UrlResult_Succeeds_WithDownloadUrl_AndMp3Ext()
+ {
+ var fake = new FakeHttpTransport { Handler = _ => Completed("https://cdn.minimax.io/track.mp3") };
+ var adapter = new MiniMaxAudioAdapter();
+
+ string pid = adapter.SubmitAsync(Req(), "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult();
+ ProviderPollResult pr = adapter.PollAsync(pid, "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult();
+
+ Assert.AreEqual(ProviderPollState.Succeeded, pr.State);
+ Assert.AreEqual("https://cdn.minimax.io/track.mp3", pr.DownloadUrl);
+ Assert.AreEqual("mp3", pr.ResultExt);
+ Assert.IsNull(pr.InlineData);
+ }
+
+ [Test]
+ public void Poll_HexResult_DecodesToInlineData()
+ {
+ // "ID3" (mp3 header bytes) hex-encoded => 494433.
+ var fake = new FakeHttpTransport { Handler = _ => Completed("494433") };
+ var adapter = new MiniMaxAudioAdapter();
+
+ string pid = adapter.SubmitAsync(Req(), "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult();
+ ProviderPollResult pr = adapter.PollAsync(pid, "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult();
+
+ Assert.AreEqual(ProviderPollState.Succeeded, pr.State);
+ Assert.IsNotNull(pr.InlineData);
+ CollectionAssert.AreEqual(new byte[] { 0x49, 0x44, 0x33 }, pr.InlineData);
+ Assert.AreEqual("mp3", pr.ResultExt);
+ }
+
+ [Test]
+ public void Submit_NonZeroStatusCode_Fails_AndRedactsKey()
+ {
+ var fake = new FakeHttpTransport
+ {
+ Handler = _ => Json("{\"base_resp\":{\"status_code\":1004,\"status_msg\":\"bad key mmkey123\"}}")
+ };
+ var adapter = new MiniMaxAudioAdapter();
+
+ string pid = adapter.SubmitAsync(Req(), "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult();
+ ProviderPollResult pr = adapter.PollAsync(pid, "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult();
+
+ Assert.AreEqual(ProviderPollState.Failed, pr.State);
+ StringAssert.DoesNotContain("mmkey123", pr.Error);
+ }
+
+ [Test]
+ public void Submit_InProgressStatus_Fails_NoPollTarget()
+ {
+ var fake = new FakeHttpTransport
+ {
+ Handler = _ => Json("{\"data\":{\"status\":1},\"base_resp\":{\"status_code\":0}}")
+ };
+ var adapter = new MiniMaxAudioAdapter();
+
+ string pid = adapter.SubmitAsync(Req(), "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult();
+ ProviderPollResult pr = adapter.PollAsync(pid, "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult();
+
+ Assert.AreEqual(ProviderPollState.Failed, pr.State);
+ }
+
+ [Test]
+ public void Submit_CompletedNoAudio_Fails()
+ {
+ var fake = new FakeHttpTransport
+ {
+ Handler = _ => Json("{\"data\":{\"status\":2},\"base_resp\":{\"status_code\":0}}")
+ };
+ var adapter = new MiniMaxAudioAdapter();
+
+ string pid = adapter.SubmitAsync(Req(), "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult();
+ ProviderPollResult pr = adapter.PollAsync(pid, "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult();
+
+ Assert.AreEqual(ProviderPollState.Failed, pr.State);
+ StringAssert.Contains("audio", pr.Error);
+ }
+
+ [Test]
+ public void Submit_HttpError_Throws_RedactsKey()
+ {
+ var fake = new FakeHttpTransport
+ {
+ Handler = _ => new HttpResult { Status = 401, IsSuccess = false, Text = "{\"base_resp\":{\"status_msg\":\"unauthorized mmkey123\"}}" }
+ };
+ var adapter = new MiniMaxAudioAdapter();
+
+ Exception ex = Assert.Throws(() =>
+ adapter.SubmitAsync(Req(), "mmkey123", fake, CancellationToken.None).GetAwaiter().GetResult());
+ StringAssert.DoesNotContain("mmkey123", ex.Message);
+ }
+ }
+}
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/MiniMaxAudioAdapterTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/MiniMaxAudioAdapterTests.cs.meta
new file mode 100644
index 000000000..706788edf
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/AssetGen/MiniMaxAudioAdapterTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: cc3f40ccdaa241979023e3ab09a9cd23
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: