From 9c5d88f8ed20b8cd08d5c879894df58a6c9c2dc8 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Mon, 3 Aug 2026 15:31:08 +0200 Subject: [PATCH 1/4] fix(minimax): map native speech status codes to real errors MiniMax reports t2a_v2 failures as HTTP 200 with a non-zero base_resp.status_code. Relaying every one as a 502 masked caller mistakes, so map the common codes: rate limits (1002/1039) to 429, authentication (1004) to 401, insufficient balance (1008) to 402, and sensitive input / invalid parameters (1026/2013) to 400. Unknown codes still surface as provider errors, now with the native code in the message. Co-Authored-By: Claude Fable 5 --- internal/providers/minimax/audio.go | 30 +++++++++-- internal/providers/minimax/audio_test.go | 63 ++++++++++++++++++------ 2 files changed, 74 insertions(+), 19 deletions(-) diff --git a/internal/providers/minimax/audio.go b/internal/providers/minimax/audio.go index 73cef80b..7120af05 100644 --- a/internal/providers/minimax/audio.go +++ b/internal/providers/minimax/audio.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/hex" + "fmt" "io" "math" "net/http" @@ -117,11 +118,7 @@ func (p *Provider) CreateSpeech(ctx context.Context, req *core.AudioSpeechReques return nil, core.NewProviderError("minimax", http.StatusBadGateway, "failed to parse speech response", err) } if response.BaseResponse.StatusCode != 0 { - message := "minimax speech request failed" - if statusMessage := strings.TrimSpace(response.BaseResponse.StatusMsg); statusMessage != "" { - message += ": " + statusMessage - } - return nil, core.NewProviderError("minimax", http.StatusBadGateway, message, nil) + return nil, speechStatusError(response.BaseResponse.StatusCode, response.BaseResponse.StatusMsg) } if response.Data == nil || strings.TrimSpace(response.Data.Audio) == "" { return nil, core.NewProviderError("minimax", http.StatusBadGateway, "speech response contains no audio", nil) @@ -140,6 +137,29 @@ func (p *Provider) CreateSpeech(ctx context.Context, req *core.AudioSpeechReques }, nil } +// speechStatusError maps a MiniMax base_resp status code to a gateway error. +// MiniMax reports failures as HTTP 200 with a non-zero base_resp.status_code, +// so caller mistakes (invalid parameters, auth, balance, rate limits) must be +// surfaced with their real meaning rather than a blanket 502. +func speechStatusError(statusCode int, statusMessage string) error { + message := fmt.Sprintf("minimax speech request failed (status %d)", statusCode) + if statusMessage = strings.TrimSpace(statusMessage); statusMessage != "" { + message += ": " + statusMessage + } + switch statusCode { + case 1002, 1039: // rate limit / token-per-minute limit triggered + return core.NewRateLimitError("minimax", message) + case 1004: // authentication failed + return core.NewAuthenticationError("minimax", message) + case 1008: // insufficient balance + return core.NewProviderError("minimax", http.StatusPaymentRequired, message, nil) + case 1026, 2013: // sensitive input content / invalid request parameters + return core.NewInvalidRequestError(message, nil) + default: + return core.NewProviderError("minimax", http.StatusBadGateway, message, nil) + } +} + func speechFormat(responseFormat string) (string, error) { format := strings.ToLower(strings.TrimSpace(responseFormat)) if format == "" { diff --git a/internal/providers/minimax/audio_test.go b/internal/providers/minimax/audio_test.go index acdf2b33..8fc707f2 100644 --- a/internal/providers/minimax/audio_test.go +++ b/internal/providers/minimax/audio_test.go @@ -3,6 +3,7 @@ package minimax import ( "bytes" "context" + "errors" "io" "net/http" "net/http/httptest" @@ -138,21 +139,55 @@ func TestCreateSpeech_ValidatesNativeConstraints(t *testing.T) { } } -func TestCreateSpeech_ReturnsNativeStatusError(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"data":null,"base_resp":{"status_code":1004,"status_msg":"invalid voice"}}`)) - })) - defer server.Close() +func TestCreateSpeech_MapsNativeStatusCodes(t *testing.T) { + tests := []struct { + name string + nativeStatus int + statusMsg string + wantHTTPStatus int + wantType core.ErrorType + }{ + {name: "rate limit", nativeStatus: 1002, statusMsg: "rate limit triggered", wantHTTPStatus: http.StatusTooManyRequests, wantType: core.ErrorTypeRateLimit}, + {name: "tpm limit", nativeStatus: 1039, statusMsg: "token limit", wantHTTPStatus: http.StatusTooManyRequests, wantType: core.ErrorTypeRateLimit}, + {name: "auth failed", nativeStatus: 1004, statusMsg: "authentication failed", wantHTTPStatus: http.StatusUnauthorized, wantType: core.ErrorTypeAuthentication}, + {name: "insufficient balance", nativeStatus: 1008, statusMsg: "insufficient balance", wantHTTPStatus: http.StatusPaymentRequired, wantType: core.ErrorTypeProvider}, + {name: "sensitive input", nativeStatus: 1026, statusMsg: "sensitive content", wantHTTPStatus: http.StatusBadRequest, wantType: core.ErrorTypeInvalidRequest}, + {name: "invalid params", nativeStatus: 2013, statusMsg: "invalid voice_id", wantHTTPStatus: http.StatusBadRequest, wantType: core.ErrorTypeInvalidRequest}, + {name: "unknown code", nativeStatus: 1000, statusMsg: "unknown error", wantHTTPStatus: http.StatusBadGateway, wantType: core.ErrorTypeProvider}, + } - provider := NewWithHTTPClient("key", server.URL, server.Client(), llmclient.Hooks{}) - _, err := provider.CreateSpeech(context.Background(), &core.AudioSpeechRequest{ - Model: "speech-2.8-hd", - Input: "hello", - Voice: "voice-id", - }) - if err == nil || !strings.Contains(err.Error(), "invalid voice") { - t.Fatalf("CreateSpeech() error = %v, want native status message", err) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + body, _ := json.Marshal(map[string]any{ + "data": nil, + "base_resp": map[string]any{"status_code": tt.nativeStatus, "status_msg": tt.statusMsg}, + }) + _, _ = w.Write(body) + })) + defer server.Close() + + provider := NewWithHTTPClient("key", server.URL, server.Client(), llmclient.Hooks{}) + _, err := provider.CreateSpeech(context.Background(), &core.AudioSpeechRequest{ + Model: "speech-2.8-hd", + Input: "hello", + Voice: "voice-id", + }) + var gatewayErr *core.GatewayError + if !errors.As(err, &gatewayErr) { + t.Fatalf("CreateSpeech() error = %v, want *core.GatewayError", err) + } + if gatewayErr.StatusCode != tt.wantHTTPStatus { + t.Fatalf("status = %d, want %d", gatewayErr.StatusCode, tt.wantHTTPStatus) + } + if gatewayErr.Type != tt.wantType { + t.Fatalf("type = %q, want %q", gatewayErr.Type, tt.wantType) + } + if !strings.Contains(gatewayErr.Message, tt.statusMsg) { + t.Fatalf("message = %q, want substring %q", gatewayErr.Message, tt.statusMsg) + } + }) } } From e4dc0a9576469f6c67ea8860545e09c8c29b953e Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Mon, 3 Aug 2026 15:32:11 +0200 Subject: [PATCH 2/4] docs(minimax): document native TTS and add provider guide Add a MiniMax provider page covering setup, temperature clamping, the /v1/audio/speech translation to t2a_v2 (voice IDs, formats, speed range, MINIMAX_MODELS routability), error mapping, and unsupported features. Correct the Audio API page: MiniMax TTS is now natively adapted, and Xiaomi/Cohere translations already were. Co-Authored-By: Claude Fable 5 --- docs/advanced/audio-api.mdx | 14 +++--- docs/docs.json | 1 + docs/providers/minimax.mdx | 85 +++++++++++++++++++++++++++++++++++++ docs/providers/overview.mdx | 2 +- 4 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 docs/providers/minimax.mdx diff --git a/docs/advanced/audio-api.mdx b/docs/advanced/audio-api.mdx index 44f1d410..654b6f91 100644 --- a/docs/advanced/audio-api.mdx +++ b/docs/advanced/audio-api.mdx @@ -15,8 +15,11 @@ Requests route **by model** through the same registry used for chat and embeddings, so `model` selection, `provider` hints, virtual models, per-key model access rules ([user paths](/features/user-path)), and budgets all apply. Audio is served by OpenAI and the OpenAI-compatible providers (OpenRouter, Azure OpenAI, -vLLM, Oracle, MiniMax, Z.ai); a provider that doesn't support audio returns a -clear error rather than mis-routing. +vLLM, Oracle, Z.ai), plus providers whose **native audio APIs GoModel translates** +behind the same endpoints: [Xiaomi MiMo](/providers/xiaomi) (TTS and ASR via chat +completions), [Cohere](/providers/cohere) (transcription), and +[MiniMax](/providers/minimax) (TTS via its native `t2a_v2` API). A provider that +doesn't support audio returns a clear error rather than mis-routing. ## Supported endpoints @@ -74,9 +77,10 @@ through the full inference orchestrator**. Compared with `/v1/chat/completions`: - **No usage/cost metering** — audio is not token-priced, so it is not recorded in usage tracking. Requests are still authorized, budget-checked, and written to the [audit log](/advanced/admin-endpoints) under their `/v1/audio/*` path. -- **OpenAI request shape only** — requests are forwarded in OpenAI's audio format to - OpenAI-compatible upstreams. Providers with a different native audio contract are - not yet adapted behind this endpoint. +- **OpenAI request shape in, provider dialect out** — clients always send OpenAI's + audio format. OpenAI-compatible upstreams receive it unchanged; Xiaomi MiMo, + Cohere, and MiniMax requests are translated to each provider's native audio + contract. Providers beyond those are not adapted behind this endpoint. - **Realtime voice-to-voice** (the WebSocket realtime API) is not supported. For a provider whose native audio API differs from OpenAI's, use the diff --git a/docs/docs.json b/docs/docs.json index 92ae7a98..559e6252 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -166,6 +166,7 @@ "providers/xai", "providers/bailian", "providers/xiaomi", + "providers/minimax", "providers/opencode-go", "providers/vllm", "providers/multiple-ollama", diff --git a/docs/providers/minimax.mdx b/docs/providers/minimax.mdx new file mode 100644 index 00000000..4bc5becf --- /dev/null +++ b/docs/providers/minimax.mdx @@ -0,0 +1,85 @@ +--- +title: "MiniMax" +description: "Configure MiniMax in GoModel: chat models, temperature handling, and native text-to-speech through the standard audio endpoint." +icon: "waveform" +keywords: ["MiniMax", "text-to-speech", "TTS", "t2a_v2", "provider setup"] +--- + +MiniMax speaks an OpenAI-compatible chat API, so chat models work out of the +box. Text-to-speech, however, uses MiniMax's own `t2a_v2` API — GoModel +translates the standard `/v1/audio/speech` endpoint into that dialect for you. + +## Configure + +```bash +MINIMAX_API_KEY=... +``` + +Or in `config.yaml`: + +```yaml +providers: + minimax: + type: minimax + base_url: "https://api.minimax.io/v1" + api_key: "${MINIMAX_API_KEY}" +``` + +`MINIMAX_BASE_URL` overrides the endpoint (default +`https://api.minimax.io/v1`); accounts on the China platform should set it to +`https://api.minimaxi.com/v1`. + +## Temperature + +MiniMax requires `temperature` in `(0.0, 1.0]` and rejects zero. GoModel clamps +a zero or negative temperature to `1.0` so OpenAI-style requests that pin +`temperature: 0` keep working. + +## Text-to-speech + +`POST /v1/audio/speech` is translated to MiniMax's synchronous +[`t2a_v2`](https://platform.minimax.io/docs/api-reference/speech-t2a-v2) +API and the hex-encoded audio is decoded back to binary: + +```bash +curl https://your-gateway/v1/audio/speech \ + -H "Authorization: Bearer $GOMODEL_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "speech-2.6-hd", + "input": "Hello from GoModel.", + "voice": "English_expressive_narrator", + "response_format": "mp3" + }' \ + --output speech.mp3 +``` + +- `voice` takes a **MiniMax voice ID** (for example + `English_expressive_narrator`), not an OpenAI voice name like `alloy`. +- `response_format` supports `mp3` (default), `wav`, `flac`, and `pcm`. +- `speed` supports `0.5`–`2.0` (default `1.0`). + +Speech models are usually not returned by MiniMax's `/models` listing, so add +them to the configured model list to make them routable: + +```bash +MINIMAX_MODELS=speech-2.6-hd,speech-2.6-turbo +``` + +MiniMax reports failures as HTTP 200 with a native status code; GoModel maps +the common ones to real errors (invalid parameters and blocked content → 400, +authentication → 401, insufficient balance → 402, rate limits → 429) instead of +relaying them as opaque gateway errors. + +## Not supported by MiniMax + +All of these return `invalid_request_error` rather than silently dropping the +option: + +- Speech `instructions` (pick a voice ID that matches the style you want). +- Speech `response_format` values other than `mp3`/`wav`/`flac`/`pcm` and + `speed` outside `0.5`–`2.0`. +- Speech-to-text — MiniMax has no transcription API, so + `/v1/audio/transcriptions` is rejected. +- Realtime voice-to-voice — MiniMax's conversational realtime schema is not + OpenAI-compatible, so it is not exposed at `/v1/realtime`. diff --git a/docs/providers/overview.mdx b/docs/providers/overview.mdx index ce0ac119..f1700113 100644 --- a/docs/providers/overview.mdx +++ b/docs/providers/overview.mdx @@ -54,7 +54,7 @@ support, not every individual model capability exposed by an upstream provider. | Z.ai | `ZAI_API_KEY` (`ZAI_BASE_URL` optional) | `glm-5.1` | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | — | | xAI (Grok) | `XAI_API_KEY` | `grok-4.5` | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | [xAI (Grok)](/providers/xai) | | Alibaba Cloud Model Studio (Bailian) | `BAILIAN_API_KEY` (`BAILIAN_BASE_URL` optional) | `qwen3-max` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | [Alibaba Cloud Model Studio](/providers/bailian) | -| MiniMax | `MINIMAX_API_KEY` (`MINIMAX_BASE_URL` optional) | `MiniMax-M3` | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | — | +| MiniMax | `MINIMAX_API_KEY` (`MINIMAX_BASE_URL` optional) | `MiniMax-M3` | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | [MiniMax](/providers/minimax) | | Xiaomi MiMo | `XIAOMI_API_KEY` (`XIAOMI_BASE_URL` optional) | `mimo-v2.5-pro` | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | [Xiaomi MiMo](/providers/xiaomi) | | OpenCode Go | `OPENCODE_GO_API_KEY` (`OPENCODE_GO_BASE_URL` optional) | `glm-5.1` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | [OpenCode Go](/providers/opencode-go) | | Kimi Code | `KIMICODE_API_KEY` | `kimi-for-coding` | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | [Kimi Code](/providers/kimicode) | From b051dd7a41a19440ce81e8e4c922ce11f659ed38 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Mon, 3 Aug 2026 16:28:16 +0200 Subject: [PATCH 3/4] =?UTF-8?q?fix(minimax):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20map=202049/20132=20and=20keep=20provider=20attribut?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handle more codes from MiniMax's official error table: invalid API key (2049) joins 1004 as 401, invalid voice_id/samples (20132) and invisible-character input (1042) join the 400 branch, and rate growth limit (2045) plus usage limit (2056) join the 429 branch. Set Provider on the invalid-request errors so structured logs keep MiniMax attribution, and assert provider plus the native status code in the mapping test. Co-Authored-By: Claude Fable 5 --- internal/providers/minimax/audio.go | 10 ++++++---- internal/providers/minimax/audio_test.go | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/internal/providers/minimax/audio.go b/internal/providers/minimax/audio.go index 7120af05..b842f018 100644 --- a/internal/providers/minimax/audio.go +++ b/internal/providers/minimax/audio.go @@ -147,14 +147,16 @@ func speechStatusError(statusCode int, statusMessage string) error { message += ": " + statusMessage } switch statusCode { - case 1002, 1039: // rate limit / token-per-minute limit triggered + case 1002, 1039, 2045, 2056: // rate limit / token limit / rate growth limit / usage limit return core.NewRateLimitError("minimax", message) - case 1004: // authentication failed + case 1004, 2049: // not authorized / invalid API key return core.NewAuthenticationError("minimax", message) case 1008: // insufficient balance return core.NewProviderError("minimax", http.StatusPaymentRequired, message, nil) - case 1026, 2013: // sensitive input content / invalid request parameters - return core.NewInvalidRequestError(message, nil) + case 1026, 1042, 2013, 20132: // sensitive input / invisible characters / invalid params / invalid voice_id + gatewayErr := core.NewInvalidRequestError(message, nil) + gatewayErr.Provider = "minimax" + return gatewayErr default: return core.NewProviderError("minimax", http.StatusBadGateway, message, nil) } diff --git a/internal/providers/minimax/audio_test.go b/internal/providers/minimax/audio_test.go index 8fc707f2..30f56b31 100644 --- a/internal/providers/minimax/audio_test.go +++ b/internal/providers/minimax/audio_test.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "net/http/httptest" + "strconv" "strings" "testing" @@ -149,10 +150,13 @@ func TestCreateSpeech_MapsNativeStatusCodes(t *testing.T) { }{ {name: "rate limit", nativeStatus: 1002, statusMsg: "rate limit triggered", wantHTTPStatus: http.StatusTooManyRequests, wantType: core.ErrorTypeRateLimit}, {name: "tpm limit", nativeStatus: 1039, statusMsg: "token limit", wantHTTPStatus: http.StatusTooManyRequests, wantType: core.ErrorTypeRateLimit}, - {name: "auth failed", nativeStatus: 1004, statusMsg: "authentication failed", wantHTTPStatus: http.StatusUnauthorized, wantType: core.ErrorTypeAuthentication}, + {name: "usage limit", nativeStatus: 2056, statusMsg: "usage limit exceeded", wantHTTPStatus: http.StatusTooManyRequests, wantType: core.ErrorTypeRateLimit}, + {name: "auth failed", nativeStatus: 1004, statusMsg: "not authorized", wantHTTPStatus: http.StatusUnauthorized, wantType: core.ErrorTypeAuthentication}, + {name: "invalid api key", nativeStatus: 2049, statusMsg: "invalid API Key", wantHTTPStatus: http.StatusUnauthorized, wantType: core.ErrorTypeAuthentication}, {name: "insufficient balance", nativeStatus: 1008, statusMsg: "insufficient balance", wantHTTPStatus: http.StatusPaymentRequired, wantType: core.ErrorTypeProvider}, {name: "sensitive input", nativeStatus: 1026, statusMsg: "sensitive content", wantHTTPStatus: http.StatusBadRequest, wantType: core.ErrorTypeInvalidRequest}, - {name: "invalid params", nativeStatus: 2013, statusMsg: "invalid voice_id", wantHTTPStatus: http.StatusBadRequest, wantType: core.ErrorTypeInvalidRequest}, + {name: "invalid params", nativeStatus: 2013, statusMsg: "invalid params", wantHTTPStatus: http.StatusBadRequest, wantType: core.ErrorTypeInvalidRequest}, + {name: "invalid voice", nativeStatus: 20132, statusMsg: "invalid samples or voice_id", wantHTTPStatus: http.StatusBadRequest, wantType: core.ErrorTypeInvalidRequest}, {name: "unknown code", nativeStatus: 1000, statusMsg: "unknown error", wantHTTPStatus: http.StatusBadGateway, wantType: core.ErrorTypeProvider}, } @@ -187,6 +191,12 @@ func TestCreateSpeech_MapsNativeStatusCodes(t *testing.T) { if !strings.Contains(gatewayErr.Message, tt.statusMsg) { t.Fatalf("message = %q, want substring %q", gatewayErr.Message, tt.statusMsg) } + if !strings.Contains(gatewayErr.Message, strconv.Itoa(tt.nativeStatus)) { + t.Fatalf("message = %q, want native status %d", gatewayErr.Message, tt.nativeStatus) + } + if gatewayErr.Provider != "minimax" { + t.Fatalf("provider = %q, want minimax", gatewayErr.Provider) + } }) } } From 40de5ea87c643d80db25da84d3db1929720e5f2f Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Mon, 3 Aug 2026 16:39:39 +0200 Subject: [PATCH 4/4] test(minimax): cover status codes 2045 and 1042 in mapping table Co-Authored-By: Claude Fable 5 --- internal/providers/minimax/audio_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/providers/minimax/audio_test.go b/internal/providers/minimax/audio_test.go index 30f56b31..01858e02 100644 --- a/internal/providers/minimax/audio_test.go +++ b/internal/providers/minimax/audio_test.go @@ -150,11 +150,13 @@ func TestCreateSpeech_MapsNativeStatusCodes(t *testing.T) { }{ {name: "rate limit", nativeStatus: 1002, statusMsg: "rate limit triggered", wantHTTPStatus: http.StatusTooManyRequests, wantType: core.ErrorTypeRateLimit}, {name: "tpm limit", nativeStatus: 1039, statusMsg: "token limit", wantHTTPStatus: http.StatusTooManyRequests, wantType: core.ErrorTypeRateLimit}, + {name: "rate growth limit", nativeStatus: 2045, statusMsg: "rate growth limit", wantHTTPStatus: http.StatusTooManyRequests, wantType: core.ErrorTypeRateLimit}, {name: "usage limit", nativeStatus: 2056, statusMsg: "usage limit exceeded", wantHTTPStatus: http.StatusTooManyRequests, wantType: core.ErrorTypeRateLimit}, {name: "auth failed", nativeStatus: 1004, statusMsg: "not authorized", wantHTTPStatus: http.StatusUnauthorized, wantType: core.ErrorTypeAuthentication}, {name: "invalid api key", nativeStatus: 2049, statusMsg: "invalid API Key", wantHTTPStatus: http.StatusUnauthorized, wantType: core.ErrorTypeAuthentication}, {name: "insufficient balance", nativeStatus: 1008, statusMsg: "insufficient balance", wantHTTPStatus: http.StatusPaymentRequired, wantType: core.ErrorTypeProvider}, {name: "sensitive input", nativeStatus: 1026, statusMsg: "sensitive content", wantHTTPStatus: http.StatusBadRequest, wantType: core.ErrorTypeInvalidRequest}, + {name: "invisible characters", nativeStatus: 1042, statusMsg: "invisible character ratio limit", wantHTTPStatus: http.StatusBadRequest, wantType: core.ErrorTypeInvalidRequest}, {name: "invalid params", nativeStatus: 2013, statusMsg: "invalid params", wantHTTPStatus: http.StatusBadRequest, wantType: core.ErrorTypeInvalidRequest}, {name: "invalid voice", nativeStatus: 20132, statusMsg: "invalid samples or voice_id", wantHTTPStatus: http.StatusBadRequest, wantType: core.ErrorTypeInvalidRequest}, {name: "unknown code", nativeStatus: 1000, statusMsg: "unknown error", wantHTTPStatus: http.StatusBadGateway, wantType: core.ErrorTypeProvider},