mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-30 00:05:33 +08:00
* fix: remove the hide cursor on windows logic * feat(cursor): update fallback models * fix(claude): force fallback models and disable supportedModels lookup
192 lines
5.6 KiB
TypeScript
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);
|
|
}
|
|
}
|