Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ const GOOGLE_PRESETS: Preset[] = [
{ key: 'google', label: 'Google AI Studio', url: '' },
]

/** Presets that require the Pi SDK for authentication — hidden in Anthropic API Key mode */
const PI_ONLY_PRESET_KEYS: ReadonlySet<string> = new Set(['minimax-global', 'minimax-cn'])

const COMPAT_ANTHROPIC_DEFAULTS = 'anthropic/claude-opus-4.6, anthropic/claude-sonnet-4.6, anthropic/claude-haiku-4.5'
const COMPAT_OPENAI_DEFAULTS = 'openai/gpt-5.2-codex, openai/gpt-5.1-codex-mini'
const COMPAT_MINIMAX_DEFAULTS = 'MiniMax-M2.5, MiniMax-M2.5-highspeed'
Expand All @@ -130,7 +133,9 @@ function getPresetsForProvider(providerType: 'anthropic' | 'openai' | 'pi' | 'go
if (providerType === 'pi_api_key') return ANTHROPIC_PRESETS
if (providerType === 'google') return GOOGLE_PRESETS
if (providerType === 'pi') return PI_PRESETS
return providerType === 'openai' ? OPENAI_PRESETS : ANTHROPIC_PRESETS
if (providerType === 'openai') return OPENAI_PRESETS
// Anthropic mode: exclude presets that only work via Pi SDK
return ANTHROPIC_PRESETS.filter(p => !PI_ONLY_PRESET_KEYS.has(p.key))
}

