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
56 changes: 51 additions & 5 deletions src/ai-providers/agent-sdk/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,35 @@ function stripImageData(raw: string): string {
} catch { return raw }
}

// ==================== Error classification ====================

type ErrorClass = 'auth' | 'model' | 'unknown'

const AUTH_PATTERNS = [
/\b401\b/,
/\binvalid[_\s-]?api[_\s-]?key\b/i,
/\bauthentication\b/i,
/\bunauthor(?:ized|ised)\b/i,
/\bpermission[_\s-]denied\b/i,
/\bx-api-key\b/i,
]

const MODEL_PATTERNS = [
/\bmodel[_\s-]not[_\s-]found\b/i,
/\binvalid[_\s-]?model\b/i,
/\bunknown[_\s-]?model\b/i,
/\bmodel\s+.+?\s+(?:does\s+not\s+exist|is\s+not\s+(?:a\s+)?valid)\b/i,
]

function classifyError(details: Record<string, unknown>): ErrorClass {
const haystack = [details.message, details.stderr, details.stdout]
.filter((x): x is string => typeof x === 'string')
.join('\n')
if (AUTH_PATTERNS.some(p => p.test(haystack))) return 'auth'
if (MODEL_PATTERNS.some(p => p.test(haystack))) return 'model'
return 'unknown'
}

// ==================== Public ====================

/**
Expand Down Expand Up @@ -217,8 +246,18 @@ export async function askAgentSdk(
resultText = result.errors?.join('\n') ?? `Agent SDK error: ${result.subtype}`
// Log failed results with all available detail
const resultDetail = { subtype: result.subtype, errors: result.errors, result: result.result }
logger.error({ ...resultDetail, turns: result.num_turns, durationMs: result.duration_ms }, 'result_error')
console.error('[agent-sdk] Non-success result:', resultDetail)
const classification = classifyError({
message: resultText,
stderr: Array.isArray(result.errors) ? result.errors.join('\n') : undefined,
})
logger.error({ ...resultDetail, classification, turns: result.num_turns, durationMs: result.duration_ms }, 'result_error')
if (classification === 'auth') {
console.warn('[agent-sdk] Auth failed — check your API key / baseUrl in the active profile')
} else if (classification === 'model') {
console.warn('[agent-sdk] Model not available on this endpoint — check the region / model combo')
} else {
console.error('[agent-sdk] Non-success result:', resultDetail)
}
}
logger.info({ subtype: result.subtype, turns: result.num_turns, durationMs: result.duration_ms }, 'result')
}
Expand All @@ -238,9 +277,16 @@ export async function askAgentSdk(
const extraKeys = Object.keys(errObj).filter(k => !(k in details))
for (const k of extraKeys) details[k] = (errObj as any)[k]

logger.error(details, 'query_error')
// Also log to console so the developer can see it in the terminal
console.error('[agent-sdk] Claude Code process error:', details)
const classification = classifyError(details)
logger.error({ ...details, classification }, 'query_error')
if (classification === 'auth') {
// User-fixable: don't scream, just hint. Full detail already in logs/agent-sdk.log.
console.warn('[agent-sdk] Auth failed — check your API key / baseUrl in the active profile')
} else if (classification === 'model') {
console.warn('[agent-sdk] Model not available on this endpoint — check the region / model combo')
} else {
console.error('[agent-sdk] Claude Code process error:', details)
}
ok = false
const stderrHint = details.stderr ? `\nstderr: ${details.stderr}` : ''
resultText = `Agent SDK error: ${errObj.message}${stderrHint}`
Expand Down
76 changes: 74 additions & 2 deletions src/ai-providers/preset-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export interface ModelOption {
label: string
}

export interface EndpointOption {
id: string
label: string
}

export interface PresetDef {
id: string
label: string
Expand All @@ -29,6 +34,7 @@ export interface PresetDef {
defaultName: string
zodSchema: z.ZodType
models?: ModelOption[]
endpoints?: EndpointOption[]
writeOnlyFields?: string[]
}

Expand Down Expand Up @@ -143,20 +149,84 @@ export const MINIMAX: PresetDef = {
description: 'MiniMax models via Claude Agent SDK (Anthropic-compatible)',
category: 'third-party',
defaultName: 'MiniMax',
hint: 'Get your API key at minimaxi.com',
hint: 'China console: minimaxi.com — International console: minimax.io. API keys are region-locked.',
zodSchema: z.object({
backend: z.literal('agent-sdk'),
loginMethod: z.literal('api-key'),
baseUrl: z.literal('https://api.minimaxi.com/anthropic').describe('MiniMax API endpoint'),
baseUrl: z.string().default('https://api.minimaxi.com/anthropic').describe('API endpoint'),
model: z.string().default('MiniMax-M2.7').describe('Model'),
apiKey: z.string().min(1).describe('MiniMax API key'),
}),
endpoints: [
{ id: 'https://api.minimaxi.com/anthropic', label: 'China (minimaxi.com)' },
{ id: 'https://api.minimax.io/anthropic', label: 'International (minimax.io)' },
],
models: [
{ id: 'MiniMax-M2.7', label: 'MiniMax M2.7' },
],
writeOnlyFields: ['apiKey'],
}

// ==================== Third-party: GLM (Zhipu) ====================

export const GLM: PresetDef = {
id: 'glm',
label: 'GLM (Zhipu)',
description: 'Zhipu GLM models via Claude Agent SDK (Anthropic-compatible)',
category: 'third-party',
defaultName: 'GLM',
hint: 'China console: bigmodel.cn — International console: z.ai. API keys are region-locked. Latest GLM 5.1 is China-only for now.',
zodSchema: z.object({
backend: z.literal('agent-sdk'),
loginMethod: z.literal('api-key'),
baseUrl: z.string().default('https://open.bigmodel.cn/api/anthropic').describe('API endpoint'),
model: z.string().default('glm-4.7').describe('Model'),
apiKey: z.string().min(1).describe('GLM API key'),
}),
endpoints: [
{ id: 'https://open.bigmodel.cn/api/anthropic', label: 'China (bigmodel.cn)' },
{ id: 'https://api.z.ai/api/anthropic', label: 'International (z.ai)' },
],
models: [
{ id: 'glm-5.1', label: 'GLM 5.1 (China only)' },
{ id: 'glm-4.7', label: 'GLM 4.7' },
{ id: 'glm-4.6', label: 'GLM 4.6 — 200K (China only)' },
{ id: 'glm-4.5-air', label: 'GLM 4.5 Air' },
],
writeOnlyFields: ['apiKey'],
}

// ==================== Third-party: Kimi (Moonshot) ====================

// Moonshot officially pushes OpenAI Chat Completions as the primary integration
// path; we route via their secondary Anthropic-compat endpoint
// (api.moonshot.*/anthropic) to stay on agent-sdk. Our codex backend speaks
// the OpenAI Responses API, which Moonshot's direct endpoints do not
// implement, so codex isn't a viable alternative here.
export const KIMI: PresetDef = {
id: 'kimi',
label: 'Kimi (Moonshot)',
description: 'Moonshot Kimi models via Claude Agent SDK (Anthropic-compatible)',
category: 'third-party',
defaultName: 'Kimi',
hint: 'China console: platform.moonshot.cn — International console: platform.moonshot.ai. API keys are region-locked.',
zodSchema: z.object({
backend: z.literal('agent-sdk'),
loginMethod: z.literal('api-key'),
baseUrl: z.string().default('https://api.moonshot.cn/anthropic').describe('API endpoint'),
model: z.string().default('kimi-k2.5').describe('Model'),
apiKey: z.string().min(1).describe('Moonshot API key'),
}),
endpoints: [
{ id: 'https://api.moonshot.cn/anthropic', label: 'China (moonshot.cn)' },
{ id: 'https://api.moonshot.ai/anthropic', label: 'International (moonshot.ai)' },
],
models: [
{ id: 'kimi-k2.5', label: 'Kimi K2.5' },
],
writeOnlyFields: ['apiKey'],
}

