Files
claudecodeui/server/modules/providers/list/claude/claude-models.provider.ts
Haile 3b79aab958 Fix/use fallback models for claude (#806)
* fix: remove the hide cursor on windows logic

* feat(cursor): update fallback models

* fix(claude): force fallback models and disable supportedModels lookup
2026-05-29 13:33:13 +02:00

192 lines
5.6 KiB
TypeScript

import { readFile } from 'node:fs/promises';
import { sessionsDb } from '@/modules/database/index.js';
import type { IProviderModels } from '@/shared/interfaces.js';
import type {
ProviderChangeActiveModelInput,
ProviderCurrentActiveModel,
ProviderModelsDefinition,
ProviderSessionActiveModelChange,
} from '@/shared/types.js';
import {
buildDefaultProviderCurrentActiveModel,
writeProviderSessionActiveModelChange,
} from '@/shared/utils.js';
export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
OPTIONS: [
{
value: 'default',
label: 'Default (recommended)',
description: 'Use the default model (currently Opus 4.7 (1M context)) · $5/$25 per Mtok',
},
{
value: 'sonnet',
label: 'Sonnet',
description: 'Sonnet 4.6 · Best for everyday tasks · $3/$15 per Mtok',
},
{
value: 'sonnet[1m]',
label: 'Sonnet (1M context)',
description: 'Sonnet 4.6 for long sessions · $3/$15 per Mtok',
},
{
value: 'haiku',
label: 'Haiku',
description: 'Haiku 4.5 · Fastest for quick answers · $1/$5 per Mtok',
},
],
DEFAULT: 'default',
};
type ClaudeInitEvent = {
sessionId?: string;
session_id?: string;
type?: string;
subtype?: string;
model?: string;
message?: {
content?: unknown;
model?: string;
};
};
const ANSI_PATTERN = new RegExp(
'[\\u001B\\u009B][[\\]()#;?]*(?:'
+ '(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]'
+ '|(?:[\\dA-PR-TZcf-ntqry=><~]))',
'g',
);
const extractClaudeEventModel = (event: ClaudeInitEvent, sessionId: string): string | null => {
const eventSessionId = event.sessionId ?? event.session_id;
if (eventSessionId && eventSessionId !== sessionId) {
return null;
}
const contentModel = extractClaudeModelFromMessageContent(event.message?.content);
if (contentModel) {
return contentModel;
}
const directModel = event.model?.trim();
if (directModel) {
return directModel;
}
const messageModel = event.message?.model?.trim();
return messageModel || null;
};
const stripAnsi = (value: string): string => value.replace(ANSI_PATTERN, '');
const extractTaggedContent = (content: string, tagName: string): string | null => {
const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = new RegExp(`<${escapedTagName}>([\\s\\S]*?)<\\/${escapedTagName}>`).exec(content);
return match ? match[1] : null;
};
const extractClaudeModelFromTextContent = (content: string): string | null => {
const localCommandStdout = extractTaggedContent(content, 'local-command-stdout');
if (localCommandStdout !== null) {
const cleanedStdout = stripAnsi(localCommandStdout).replace(/\s+/g, ' ').trim();
const changedModel = /(?:set|changed|switched)\s+model\s+to\s+(.+?)\.?$/i.exec(cleanedStdout);
if (changedModel?.[1]?.trim()) {
return changedModel[1].trim();
}
}
const modelTag = extractTaggedContent(content, 'model')?.trim();
return modelTag || null;
};
const extractClaudeModelFromMessageContent = (content: unknown): string | null => {
if (typeof content === 'string') {
return extractClaudeModelFromTextContent(content);
}
if (!Array.isArray(content)) {
return null;
}
for (const part of content) {
if (!part || typeof part !== 'object' || !('text' in part) || typeof part.text !== 'string') {
continue;
}
const model = extractClaudeModelFromTextContent(part.text);
if (model) {
return model;
}
}
return null;
};
const readClaudeSessionModelFromJsonl = async (
sessionId: string,
jsonlPath: string,
): Promise<ProviderCurrentActiveModel | null> => {
const content = await readFile(jsonlPath, 'utf8');
const lines = content
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
for (let index = lines.length - 1; index >= 0; index -= 1) {
try {
const event = JSON.parse(lines[index]) as ClaudeInitEvent;
const model = extractClaudeEventModel(event, sessionId);
if (model) {
return { model };
}
} catch {
// Skip malformed JSONL lines that can happen during concurrent writes.
}
}
return null;
};
export class ClaudeProviderModels implements IProviderModels {
async getSupportedModels(): Promise<ProviderModelsDefinition> {
// claude creates a new jsonl file as a separate session for this request.
// As a result, it lists the workspace where this is invoked when it shouldn't.
//
// Disabled for now:
// const queryInstance = query({
// prompt: 'Get supported models',
// options: buildClaudeQueryOptions(),
// });
// const supportedModels = await queryInstance.supportedModels();
// queryInstance.close();
// return buildClaudeModelsDefinition(supportedModels);
return CLAUDE_FALLBACK_MODELS;
}
async getCurrentActiveModel(sessionId?: string): Promise<ProviderCurrentActiveModel> {
if (!sessionId?.trim()) {
return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels());
}
try {
const jsonlPath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
const activeModel = jsonlPath
? await readClaudeSessionModelFromJsonl(sessionId, jsonlPath)
: null;
if (activeModel?.model) {
return activeModel;
}
} catch {
// Fall through to the provider default when the session-backed lookup fails.
}
return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels());
}
async changeActiveModel(
input: ProviderChangeActiveModelInput,
): Promise<ProviderSessionActiveModelChange> {
return writeProviderSessionActiveModelChange('claude', input);
}
}