feat(ai): OpenAI-compatible adapter for MiniMax
This commit is contained in:
parent
b172079912
commit
b233259f18
3 changed files with 121 additions and 0 deletions
38
package-lock.json
generated
38
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
82
src/lib/ai/provider-openai.ts
Normal file
82
src/lib/ai/provider-openai.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue