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
21 changes: 19 additions & 2 deletions electron/main/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1167,8 +1167,8 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
: 'openai-completions';

let baseUrl = provider.baseUrl || defaultBaseUrl;
if ((provider.type === 'minimax-portal' || provider.type === 'minimax-portal-cn') && baseUrl && !baseUrl.endsWith('/anthropic')) {
baseUrl = baseUrl.replace(/\/$/, '') + '/anthropic';
if ((provider.type === 'minimax-portal' || provider.type === 'minimax-portal-cn') && baseUrl) {
baseUrl = baseUrl.replace(/\/v1$/, '').replace(/\/anthropic$/, '').replace(/\/$/, '') + '/anthropic';
}

// To ensure the OpenClaw Gateway's internal token refresher works,
Expand All @@ -1180,10 +1180,27 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
setOpenClawDefaultModelWithOverride(targetProviderKey, undefined, {
baseUrl,
api,
authHeader: targetProviderKey === 'minimax-portal' ? true : undefined,
// Relies on OpenClaw Gateway native auth-profiles syncing
apiKeyEnv: targetProviderKey === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
});

logger.info(`Configured openclaw.json for OAuth provider "${provider.type}"`);

// Also write models.json directly so pi-ai picks up the correct baseUrl and
// authHeader immediately, without waiting for Gateway to sync openclaw.json.
try {
const defaultModelId = provider.model?.split('/').pop();
updateAgentModelProvider(targetProviderKey, {
baseUrl,
api,
authHeader: targetProviderKey === 'minimax-portal' ? true : undefined,
apiKey: targetProviderKey === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
models: defaultModelId ? [{ id: defaultModelId, name: defaultModelId }] : [],
});
} catch (err) {
logger.warn(`Failed to update models.json for OAuth provider "${targetProviderKey}":`, err);
}
}

// For custom/ollama providers, also update the per-agent models.json
Expand Down
15 changes: 6 additions & 9 deletions electron/utils/device-oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class DeviceOAuthManager extends EventEmitter {
expires: token.expires,
// MiniMax returns a per-account resourceUrl as the API base URL
resourceUrl: token.resourceUrl,
// MiniMax uses Anthropic Messages API format
// Revert back to anthropic-messages
api: 'anthropic-messages',
region,
});
Expand Down Expand Up @@ -217,29 +217,26 @@ class DeviceOAuthManager extends EventEmitter {
// This mirrors what the OpenClaw plugin's configPatch does after CLI login.
// The baseUrl comes from token.resourceUrl (per-account URL from the OAuth server)
// or falls back to the provider's default public endpoint.
// Note: MiniMax Anthropic-compatible API requires the /anthropic suffix.
const defaultBaseUrl = providerType === 'minimax-portal'
? 'https://api.minimax.io/anthropic'
: (providerType === 'minimax-portal-cn' ? 'https://api.minimaxi.com/anthropic' : 'https://portal.qwen.ai/v1');

let baseUrl = token.resourceUrl || defaultBaseUrl;

// If MiniMax returned a resourceUrl (e.g. https://api.minimax.io) but no /anthropic suffix,
// we must append it because we use the 'anthropic-messages' API mode
if (providerType.startsWith('minimax-portal') && baseUrl && !baseUrl.endsWith('/anthropic')) {
baseUrl = baseUrl.replace(/\/$/, '') + '/anthropic';
// Ensure the base URL ends with /anthropic
if (providerType.startsWith('minimax-portal') && baseUrl) {
baseUrl = baseUrl.replace(/\/v1$/, '').replace(/\/anthropic$/, '').replace(/\/$/, '') + '/anthropic';
}

try {
const tokenProviderId = providerType.startsWith('minimax-portal') ? 'minimax-portal' : providerType;
setOpenClawDefaultModelWithOverride(tokenProviderId, undefined, {
baseUrl,
api: token.api,
// Tells OpenClaw's anthropic adapter to use `Authorization: Bearer` instead of `x-api-key`
authHeader: providerType.startsWith('minimax-portal') ? true : undefined,
// OAuth placeholder — tells Gateway to resolve credentials
// from auth-profiles.json (type: 'oauth') instead of a static API key.
// This matches what the OpenClaw plugin's configPatch writes:
// minimax-portal → apiKey: 'minimax-oauth'
// qwen-portal → apiKey: 'qwen-oauth'
apiKeyEnv: tokenProviderId === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
});
} catch (err) {
Expand Down
53 changes: 52 additions & 1 deletion electron/utils/openclaw-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,32 @@ export function saveOAuthTokenToOpenClaw(
console.log(`Saved OAuth token for provider "${provider}" to OpenClaw auth-profiles (agents: ${agentIds.join(', ')})`);
}

/**
* Retrieve an OAuth token from OpenClaw's auth-profiles.json.
* Useful when the Gateway does not natively inject the Authorization header.
*
* @param provider - Provider type (e.g., 'minimax-portal')
* @param agentId - Optional single agent ID to read from, defaults to 'main'
* @returns The OAuth token access string or null if not found
*/
export function getOAuthTokenFromOpenClaw(
provider: string,
agentId = 'main'
): string | null {
try {
const store = readAuthProfiles(agentId);
const profileId = `${provider}:default`;
const profile = store.profiles[profileId];

if (profile && profile.type === 'oauth' && 'access' in profile) {
return (profile as OAuthProfileEntry).access;
}
} catch (err) {
console.warn(`[getOAuthToken] Failed to read token for ${provider}:`, err);
}
return null;
}

/**
* Save a provider API key to OpenClaw's auth-profiles.json
* This writes the key in the format OpenClaw expects so the gateway
Expand Down Expand Up @@ -278,7 +304,25 @@ export function removeProviderFromOpenClaw(provider: string): void {
}
}

// 2. Remove from openclaw.json
// 2. Remove from models.json (per-agent model registry used by pi-ai directly)
for (const agentId of agentIds) {
const modelsPath = join(homedir(), '.openclaw', 'agents', agentId, 'agent', 'models.json');
try {
if (existsSync(modelsPath)) {
const data = JSON.parse(readFileSync(modelsPath, 'utf-8')) as Record<string, unknown>;
const providers = data.providers as Record<string, unknown> | undefined;
if (providers && providers[provider]) {
delete providers[provider];
writeFileSync(modelsPath, JSON.stringify(data, null, 2), 'utf-8');
console.log(`Removed models.json entry for provider "${provider}" (agent "${agentId}")`);
}
}
} catch (err) {
console.warn(`Failed to remove provider ${provider} from models.json (agent "${agentId}"):`, err);
}
}

// 3. Remove from openclaw.json
const configPath = join(homedir(), '.openclaw', 'openclaw.json');
try {
if (existsSync(configPath)) {
Expand Down Expand Up @@ -447,6 +491,7 @@ interface RuntimeProviderConfigOverride {
api?: string;
apiKeyEnv?: string;
headers?: Record<string, string>;
authHeader?: boolean;
}

/**
Expand Down Expand Up @@ -573,6 +618,9 @@ export function setOpenClawDefaultModelWithOverride(
if (override.headers && Object.keys(override.headers).length > 0) {
nextProvider.headers = override.headers;
}
if (override.authHeader !== undefined) {
nextProvider.authHeader = override.authHeader;
}

providers[provider] = nextProvider;
models.providers = providers;
Expand Down Expand Up @@ -766,6 +814,8 @@ export function updateAgentModelProvider(
api?: string;
models?: Array<{ id: string; name: string }>;
apiKey?: string;
/** When true, pi-ai sends Authorization: Bearer instead of x-api-key */
authHeader?: boolean;
}
): void {
const agentIds = discoverAgentIds();
Expand Down Expand Up @@ -804,6 +854,7 @@ export function updateAgentModelProvider(
if (entry.api !== undefined) existing.api = entry.api;
if (mergedModels.length > 0) existing.models = mergedModels;
if (entry.apiKey !== undefined) existing.apiKey = entry.apiKey;
if (entry.authHeader !== undefined) existing.authHeader = entry.authHeader;

providers[providerType] = existing;
data.providers = providers;
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"i18next": "^25.8.11",
"jsdom": "^28.1.0",
"lucide-react": "^0.563.0",
"openclaw": "2026.2.24",
"openclaw": "2026.2.26",
"png2icons": "^2.0.1",
"postcss": "^8.5.6",
"react": "^19.2.4",
Expand All @@ -107,4 +107,4 @@
"zx": "^8.8.5"
},
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8"
}
}
Loading