feat(ai): OpenAI-compatible adapter for MiniMax

This commit is contained in:
Dave 2026-09-16 15:53:34 +02:00
parent b172079912
commit b233259f18
3 changed files with 121 additions and 0 deletions

38
package-lock.json generated
View file

@ -16,6 +16,7 @@
"clsx": "^2.1.1",
"lucide-react": "^1.46.0",
"next": "16.2.6",
"openai": "^7.15.0",
"pocketbase": "^0.26.9",
"react": "19.2.4",
"react-dom": "19.2.4",
@ -3342,6 +3343,43 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/openai": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-7.15.0.tgz",
"integrity": "sha512-2DIwesnPSduw68MFx7nAdgvshrgvS8WOan5W8+tgUMlAy/tY+Q41g80Pa+w/v64rHA6rtn+dketa8QvMVxU96w==",
"license": "Apache-2.0",
"engines": {
"node": ">=22.0.0"
},
"peerDependencies": {
"@aws-sdk/credential-provider-node": ">=3.972.0 <4",
"@smithy/hash-node": ">=4.3.0 <5",
"@smithy/signature-v4": ">=5.4.0 <6",
"undici": ">=5 <9",
"ws": "^8.21.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-provider-node": {
"optional": true
},
"@smithy/hash-node": {
"optional": true
},
"@smithy/signature-v4": {
"optional": true
},
"undici": {
"optional": true
},
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",

View file

@ -22,6 +22,7 @@
"clsx": "^2.1.1",
"lucide-react": "^1.46.0",
"next": "16.2.6",
"openai": "^7.15.0",
"pocketbase": "^0.26.9",
"react": "19.2.4",
"react-dom": "19.2.4",

View file

@ -0,0 +1,82 @@
import "server-only";
import OpenAI from "openai";
import { stripThinking } from "@/lib/ai/think-strip";
import { AiError, type AiProvider, type CompletionRequest, type CompletionResult, type ProviderName } from "@/lib/ai/types";
/**
* OpenAI-compatible adapter — this is the MiniMax path.
*
* MiniMax specifics, taken from comments in MEGAIFRASTRUCTURE's equivalent
* adapter. They are the product of it going wrong, not of reading docs:
*
* - Use the NATIVE OpenAI-compatible route, https://api.minimax.io/v1. Their
* /anthropic shim does not reliably re-parse stringified arguments.
* - The key must be an Open Platform key, NOT a Token Plan Subscription Key.
* They are not interchangeable and the error does not say which you have.
* - `n` must be 1; presence_penalty and frequency_penalty are ignored.
* - M2.x emits its chain of thought inline in `content`, wrapped in <think>
* tags — hence stripThinking below. Without it every client-facing summary
* opens with the model deliberating.
*/
const MINIMAX_BASE_URL = "https://api.minimax.io/v1";
export function createOpenAiCompatibleProvider(
apiKey: string,
model: string,
provider: ProviderName
): AiProvider {
const client = new OpenAI({
apiKey,
...(provider === "minimax"
? { baseURL: process.env.MINIMAX_BASE_URL || MINIMAX_BASE_URL }
: {}),
});
return {
name: provider,
async complete(request: CompletionRequest): Promise<CompletionResult> {
let response: OpenAI.Chat.Completions.ChatCompletion;
try {
response = await client.chat.completions.create({
model,
max_tokens: Math.max(request.maxTokens, 4096),
n: 1,
messages: [
{ role: "system", content: request.system },
{ role: "user", content: request.user },
],
});
} catch (error) {
if (error instanceof OpenAI.APIError) {
const retryable = error.status === 429 || (error.status ?? 0) >= 500;
throw new AiError(`${provider} error ${error.status}: ${error.message}`, retryable);
}
throw new AiError(
error instanceof Error ? error.message : `${provider} request failed`,
true
);
}
const choice = response.choices[0];
if (choice?.finish_reason === "length") {
throw new AiError("The response was cut off by max_tokens", true);
}
// stripThinking is not optional on MiniMax — see the header.
const text = stripThinking(choice?.message?.content ?? "");
return {
text,
inputTokens: response.usage?.prompt_tokens ?? 0,
outputTokens: response.usage?.completion_tokens ?? 0,
model: response.model,
};
},
};
}