Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ Manual `memoryProvider` modes:
- `openai-chat`: OpenAI Chat Completions compatible API with tool/function calling. This can work with compatible proxies such as LiteLLM only when the selected upstream model and proxy preserve tool calls.
- `openai-responses`: OpenAI Responses API with function-call output.
- `anthropic`: Anthropic Messages API with tool use.
- `minimax`: MiniMax Anthropic Messages-compatible endpoint. Set `memoryApiUrl` to the global endpoint (`https://api.minimax.io`) or the China endpoint (`https://api.minimaxi.com`); the `/anthropic/v1/messages` path and `x-api-key` header are applied automatically. MiniMax text models such as `MiniMax-M3` support the adaptive thinking modes used by this plugin via `memoryExtraParams`.

Troubleshooting:

Expand Down
21 changes: 15 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ interface OpenCodeMemConfig {
autoCaptureIterationTimeout?: number;
autoCaptureMaxRetries?: number;
autoCaptureLanguage?: string;
memoryProvider?: "openai-chat" | "openai-responses" | "anthropic";
memoryProvider?: "openai-chat" | "openai-responses" | "anthropic" | "minimax";
memoryModel?: string;
memoryApiUrl?: string;
memoryApiKey?: string;
Expand Down Expand Up @@ -124,7 +124,7 @@ const DEFAULTS: Required<
memoryModel?: string;
memoryApiUrl?: string;
memoryApiKey?: string;
memoryProvider?: "openai-chat" | "openai-responses" | "anthropic";
memoryProvider?: "openai-chat" | "openai-responses" | "anthropic" | "minimax";
memoryTemperature?: number | false;
memoryExtraParams?: Record<string, unknown>;
opencodeProvider?: string;
Expand Down Expand Up @@ -343,7 +343,7 @@ const CONFIG_TEMPLATE = `{

"autoCaptureEnabled": true,

// Provider type: "openai-chat" | "openai-responses" | "anthropic"
// Provider type: "openai-chat" | "openai-responses" | "anthropic" | "minimax"
// Note: "openai-chat" is a generic OpenAI API-compatible mode.
// Any service that follows the OpenAI Chat Completions API can use it via custom "memoryApiUrl".
"memoryProvider": "openai-chat",
Expand Down Expand Up @@ -386,14 +386,23 @@ const CONFIG_TEMPLATE = `{
// "memoryModel": "claude-3-5-haiku-20241022"
// "memoryApiUrl": "https://api.anthropic.com/v1"
// "memoryApiKey": "sk-ant-..."


// MiniMax (Anthropic Messages-compatible endpoint, with session support):
// "memoryProvider": "minimax"
// "memoryModel": "MiniMax-M3"
// "memoryApiUrl": "https://api.minimax.io" // global endpoint
// "memoryApiKey": "<MiniMax API key>"
// // China endpoint: "memoryApiUrl": "https://api.minimaxi.com"
// // Optional adaptive thinking for MiniMax-M3:
// "memoryExtraParams": { "thinking": { "type": "adaptive" } }

// Groq (OpenAI-compatible, use openai-chat provider):
// "memoryProvider": "openai-chat"
// "memoryModel": "llama-3.3-70b-versatile"
// "memoryApiUrl": "https://api.groq.com/openai/v1"
// "memoryApiKey": "gsk_..."

// Maximum iterations for multi-turn AI analysis (for openai-responses and anthropic)
// Maximum iterations for multi-turn AI analysis (for openai-responses, anthropic, and minimax)
"autoCaptureMaxIterations": 5,

// Timeout per iteration in milliseconds (30 seconds default)
Expand Down Expand Up @@ -598,7 +607,7 @@ function buildConfig(fileConfig: OpenCodeMemConfig) {
autoCaptureMaxRetries: fileConfig.autoCaptureMaxRetries ?? DEFAULTS.autoCaptureMaxRetries,
autoCaptureLanguage: fileConfig.autoCaptureLanguage,
memoryProvider: (fileConfig.memoryProvider ?? "openai-chat") as
"openai-chat" | "openai-responses" | "anthropic",
"openai-chat" | "openai-responses" | "anthropic" | "minimax",
memoryModel: fileConfig.memoryModel,
memoryApiUrl: fileConfig.memoryApiUrl,
memoryApiKey,
Expand Down
6 changes: 5 additions & 1 deletion src/services/ai/ai-provider-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BaseAIProvider, type ProviderConfig } from "./providers/base-provider.j
import { OpenAIChatCompletionProvider } from "./providers/openai-chat-completion.js";
import { OpenAIResponsesProvider } from "./providers/openai-responses.js";
import { AnthropicMessagesProvider } from "./providers/anthropic-messages.js";
import { MiniMaxProvider } from "./providers/minimax.js";
import { GoogleGeminiProvider } from "./providers/google-gemini.js";
import { aiSessionManager } from "./session/ai-session-manager.js";
import type { AIProviderType } from "./session/session-types.js";
Expand All @@ -18,6 +19,9 @@ export class AIProviderFactory {
case "anthropic":
return new AnthropicMessagesProvider(config, aiSessionManager);

case "minimax":
return new MiniMaxProvider(config, aiSessionManager);

case "google-gemini":
return new GoogleGeminiProvider(config, aiSessionManager);

Expand All @@ -27,7 +31,7 @@ export class AIProviderFactory {
}

static getSupportedProviders(): AIProviderType[] {
return ["openai-chat", "openai-responses", "anthropic", "google-gemini"];
return ["openai-chat", "openai-responses", "anthropic", "minimax", "google-gemini"];
}

static async cleanupExpiredSessions(): Promise<number> {
Expand Down
59 changes: 51 additions & 8 deletions src/services/ai/providers/anthropic-messages.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BaseAIProvider, type ToolCallResult } from "./base-provider.js";
import { applySafeExtraParams, BaseAIProvider, type ToolCallResult } from "./base-provider.js";
import { AISessionManager } from "../session/ai-session-manager.js";
import { ToolSchemaConverter, type ChatCompletionTool } from "../tools/tool-schema.js";
import type { AIProviderType } from "../session/session-types.js";
import { log } from "../../logger.js";
import { UserProfileValidator } from "../validators/user-profile-validator.js";

Expand Down Expand Up @@ -28,6 +29,14 @@ interface AnthropicResponse {
};
}

/**
* Base implementation for providers that speak the Anthropic Messages API
* (Anthropic itself plus Anthropic-compatible gateways such as MiniMax).
*
* Subclasses override the session provider tag and the resolved endpoint URL so
* the session store and request routing reflect the upstream provider while the
* message/tool-call handling stays shared.
*/
export class AnthropicMessagesProvider extends BaseAIProvider {
private aiSessionManager: AISessionManager;

Expand All @@ -44,17 +53,47 @@ export class AnthropicMessagesProvider extends BaseAIProvider {
return true;
}

/**
* Provider tag stored on AI sessions so a subclass (e.g. MiniMax) records its
* own tag instead of the literal "anthropic" value.
*/
protected sessionProviderTag(): AIProviderType {
return "anthropic";
}

/**
* Resolve the Messages endpoint URL. The default appends `/messages` to the
* configured base URL, matching Anthropic's `https://api.anthropic.com/v1`
* base. Subclasses with a different path layout override this.
*/
protected resolveEndpoint(): string {
return `${this.config.apiUrl}/messages`;
}

protected apiErrorLogLabel(): string {
return "Anthropic Messages API error";
}

protected toolValidationErrorLogLabel(): string {
return "Anthropic tool response validation failed";
}

protected timeoutLabel(): string {
return "Anthropic API request timeout";
}

async executeToolCall(
systemPrompt: string,
userPrompt: string,
toolSchema: ChatCompletionTool,
sessionId: string
): Promise<ToolCallResult> {
let session = await this.aiSessionManager.getSession(sessionId, "anthropic");
const providerTag = this.sessionProviderTag();
let session = await this.aiSessionManager.getSession(sessionId, providerTag);

if (!session) {
session = await this.aiSessionManager.createSession({
provider: "anthropic",
provider: providerTag,
sessionId,
metadata: { systemPrompt },
});
Expand Down Expand Up @@ -97,14 +136,18 @@ export class AnthropicMessagesProvider extends BaseAIProvider {
try {
const tool = ToolSchemaConverter.toAnthropic(toolSchema);

const requestBody = {
const requestBody: Record<string, unknown> = {
model: this.config.model,
max_tokens: this.config.maxTokens ?? 4096,
system: systemPrompt,
messages,
tools: [tool],
};

if (this.config.extraParams) {
applySafeExtraParams(requestBody, this.config.extraParams);
}

const headers: Record<string, string> = {
"Content-Type": "application/json",
"anthropic-version": "2023-06-01",
Expand All @@ -114,7 +157,7 @@ export class AnthropicMessagesProvider extends BaseAIProvider {
headers["x-api-key"] = this.config.apiKey;
}

const response = await fetch(`${this.config.apiUrl}/messages`, {
const response = await fetch(this.resolveEndpoint(), {
method: "POST",
headers,
body: JSON.stringify(requestBody),
Expand All @@ -125,7 +168,7 @@ export class AnthropicMessagesProvider extends BaseAIProvider {

if (!response.ok) {
const errorText = await response.text().catch(() => response.statusText);
log("Anthropic Messages API error", {
log(this.apiErrorLogLabel(), {
provider: this.getProviderName(),
model: this.config.model,
status: response.status,
Expand Down Expand Up @@ -170,7 +213,7 @@ export class AnthropicMessagesProvider extends BaseAIProvider {
};
} catch (validationError) {
const errorStack = validationError instanceof Error ? validationError.stack : undefined;
log("Anthropic tool response validation failed", {
log(this.toolValidationErrorLogLabel(), {
error: String(validationError),
stack: errorStack,
errorType:
Expand Down Expand Up @@ -210,7 +253,7 @@ export class AnthropicMessagesProvider extends BaseAIProvider {
if (error instanceof Error && error.name === "AbortError") {
return {
success: false,
error: `API request timeout (${this.config.iterationTimeout}ms)`,
error: `${this.timeoutLabel()} (${this.config.iterationTimeout}ms)`,
iterations,
};
}
Expand Down
63 changes: 63 additions & 0 deletions src/services/ai/providers/minimax.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { type ProviderConfig } from "./base-provider.js";
import { AISessionManager } from "../session/ai-session-manager.js";
import { AnthropicMessagesProvider } from "./anthropic-messages.js";
import type { AIProviderType } from "../session/session-types.js";

/**
* MiniMax provider.
*
* MiniMax exposes an Anthropic Messages-compatible endpoint for its text
* models. The global endpoint (https://api.minimax.io) and the China endpoint
* (https://api.minimaxi.com) both expose the same `/anthropic/v1/messages`
* path and authenticate with the `x-api-key` header. This provider reuses the
* Anthropic Messages request/response handling and only overrides the resolved
* request endpoint URL and the session provider tag, so MiniMax is
* distinguishable from Anthropic in the session store and diagnostics.
*
* Users configure the base URL as `memoryApiUrl`:
* - global endpoint: "https://api.minimax.io"
* - China endpoint: "https://api.minimaxi.com"
*/
export class MiniMaxProvider extends AnthropicMessagesProvider {
constructor(config: ProviderConfig, aiSessionManager: AISessionManager) {
super(config, aiSessionManager);
}

override getProviderName(): string {
return "minimax";
}

/**
* Resolve the Anthropic Messages endpoint URL for MiniMax.
*
* MiniMax's Messages endpoint lives at `<base>/anthropic/v1/messages`. The
* base URL is normalized so users can supply the host with or without a
* trailing `/anthropic`, `/anthropic/v1`, or `/anthropic/v1/messages` suffix.
*/
override resolveEndpoint(): string {
let base = (this.config.apiUrl || "").trim().replace(/\/+$/, "");
if (!base) {
throw new Error("MiniMax provider requires a configured memoryApiUrl");
}
base = base.replace(/\/anthropic\/v1\/messages\/?$/, "");
base = base.replace(/\/anthropic\/v1\/?$/, "");
base = base.replace(/\/anthropic\/?$/, "");
return `${base}/anthropic/v1/messages`;
}

override sessionProviderTag(): AIProviderType {
return "minimax";
}

protected override apiErrorLogLabel(): string {
return "MiniMax Messages API error";
}

protected override toolValidationErrorLogLabel(): string {
return "MiniMax tool response validation failed";
}

protected override timeoutLabel(): string {
return "MiniMax API request timeout";
}
}
3 changes: 2 additions & 1 deletion src/services/ai/session/session-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type AIProviderType = "openai-chat" | "openai-responses" | "anthropic" | "google-gemini";
export type AIProviderType =
"openai-chat" | "openai-responses" | "anthropic" | "minimax" | "google-gemini";

export interface AIMessage {
id?: number;
Expand Down
3 changes: 2 additions & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export interface MemoryMetadata {
[key: string]: unknown;
}

export type AIProviderType = "openai-chat" | "openai-responses" | "anthropic";
export type AIProviderType =
"openai-chat" | "openai-responses" | "anthropic" | "minimax" | "google-gemini";
Loading