// ==================== Custom ====================

export const CUSTOM: PresetDef = {
Expand Down Expand Up @@ -185,5 +255,7 @@ export const PRESET_CATALOG: PresetDef[] = [
CODEX_API,
GEMINI,
MINIMAX,
GLM,
KIMI,
CUSTOM,
]
17 changes: 11 additions & 6 deletions src/ai-providers/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,17 @@ function buildJsonSchema(def: PresetDef): Record<string, unknown> {
const raw = z.toJSONSchema(def.zodSchema) as Record<string, unknown>
const props = (raw.properties ?? {}) as Record<string, Record<string, unknown>>

// Replace model enum with oneOf (labeled options)
const mf = 'model'
if (def.models?.length && props[mf]) {
const oneOf = def.models.map(m => ({ const: m.id, title: m.label }))
const { enum: _e, ...rest } = props[mf]
props[mf] = { ...rest, oneOf }
// Replace scalar string fields with labeled oneOf when a catalog is provided
const labeledFields: Array<[string, Array<{ id: string; label: string }> | undefined]> = [
['model', def.models],
['baseUrl', def.endpoints],
]
for (const [field, options] of labeledFields) {
if (options?.length && props[field]) {
const oneOf = options.map(o => ({ const: o.id, title: o.label }))
const { enum: _e, ...rest } = props[field]
props[field] = { ...rest, oneOf }
}
}

// Mark writeOnly fields
Expand Down
Loading