function getPresetForUrl(url: string, presets: Preset[]): PresetKey {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ export type PresetKey = string

/**
* Preset keys that are regional variants of a canonical Pi auth provider.
* The Pi SDK and server-side display name maps only recognize the canonical key.
* The Pi SDK recognizes both 'minimax' and 'minimax-cn' as separate providers
* with distinct base URLs (api.minimax.io vs api.minimaxi.com), so only
* 'minimax-global' needs aliasing — 'minimax-cn' maps 1:1 to the Pi SDK provider.
*/
const PI_AUTH_PROVIDER_ALIASES: Record<string, string> = {
'minimax-global': 'minimax',
'minimax-cn': 'minimax',
}
Comment thread
Kathie-yu marked this conversation as resolved.

export function resolvePiAuthProviderForSubmit(
Expand Down
9 changes: 8 additions & 1 deletion packages/pi-agent-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,14 @@ function resolvePiModel(
// "azure-openai-responses") and the wrong one matches first.
if (piAuthProvider) {
const exact = modelRegistry.find(piAuthProvider, bareId);
if (exact) return exact;
if (exact) {
// MiniMax CN API rejects model IDs with the 'MiniMax-' prefix (e.g. 500 for
// 'MiniMax-M2.5-highspeed') but accepts bare names ('M2.5-highspeed').
if (piAuthProvider === 'minimax-cn' && exact.id.startsWith('MiniMax-')) {
return { ...exact, id: exact.id.slice('MiniMax-'.length) };
}
return exact;
}
}

// Fallback: search all available models
Expand Down
1 change: 1 addition & 0 deletions packages/server-core/src/domain/connection-setup-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ const PI_AUTH_PROVIDER_DISPLAY_NAMES: Record<string, string> = {
zai: 'z.ai',
huggingface: 'Hugging Face',
minimax: 'Minimax',
'minimax-cn': 'Minimax CN',
'kimi-coding': 'Kimi (Coding)',
'vercel-ai-gateway': 'Vercel AI Gateway',
}
Expand Down
4 changes: 3 additions & 1 deletion packages/shared/src/agent/backend/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,7 @@ export async function testBackendConnection(args: {

const { driver, resolvedPaths } = resolveDriverRuntime(args.provider, args.hostRuntime);
if (driver.testConnection) {
return driver.testConnection({
const driverResult = await driver.testConnection({
provider: args.provider,
apiKey: trimmedKey,
model: testModel,
Expand All @@ -732,6 +732,8 @@ export async function testBackendConnection(args: {
resolvedPaths,
timeoutMs: args.timeoutMs ?? 20000,
});
// null = driver declined to handle; fall through to generic subprocess test
if (driverResult !== null) return driverResult;
}

const cwd = homedir();
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/agent/backend/internal/driver-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export interface ProviderDriver {
initializeHostRuntime?: (args: DriverHostRuntimeArgs) => void;
fetchModels?: (args: DriverFetchModelsArgs) => Promise<ModelFetchResult>;
validateStoredConnection?: (args: DriverValidateStoredConnectionArgs) => Promise<StoredConnectionValidationResult>;
testConnection?: (args: DriverTestConnectionArgs) => Promise<{ success: boolean; error?: string }>;
testConnection?: (args: DriverTestConnectionArgs) => Promise<{ success: boolean; error?: string } | null>;
prepareRuntime?: (args: DriverBuildArgs) => void;
buildRuntime: (args: DriverBuildArgs) => BackendRuntimePayload;
}
Expand Down
89 changes: 88 additions & 1 deletion packages/shared/src/agent/backend/internal/drivers/pi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { existsSync } from 'node:fs';
import type { ProviderDriver } from '../driver-types.ts';
import type { ProviderDriver, DriverTestConnectionArgs } from '../driver-types.ts';
import type { ModelDefinition } from '../../../../config/models.ts';
import { getAllPiModels, getPiModelsForAuthProvider } from '../../../../config/models-pi.ts';
import { getPiProviderBaseUrl } from '../../../../config/models-pi.ts';

/**
* Fetch models dynamically from the GitHub Copilot API using @github/copilot-sdk.
Expand Down Expand Up @@ -79,6 +80,51 @@ async function fetchCopilotModels(
}));
}

/**
* Lightweight direct HTTP test for Pi providers that expose an Anthropic-compatible
* messages endpoint. Avoids spawning a full Pi subprocess (which can exceed the
* 20s test timeout due to SDK initialization overhead).
*/
async function testAnthropicCompatible(
apiKey: string,
baseUrl: string,
model: string,
timeoutMs: number,
): Promise<{ success: boolean; error?: string }> {
const url = `${baseUrl.replace(/\/$/, '')}/v1/messages`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);

try {
const res = await fetch(url, {
method: 'POST',
signal: controller.signal,
headers: {
'content-type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model,
max_tokens: 16,
messages: [{ role: 'user', content: 'Say ok' }],
}),
});

if (res.ok) return { success: true };

const text = await res.text().catch(() => '');
return { success: false, error: `${res.status} ${text}`.slice(0, 500) };
} catch (err) {
if ((err as Error).name === 'AbortError') {
return { success: false, error: 'Connection test timed out' };
}
return { success: false, error: (err as Error).message };
} finally {
clearTimeout(timer);
}
}

export const piDriver: ProviderDriver = {
provider: 'pi',
buildRuntime: ({ context, providerOptions, resolvedPaths }) => ({
Expand Down Expand Up @@ -119,5 +165,46 @@ export const piDriver: ProviderDriver = {

return { models };
},
testConnection: async (args: DriverTestConnectionArgs): Promise<{ success: boolean; error?: string } | null> => {
const piAuthProvider = args.connection?.piAuthProvider;
if (!piAuthProvider) {
// No provider hint — fall back to generic subprocess path
return null;
}

// Resolve the model's API type from the Pi SDK registry.
// For anthropic-messages providers, do a lightweight direct HTTP test
// instead of spawning a full Pi subprocess (which can exceed the timeout).
let modelApi: string | undefined;
let modelBaseUrl: string | undefined;
try {
const { getModels } = await import('@mariozechner/pi-ai');
const models = getModels(piAuthProvider as Parameters<typeof getModels>[0]);
const requestedId = args.model.startsWith('pi/') ? args.model.slice(3) : args.model;
const match = models.find(m => m.id === requestedId) || models[0];
if (match) {
modelApi = (match as { api?: string }).api;
modelBaseUrl = (match as { baseUrl?: string }).baseUrl;
}
} catch { /* ignore — fall through to subprocess */ }

if (modelApi !== 'anthropic-messages') {
// Non-Anthropic API types need the full Pi SDK — let factory.ts handle it
return null;
}

const baseUrl = args.baseUrl?.trim() || modelBaseUrl || getPiProviderBaseUrl(piAuthProvider);
if (!baseUrl) {
return { success: false, error: 'Could not determine API endpoint for provider' };
}

// Strip Pi SDK's 'pi/' prefix — Anthropic-compatible endpoints only accept bare model IDs
let bareModel = args.model.startsWith('pi/') ? args.model.slice(3) : args.model;
// MiniMax CN API doesn't accept the 'MiniMax-' prefix on model names
if (piAuthProvider === 'minimax-cn' && bareModel.startsWith('MiniMax-')) {
bareModel = bareModel.slice('MiniMax-'.length);
}
return testAnthropicCompatible(args.apiKey, baseUrl, bareModel, args.timeoutMs);
},
validateStoredConnection: async () => ({ success: true }),
};