[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,358 @@
import type { ProviderStyle } from './types';
import { resolveOpenAIApi, resolveProviderStyle, type ProviderConfig } from './types';
import { CATTY_REASONING_LEVELS } from './composerPicker';
const ANTHROPIC_THINKING_BUDGET: Record<'low' | 'medium' | 'high', number> = {
low: 4_000,
medium: 10_000,
high: 20_000,
};
const GEMINI_25_THINKING_BUDGET: Record<'low' | 'medium' | 'high', number> = {
low: 1_024,
medium: 8_192,
high: 16_384,
};
const REASONING_RANK = ['off', 'minimal', 'low', 'medium', 'high'] as const;
const LEVELS_LOW_MEDIUM_HIGH = ['low', 'medium', 'high'] as const;
const LEVELS_LOW_HIGH = ['low', 'high'] as const;
const LEVELS_MINIMAL_LOW_MEDIUM_HIGH = ['minimal', 'low', 'medium', 'high'] as const;
const LEVELS_MINIMAL_HIGH = ['minimal', 'high'] as const;
export type CattyReasoningProviderOptions = Record<string, Record<string, unknown>>;
/** Extra completion tokens the SDK will add on top of maxTokens for thinking. */
export function estimateReasoningOutputReserve(
options: CattyReasoningProviderOptions | undefined,
): number {
if (!options) return 0;
const anthropicThinking = options.anthropic?.thinking as { budgetTokens?: unknown } | undefined;
if (typeof anthropicThinking?.budgetTokens === 'number' && anthropicThinking.budgetTokens > 0) {
return Math.ceil(anthropicThinking.budgetTokens);
}
const googleConfig = options.google?.thinkingConfig as { thinkingBudget?: unknown } | undefined;
if (typeof googleConfig?.thinkingBudget === 'number' && googleConfig.thinkingBudget > 0) {
return Math.ceil(googleConfig.thinkingBudget);
}
return 0;
}
/**
* OpenAI Responses turns must run stateless (`store: false`).
*
* The Responses API defaults to `store: true`, which makes the SDK replay
* prior reasoning turns as server-side item references (`rs_…` ids).
* Relays — the main reason Responses mode is opt-in here — frequently do not
* persist items, so the next turn fails with "Item with id 'rs_…' not found.
* Items are not persisted when `store` is set to false." With `store: false`
* the SDK instead requests `reasoning.encrypted_content` and replays the full
* reasoning items inline, which works against both relays and OpenAI directly.
*
* The installed `@ai-sdk/openai` only auto-adds the encrypted-content include
* for model IDs its own capability detector recognizes (o-series / gpt-5).
* Relay reasoners such as `deepseek-r1`, `gpt-oss`, or `grok-4` pass
* Netcatty's broader classifier but not the SDK's, so the include must be
* requested explicitly or the provider returns no replayable ciphertext and
* the reasoning items get dropped from subsequent turns. The SDK dedupes the
* include when it would have added it anyway.
*
* The include is therefore requested for *every* stateless Responses turn,
* not gated on a reasoning classifier: relay model IDs are arbitrary, and
* always-thinking models whose IDs match no known pattern (e.g. DeepSeek's
* default `deepseek-v4-flash`, which streams thinking deltas) would silently
* lose replayable reasoning. Models that emit no reasoning simply have no
* reasoning items to encrypt, so the extra include is a no-op for them.
*/
const REASONING_ENCRYPTED_CONTENT_INCLUDE = 'reasoning.encrypted_content';
export function applyResponsesApiStatelessStoreOption(
provider: Pick<ProviderConfig, 'openaiApi' | 'providerId' | 'style'> | null | undefined,
options: CattyReasoningProviderOptions | undefined,
): CattyReasoningProviderOptions | undefined {
if (!provider || resolveProviderStyle(provider) !== 'openai') return options;
if (resolveOpenAIApi(provider) !== 'responses') return options;
const openaiOptions: Record<string, unknown> = {
...options?.openai,
store: false,
};
const existingInclude = Array.isArray(openaiOptions.include) ? openaiOptions.include : [];
if (!existingInclude.includes(REASONING_ENCRYPTED_CONTENT_INCLUDE)) {
openaiOptions.include = [...existingInclude, REASONING_ENCRYPTED_CONTENT_INCLUDE];
}
return {
...options,
openai: openaiOptions,
};
}
export function buildCattyReasoningProviderOptions(
provider: Pick<ProviderConfig, 'providerId' | 'style'> | null | undefined,
effort: string | null | undefined,
modelId?: string,
): CattyReasoningProviderOptions | undefined {
if (!provider) return undefined;
const rawEffort = typeof effort === 'string' ? effort.trim() : '';
if (!rawEffort) return undefined;
const style: ProviderStyle = resolveProviderStyle(provider);
const advertised = cattyReasoningLevelsForSelection(provider, modelId);
const resolved = advertised.length
? resolveVisibleCattyThinkingLevel(advertised, rawEffort)
: rawEffort;
if (!resolved) return undefined;
if (style === 'openai') {
if (modelId && !openaiModelLikelySupportsReasoning(modelId)) return undefined;
if (resolved === 'off') {
if (modelId && openaiModelSupportsNoneReasoning(modelId)) {
return { openai: { reasoningEffort: 'none' } };
}
return undefined;
}
return { openai: { reasoningEffort: resolved } };
}
if (style === 'anthropic') {
if (modelId && !anthropicModelLikelySupportsThinking(modelId)) return undefined;
if (resolved === 'off') {
if (modelId && anthropicUsesAdaptiveThinking(modelId) && anthropicAllowsDisabledThinking(modelId)) {
return { anthropic: { thinking: { type: 'disabled' } } };
}
return undefined;
}
if (resolved !== 'low' && resolved !== 'medium' && resolved !== 'high') return undefined;
if (modelId && anthropicUsesAdaptiveThinking(modelId)) {
return {
anthropic: {
thinking: { type: 'adaptive' },
effort: resolved,
},
};
}
return {
anthropic: {
thinking: {
type: 'enabled',
budgetTokens: ANTHROPIC_THINKING_BUDGET[resolved],
},
},
};
}
if (style === 'google') {
if (!modelId || !googleModelLikelySupportsThinking(modelId)) return undefined;
if (isGemini3Model(modelId)) {
if (resolved !== 'minimal' && resolved !== 'low' && resolved !== 'medium' && resolved !== 'high') {
return undefined;
}
return {
google: {
thinkingConfig: {
thinkingLevel: resolved,
includeThoughts: resolved !== 'minimal',
},
},
};
}
if (resolved === 'off') {
if (!googleModelAllowsDisabledThinking(modelId)) return undefined;
return {
google: {
thinkingConfig: {
thinkingBudget: 0,
includeThoughts: false,
},
},
};
}
if (resolved !== 'low' && resolved !== 'medium' && resolved !== 'high') return undefined;
return {
google: {
thinkingConfig: {
thinkingBudget: GEMINI_25_THINKING_BUDGET[resolved],
includeThoughts: true,
},
},
};
}
return undefined;
}
/** Extended thinking is Claude 3.7+ / 4+ / 5+; original Claude 3 Haiku/Sonnet reject it. */
export function anthropicModelLikelySupportsThinking(modelId: string): boolean {
const parsed = parseClaudeModel(modelId);
if (!parsed) return false;
if (parsed.major >= 4) return true;
return parsed.major === 3 && parsed.minor >= 7;
}
export function googleModelLikelySupportsThinking(modelId: string): boolean {
const id = modelId.trim().toLowerCase();
return /gemini-3|gemini-2\.5|gemini-2\.0-flash-thinking|thinking/.test(id);
}
/**
* `reasoning_effort: "none"` is only valid on GPT-5.1+ (and later minors).
* Bare gpt-5 / o3 / o4-mini accept low|medium|high (and sometimes minimal),
* but reject none. Chat snapshots are not reasoners.
*/
export function openaiModelSupportsNoneReasoning(modelId: string): boolean {
const id = modelId.trim().toLowerCase();
if (!id || openaiModelIsChatSnapshot(id)) return false;
return /gpt-5\.(?:[1-9]\d*)/.test(id);
}
/** Original GPT-5 (not 5.1+) accepts `minimal` as the floor instead of `none`. */
export function openaiModelSupportsMinimalReasoning(modelId: string): boolean {
const id = modelId.trim().toLowerCase();
if (!id || openaiModelIsChatSnapshot(id)) return false;
if (openaiModelSupportsNoneReasoning(id)) return false;
return /gpt-5/.test(id);
}
/** OpenAI-compat models that accept `reasoning_effort` (o-series, GPT-5, reasoners). */
export function openaiModelLikelySupportsReasoning(modelId: string): boolean {
const id = modelId.trim().toLowerCase();
if (!id) return false;
if (openaiModelIsChatSnapshot(id)) return false;
return (
/(^|[^a-z0-9])o[1-4]([^a-z0-9]|$)/.test(id)
|| /gpt-5/.test(id)
|| /gpt-oss/.test(id)
|| /reasoner|reasoning/.test(id)
|| /deepseek-r1/.test(id)
|| /grok-4/.test(id)
);
}
/** Levels shown on the Catty thinking chip, or empty when the model cannot take them. */
export function cattyReasoningLevelsForSelection(
provider: Pick<ProviderConfig, 'providerId' | 'style'> | null | undefined,
modelId?: string,
): readonly string[] {
if (!provider) return [];
const style = resolveProviderStyle(provider);
if (style === 'anthropic') {
if (!modelId || !anthropicModelLikelySupportsThinking(modelId)) return [];
if (!anthropicAllowsDisabledThinking(modelId)) return LEVELS_LOW_MEDIUM_HIGH;
return CATTY_REASONING_LEVELS;
}
if (style === 'google') {
if (!modelId || !googleModelLikelySupportsThinking(modelId)) return [];
if (isGemini3Model(modelId)) return gemini3AdvertisedLevels(modelId);
if (!googleModelAllowsDisabledThinking(modelId)) return LEVELS_LOW_MEDIUM_HIGH;
return CATTY_REASONING_LEVELS;
}
if (style === 'openai') {
if (!modelId || !openaiModelLikelySupportsReasoning(modelId)) return [];
if (openaiModelSupportsNoneReasoning(modelId)) return CATTY_REASONING_LEVELS;
if (openaiModelSupportsMinimalReasoning(modelId)) return LEVELS_MINIMAL_LOW_MEDIUM_HIGH;
return LEVELS_LOW_MEDIUM_HIGH;
}
return [];
}
/**
* Pick a level the current model actually advertises; never keep a stale chip value.
* Missing mid-levels prefer the next higher advertised value (Gemini 3 Pro medium → high)
* so the chip, persisted pref, and request payload stay aligned.
*/
export function resolveVisibleCattyThinkingLevel(
levels: readonly string[],
selected: string | undefined,
): string | undefined {
if (!levels.length) return undefined;
const raw = selected?.trim().toLowerCase();
if (raw && levels.includes(raw)) return raw;
if (raw === 'off') {
if (levels.includes('minimal')) return 'minimal';
return levels[0];
}
const selectedRank = raw ? REASONING_RANK.indexOf(raw as typeof REASONING_RANK[number]) : -1;
if (selectedRank < 0) return levels[0];
for (let i = selectedRank + 1; i < REASONING_RANK.length; i += 1) {
if (levels.includes(REASONING_RANK[i])) return REASONING_RANK[i];
}
for (let i = selectedRank - 1; i >= 0; i -= 1) {
if (levels.includes(REASONING_RANK[i])) return REASONING_RANK[i];
}
return levels[0];
}
function openaiModelIsChatSnapshot(modelId: string): boolean {
return /gpt-5(?:\.\d+)?-chat/.test(modelId);
}
function isGemini3Model(modelId: string): boolean {
return modelId.trim().toLowerCase().includes('gemini-3');
}
function gemini3AdvertisedLevels(modelId: string): readonly string[] {
const id = modelId.trim().toLowerCase();
if (/flash-lite-image/.test(id)) return LEVELS_MINIMAL_HIGH;
if (/gemini-3\.7/.test(id) && /flash/.test(id)) return LEVELS_LOW_MEDIUM_HIGH;
if (/pro/.test(id) && !/flash/.test(id)) {
return /gemini-3\.1/.test(id) ? LEVELS_LOW_MEDIUM_HIGH : LEVELS_LOW_HIGH;
}
if (/flash/.test(id)) return LEVELS_MINIMAL_LOW_MEDIUM_HIGH;
return LEVELS_LOW_MEDIUM_HIGH;
}
function googleModelAllowsDisabledThinking(modelId: string): boolean {
const id = modelId.trim().toLowerCase();
return /flash-lite|flash/.test(id) && !/pro/.test(id);
}
type ClaudeModel = {
family: string;
major: number;
minor: number;
};
function parseClaudeMinor(raw: string | undefined): number {
if (!raw) return 0;
const minor = Number.parseInt(raw, 10);
// Date suffixes like 20250514 are not minor versions.
return Number.isFinite(minor) && minor < 100 ? minor : 0;
}
function parseClaudeModel(modelId: string): ClaudeModel | null {
const id = modelId.trim().toLowerCase();
if (!id) return null;
const familyMatch = id.match(
/claude-(opus|sonnet|haiku|fable|mythos)(?:-preview)?(?:[-.](\d+)(?:[-.](\d+))?)?/,
);
if (familyMatch) {
return {
family: familyMatch[1],
major: familyMatch[2] ? Number.parseInt(familyMatch[2], 10) : 5,
minor: parseClaudeMinor(familyMatch[3]),
};
}
const threeSeven = id.match(/claude-3[-.]7/);
if (threeSeven) return { family: 'sonnet', major: 3, minor: 7 };
const bare = id.match(/claude-(\d+)(?:[-.](\d+))?/);
if (bare) {
return {
family: 'claude',
major: Number.parseInt(bare[1], 10),
minor: parseClaudeMinor(bare[2]),
};
}
return null;
}
/** 4.6+ and Claude 5 reject or deprecate manual `type: "enabled"`. */
function anthropicUsesAdaptiveThinking(modelId: string): boolean {
const parsed = parseClaudeModel(modelId);
if (!parsed) return false;
if (parsed.major >= 5) return true;
return parsed.major === 4 && parsed.minor >= 6;
}
/** Fable 5 / Mythos 5 are always-on and reject `thinking.type: "disabled"`. */
function anthropicAllowsDisabledThinking(modelId: string): boolean {
const parsed = parseClaudeModel(modelId);
if (!parsed) return true;
return parsed.family !== 'fable' && parsed.family !== 'mythos';
}