mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-09 11:51:15 +02:00
529 lines
17 KiB
TypeScript
529 lines
17 KiB
TypeScript
import {
|
|
type CustomModelSetting,
|
|
MODEL_SLUG_ALIASES_BY_PROVIDER,
|
|
ModelCapabilities,
|
|
type ModelSelection,
|
|
ProviderDriverKind,
|
|
ProviderInstanceId,
|
|
type ProviderOptionDescriptor,
|
|
type ProviderOptionSelection,
|
|
} from "@t3tools/contracts";
|
|
import * as Option from "effect/Option";
|
|
import * as Schema from "effect/Schema";
|
|
import { copySorted } from "./Array.ts";
|
|
|
|
const DEFAULT_PROVIDER_DRIVER_KIND = ProviderDriverKind.make("codex");
|
|
|
|
/** Choose the command for a model change against the thread's current provider instance. */
|
|
export function modelSelectionCommandType(
|
|
currentInstanceId: ProviderInstanceId,
|
|
selection: ModelSelection,
|
|
) {
|
|
return currentInstanceId === selection.instanceId
|
|
? ("thread.model-selection.set" as const)
|
|
: ("provider.switch" as const);
|
|
}
|
|
|
|
export interface SelectableModelOption {
|
|
slug: string;
|
|
name: string;
|
|
aliases?: ReadonlyArray<string> | undefined;
|
|
}
|
|
|
|
export function createModelCapabilities(input: {
|
|
optionDescriptors: ReadonlyArray<ProviderOptionDescriptor>;
|
|
}): ModelCapabilities {
|
|
return {
|
|
optionDescriptors: input.optionDescriptors.map(cloneDescriptor),
|
|
};
|
|
}
|
|
|
|
function getRawSelectionValueById(
|
|
selections: ReadonlyArray<ProviderOptionSelection> | null | undefined,
|
|
id: string,
|
|
): string | boolean | undefined {
|
|
const selection = selections?.find((candidate) => candidate.id === id);
|
|
return selection?.value;
|
|
}
|
|
|
|
function getProviderOptionSelectionValue(
|
|
selections: ReadonlyArray<ProviderOptionSelection> | null | undefined,
|
|
id: string,
|
|
): string | boolean | undefined {
|
|
return getRawSelectionValueById(selections, id);
|
|
}
|
|
|
|
export function getProviderOptionStringSelectionValue(
|
|
selections: ReadonlyArray<ProviderOptionSelection> | null | undefined,
|
|
id: string,
|
|
): string | undefined {
|
|
const value = getProviderOptionSelectionValue(selections, id);
|
|
return typeof value === "string" ? value : undefined;
|
|
}
|
|
|
|
export function getProviderOptionBooleanSelectionValue(
|
|
selections: ReadonlyArray<ProviderOptionSelection> | null | undefined,
|
|
id: string,
|
|
): boolean | undefined {
|
|
const value = getProviderOptionSelectionValue(selections, id);
|
|
return typeof value === "boolean" ? value : undefined;
|
|
}
|
|
|
|
export function getModelSelectionStringOptionValue(
|
|
modelSelection: ModelSelection | null | undefined,
|
|
id: string,
|
|
): string | undefined {
|
|
return getProviderOptionStringSelectionValue(modelSelection?.options, id);
|
|
}
|
|
|
|
export function getModelSelectionBooleanOptionValue(
|
|
modelSelection: ModelSelection | null | undefined,
|
|
id: string,
|
|
): boolean | undefined {
|
|
return getProviderOptionBooleanSelectionValue(modelSelection?.options, id);
|
|
}
|
|
|
|
function canonicalModelSelectionOptions(
|
|
modelSelection: ModelSelection,
|
|
): ReadonlyArray<readonly [id: string, value: string | boolean]> {
|
|
return copySorted(
|
|
(modelSelection.options ?? []).map(
|
|
(selection): readonly [id: string, value: string | boolean] => [
|
|
selection.id,
|
|
selection.value,
|
|
],
|
|
),
|
|
(
|
|
[leftId, leftValue]: readonly [id: string, value: string | boolean],
|
|
[rightId, rightValue]: readonly [id: string, value: string | boolean],
|
|
) => {
|
|
const idOrder = leftId.localeCompare(rightId);
|
|
return idOrder !== 0 ? idOrder : String(leftValue).localeCompare(String(rightValue));
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Compares the complete provider selection while treating option ordering and
|
|
* an omitted empty option list as presentation details.
|
|
*/
|
|
export function modelSelectionsEqual(left: ModelSelection, right: ModelSelection): boolean {
|
|
if (left.instanceId !== right.instanceId || left.model !== right.model) {
|
|
return false;
|
|
}
|
|
const leftOptions = canonicalModelSelectionOptions(left);
|
|
const rightOptions = canonicalModelSelectionOptions(right);
|
|
return (
|
|
leftOptions.length === rightOptions.length &&
|
|
leftOptions.every(
|
|
([id, value], index) => id === rightOptions[index]?.[0] && value === rightOptions[index]?.[1],
|
|
)
|
|
);
|
|
}
|
|
|
|
function resolveDescriptorChoiceValue(
|
|
descriptor: Extract<ProviderOptionDescriptor, { type: "select" }>,
|
|
raw: string | null | undefined,
|
|
): string | undefined {
|
|
const trimmed = trimOrNull(raw);
|
|
if (!trimmed) {
|
|
return descriptor.currentValue ?? descriptor.options.find((option) => option.isDefault)?.id;
|
|
}
|
|
if (descriptor.options.length === 0) {
|
|
return trimmed;
|
|
}
|
|
if (
|
|
descriptor.promptInjectedValues?.includes(trimmed) &&
|
|
descriptor.options.some((option) => option.id === trimmed)
|
|
) {
|
|
return descriptor.options.find((option) => option.isDefault)?.id;
|
|
}
|
|
if (descriptor.options.some((option) => option.id === trimmed)) {
|
|
return trimmed;
|
|
}
|
|
return descriptor.currentValue ?? descriptor.options.find((option) => option.isDefault)?.id;
|
|
}
|
|
|
|
function cloneDescriptor(descriptor: ProviderOptionDescriptor): ProviderOptionDescriptor {
|
|
return descriptor.type === "select"
|
|
? {
|
|
...descriptor,
|
|
options: [...descriptor.options],
|
|
...(descriptor.promptInjectedValues
|
|
? { promptInjectedValues: [...descriptor.promptInjectedValues] }
|
|
: {}),
|
|
}
|
|
: { ...descriptor };
|
|
}
|
|
|
|
function cloneSelection(selection: ProviderOptionSelection): ProviderOptionSelection {
|
|
return { ...selection };
|
|
}
|
|
|
|
function withDescriptorCurrentValue(
|
|
descriptor: ProviderOptionDescriptor,
|
|
rawCurrentValue: string | boolean | undefined,
|
|
): ProviderOptionDescriptor {
|
|
if (descriptor.type === "boolean") {
|
|
if (typeof rawCurrentValue === "boolean") {
|
|
return {
|
|
...descriptor,
|
|
currentValue: rawCurrentValue,
|
|
};
|
|
}
|
|
return descriptor;
|
|
}
|
|
const currentValue =
|
|
typeof rawCurrentValue === "string"
|
|
? resolveDescriptorChoiceValue(descriptor, rawCurrentValue)
|
|
: resolveDescriptorChoiceValue(descriptor, descriptor.currentValue);
|
|
if (!currentValue) {
|
|
const { currentValue: _unusedCurrentValue, ...rest } = descriptor;
|
|
return rest;
|
|
}
|
|
return {
|
|
...descriptor,
|
|
currentValue,
|
|
};
|
|
}
|
|
|
|
export function getProviderOptionDescriptors(input: {
|
|
caps: ModelCapabilities;
|
|
selections?: ReadonlyArray<ProviderOptionSelection> | null | undefined;
|
|
}): ReadonlyArray<ProviderOptionDescriptor> {
|
|
const { caps, selections } = input;
|
|
const baseDescriptors = (caps.optionDescriptors ?? []).map(cloneDescriptor);
|
|
|
|
return baseDescriptors.map((descriptor) =>
|
|
withDescriptorCurrentValue(
|
|
descriptor,
|
|
getRawSelectionValueById(selections, descriptor.id) ?? descriptor.currentValue,
|
|
),
|
|
);
|
|
}
|
|
|
|
function getReportedOptionValue(
|
|
id: string,
|
|
selection?: ModelSelection | null,
|
|
reportedSelection?: ModelSelection | null,
|
|
) {
|
|
if (
|
|
!selection ||
|
|
!reportedSelection ||
|
|
selection.instanceId !== reportedSelection.instanceId ||
|
|
selection.model !== reportedSelection.model ||
|
|
selection.options?.some((option) => option.id === id)
|
|
)
|
|
return undefined;
|
|
return getRawSelectionValueById(reportedSelection.options, id);
|
|
}
|
|
|
|
export function getProviderOptionCurrentValue(
|
|
descriptor: ProviderOptionDescriptor | null | undefined,
|
|
selection?: ModelSelection | null,
|
|
reportedSelection?: ModelSelection | null,
|
|
): string | boolean | undefined {
|
|
if (!descriptor) {
|
|
return undefined;
|
|
}
|
|
const hasExplicitOption = selection?.options?.some((option) => option.id === descriptor.id);
|
|
// Reported values are display-only; callers that build dispatch options omit this context.
|
|
const reportedValue = getReportedOptionValue(descriptor.id, selection, reportedSelection);
|
|
if (reportedValue !== undefined) return reportedValue;
|
|
if (descriptor.id === "variant" && selection && !hasExplicitOption) return undefined;
|
|
if (descriptor.type === "boolean") {
|
|
return descriptor.currentValue;
|
|
}
|
|
if (descriptor.currentValue) {
|
|
return descriptor.currentValue;
|
|
}
|
|
return descriptor.options.find((option) => option.isDefault)?.id;
|
|
}
|
|
|
|
export function getProviderOptionCurrentLabel(
|
|
descriptor: ProviderOptionDescriptor | null | undefined,
|
|
selection?: ModelSelection | null,
|
|
reportedSelection?: ModelSelection | null,
|
|
): string | undefined {
|
|
if (!descriptor) {
|
|
return undefined;
|
|
}
|
|
if (descriptor.type === "boolean") {
|
|
return typeof descriptor.currentValue === "boolean"
|
|
? descriptor.currentValue
|
|
? "On"
|
|
: "Off"
|
|
: undefined;
|
|
}
|
|
const currentValue = getProviderOptionCurrentValue(descriptor, selection, reportedSelection);
|
|
return (
|
|
descriptor.options.find((option) => option.id === currentValue)?.label ??
|
|
(getReportedOptionValue(descriptor.id, selection, reportedSelection) === "default"
|
|
? "Default"
|
|
: descriptor.id === "variant"
|
|
? "Unknown"
|
|
: undefined)
|
|
);
|
|
}
|
|
|
|
export function buildProviderOptionSelectionsFromDescriptors(
|
|
descriptors: ReadonlyArray<ProviderOptionDescriptor> | null | undefined,
|
|
): Array<ProviderOptionSelection> | undefined {
|
|
if (!descriptors || descriptors.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const nextSelections: Array<ProviderOptionSelection> = [];
|
|
|
|
for (const descriptor of descriptors) {
|
|
const value = getProviderOptionCurrentValue(descriptor);
|
|
if (typeof value === "string" || typeof value === "boolean") {
|
|
nextSelections.push({ id: descriptor.id, value });
|
|
}
|
|
}
|
|
|
|
return nextSelections.length > 0 ? nextSelections : undefined;
|
|
}
|
|
|
|
export function buildExplicitProviderOptionSelectionsFromDescriptors(
|
|
descriptors: ReadonlyArray<ProviderOptionDescriptor> | null | undefined,
|
|
selections: ReadonlyArray<ProviderOptionSelection> | null | undefined,
|
|
): Array<ProviderOptionSelection> | undefined {
|
|
if (!selections || selections.length === 0) {
|
|
return undefined;
|
|
}
|
|
const explicitIds = new Set(selections.map((selection) => selection.id));
|
|
const normalized = buildProviderOptionSelectionsFromDescriptors(descriptors)?.filter(
|
|
(selection) => explicitIds.has(selection.id),
|
|
);
|
|
return normalized && normalized.length > 0 ? normalized : undefined;
|
|
}
|
|
|
|
export function isClaudeUltrathinkPrompt(text: string | null | undefined): boolean {
|
|
return typeof text === "string" && /\bultrathink\b/i.test(text);
|
|
}
|
|
|
|
/** Compare Codex model families without changing provider-owned dispatch identifiers. */
|
|
export function codexModelFamily(slug: string): string {
|
|
return slug.startsWith("openai.gpt-") ? slug.slice("openai.".length) : slug;
|
|
}
|
|
|
|
export function formatCodexModelName(name: string): string {
|
|
return name.replace(/^gpt/i, "GPT").replace(/-([a-z])/g, (_, c) => "-" + c.toUpperCase());
|
|
}
|
|
|
|
export function formatModelSlugName(slug: string): string {
|
|
const separator = slug.lastIndexOf("/") + 1;
|
|
const prefix = slug.slice(0, separator);
|
|
const name = slug.slice(separator);
|
|
if (/^gpt-\d/i.test(name)) return prefix + formatCodexModelName(name);
|
|
if (!/^(claude-(opus|sonnet|haiku|fable)|gemini|grok|composer)-\d/i.test(name)) return slug;
|
|
return (
|
|
prefix +
|
|
name
|
|
.replace(/^(claude-[a-z]+-\d+)-(\d{1,2})(?=-|\[|$)/i, "$1.$2")
|
|
.split("-")
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
.join(" ")
|
|
);
|
|
}
|
|
|
|
export function normalizeModelSlug(
|
|
model: string | null | undefined,
|
|
provider: ProviderDriverKind = DEFAULT_PROVIDER_DRIVER_KIND,
|
|
): string | null {
|
|
const trimmed = normalizeCustomModelSlug(model);
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
|
|
const aliases = MODEL_SLUG_ALIASES_BY_PROVIDER[provider] ?? {};
|
|
const aliased = Object.prototype.hasOwnProperty.call(aliases, trimmed)
|
|
? aliases[trimmed]
|
|
: undefined;
|
|
return typeof aliased === "string" ? aliased : trimmed;
|
|
}
|
|
|
|
/** Custom model identifiers are provider-owned, so only trim them; never expand aliases. */
|
|
export function normalizeCustomModelSlug(model: string | null | undefined): string | null {
|
|
if (typeof model !== "string") {
|
|
return null;
|
|
}
|
|
|
|
return model.trim() || null;
|
|
}
|
|
|
|
/** A custom model setting with its optional fields resolved. */
|
|
export interface CustomModelDefinition {
|
|
readonly slug: string;
|
|
readonly name: string;
|
|
readonly capabilities: ModelCapabilities | null;
|
|
}
|
|
|
|
const decodeCustomModelCapabilities = Schema.decodeUnknownOption(ModelCapabilities);
|
|
|
|
/**
|
|
* Read a `customModels` setting into resolved definitions. Accepts the typed
|
|
* union as well as the opaque `providerInstances[id].config` blob clients see,
|
|
* so it tolerates bare slugs, malformed rows, and unparseable capabilities
|
|
* (dropped rather than failing the whole list). Slugs are trimmed and
|
|
* deduplicated, first occurrence wins; `name` falls back to the slug.
|
|
*/
|
|
export function readCustomModelEntries(value: unknown): CustomModelDefinition[] {
|
|
if (!Array.isArray(value)) return [];
|
|
const entries: CustomModelDefinition[] = [];
|
|
const seen = new Set<string>();
|
|
for (const raw of value) {
|
|
const record =
|
|
typeof raw === "string"
|
|
? { slug: raw }
|
|
: raw !== null && typeof raw === "object"
|
|
? (raw as { slug?: unknown; name?: unknown; capabilities?: unknown })
|
|
: null;
|
|
if (!record) continue;
|
|
const slug = normalizeCustomModelSlug(typeof record.slug === "string" ? record.slug : null);
|
|
if (!slug || seen.has(slug)) continue;
|
|
seen.add(slug);
|
|
const name =
|
|
(typeof record.name === "string" ? normalizeCustomModelSlug(record.name) : null) ?? slug;
|
|
const capabilities =
|
|
record.capabilities === undefined || record.capabilities === null
|
|
? null
|
|
: Option.getOrNull(decodeCustomModelCapabilities(record.capabilities));
|
|
entries.push({
|
|
slug,
|
|
name,
|
|
capabilities: capabilities
|
|
? createModelCapabilities({ optionDescriptors: capabilities.optionDescriptors ?? [] })
|
|
: null,
|
|
});
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
/**
|
|
* Write a definition back to the compact stored shape: a bare slug when it
|
|
* carries nothing custom, otherwise an entry with only the set fields.
|
|
*/
|
|
export function toCustomModelSetting(entry: CustomModelDefinition): CustomModelSetting {
|
|
const descriptors = entry.capabilities?.optionDescriptors ?? [];
|
|
const name = entry.name !== entry.slug ? entry.name : undefined;
|
|
if (!name && descriptors.length === 0) return entry.slug;
|
|
return {
|
|
slug: entry.slug,
|
|
...(name ? { name } : {}),
|
|
...(descriptors.length > 0
|
|
? { capabilities: createModelCapabilities({ optionDescriptors: descriptors }) }
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
export function resolveSelectableModel(
|
|
provider: ProviderDriverKind,
|
|
value: string | null | undefined,
|
|
options: ReadonlyArray<SelectableModelOption>,
|
|
): string | null {
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
|
|
const trimmed = value.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
|
|
const direct = options.find((option) => option.slug === trimmed);
|
|
if (direct) {
|
|
return direct.slug;
|
|
}
|
|
|
|
const byName = options.find((option) => option.name.toLowerCase() === trimmed.toLowerCase());
|
|
if (byName) {
|
|
return byName.slug;
|
|
}
|
|
|
|
const byAlias = options.find((option) =>
|
|
option.aliases?.some((alias) => alias.toLowerCase() === trimmed.toLowerCase()),
|
|
);
|
|
if (byAlias) {
|
|
return byAlias.slug;
|
|
}
|
|
|
|
const normalized = normalizeModelSlug(trimmed, provider);
|
|
if (!normalized) {
|
|
return null;
|
|
}
|
|
|
|
const resolved = options.find((option) => option.slug === normalized);
|
|
return resolved ? resolved.slug : null;
|
|
}
|
|
|
|
/** Trim a string, returning null for empty/missing values. */
|
|
function trimOrNull<T extends string>(value: T | null | undefined): T | null {
|
|
if (typeof value !== "string") return null;
|
|
const trimmed = value.trim() as T;
|
|
return trimmed || null;
|
|
}
|
|
|
|
function cloneSelections(
|
|
selections: ReadonlyArray<ProviderOptionSelection>,
|
|
): Array<ProviderOptionSelection> {
|
|
return selections.map(cloneSelection);
|
|
}
|
|
|
|
export function createModelSelection(
|
|
instanceId: ProviderInstanceId,
|
|
model: string,
|
|
options?: ReadonlyArray<ProviderOptionSelection> | null,
|
|
): ModelSelection {
|
|
const selections = options ? cloneSelections(options) : [];
|
|
const base: ModelSelection = {
|
|
instanceId,
|
|
model,
|
|
};
|
|
return selections.length > 0 ? { ...base, options: selections } : base;
|
|
}
|
|
|
|
/**
|
|
* Returns the effort value if it is a prompt-injected value according to
|
|
* any select descriptor in the given capabilities, or null otherwise.
|
|
*
|
|
* Unlike a single `find`, this checks every descriptor so that the
|
|
* correct descriptor's `promptInjectedValues` list is consulted even when
|
|
* multiple select descriptors exist.
|
|
*/
|
|
export function resolvePromptInjectedEffort(
|
|
caps: ModelCapabilities,
|
|
rawEffort: string | null | undefined,
|
|
): string | null {
|
|
const trimmed = trimOrNull(rawEffort);
|
|
if (!trimmed) return null;
|
|
const descriptors = getProviderOptionDescriptors({ caps });
|
|
for (const descriptor of descriptors) {
|
|
if (descriptor.type === "select" && descriptor.promptInjectedValues?.includes(trimmed)) {
|
|
return trimmed;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function applyClaudePromptEffortPrefix(
|
|
text: string,
|
|
effort: string | null | undefined,
|
|
): string {
|
|
const trimmed = text.trim();
|
|
if (!trimmed) {
|
|
return trimmed;
|
|
}
|
|
// Prefixing a slash command turns it into plain prose, so Claude never
|
|
// runs it. Command names come from arbitrary file names ("/deploy.prod",
|
|
// "/plugin:skill"), so accept any first token without a second slash;
|
|
// absolute paths like "/home/theo/app.ts" keep the prefix.
|
|
if (effort !== "ultrathink" || /^\/[^\s/]+(?:\s|$)/u.test(trimmed)) {
|
|
return trimmed;
|
|
}
|
|
if (trimmed.startsWith("Ultrathink:")) {
|
|
return trimmed;
|
|
}
|
|
return `Ultrathink:\n${trimmed}`;
|
|
}
|