[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,706 @@
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createGoogle } from '@ai-sdk/google';
import type { ProviderConfig, ProviderStyle } from '../types';
import { resolveOpenAIApi, resolveProviderStyle } from '../types';
import { normalizeAnthropicSdkBaseURL } from '../anthropicCompatBaseUrl';
import { normalizeOllamaSdkBaseURL } from '../ollamaCompatBaseUrl';
export { normalizeOllamaSdkBaseURL };
import {
applyOpenAIChatContinuationToBody,
extractProviderContinuationFromRawChunk,
mergeProviderContinuation,
rawOpenAIChatChunkHasToolCalls,
repairOpenAIChatToolResultPairsInBody,
type OpenAIChatAssistantFields,
} from '../providerContinuation';
export interface ProviderRequestContext {
getOpenAIChatAssistantFields?: () => Array<OpenAIChatAssistantFields | undefined>;
streamIdleTimeoutMs?: number;
}
/**
* Bridge API subset used for SDK fetch adapter.
*/
interface BridgeAPI {
aiFetch(
url: string,
method: string,
headers: Record<string, string>,
body?: string,
providerId?: string,
): Promise<{
ok: boolean;
status: number;
data: string;
error?: string;
}>;
aiChatStream(
requestId: string,
url: string,
headers: Record<string, string>,
body: string,
providerId?: string,
idleTimeoutMs?: number,
): Promise<{ ok: boolean; statusCode?: number; statusText?: string; error?: string; aborted?: boolean }>;
onAiStreamData(requestId: string, cb: (data: string) => void): () => void;
onAiStreamEnd(requestId: string, cb: () => void): () => void;
onAiStreamError(requestId: string, cb: (error: string) => void): () => void;
aiChatCancel(requestId: string): Promise<boolean>;
}
function getBridge(): BridgeAPI | null {
const w = window as unknown as { netcatty?: BridgeAPI };
return w.netcatty ?? null;
}
/**
* Detect whether a request is likely a streaming request.
* AI SDK streaming requests use POST with `"stream": true` in the body.
*/
function isStreamingRequest(init?: RequestInit): boolean {
if (!init?.body) return false;
try {
const bodyStr = typeof init.body === 'string' ? init.body : null;
if (!bodyStr) return false;
const parsed = JSON.parse(bodyStr);
return parsed.stream === true;
} catch {
return false;
}
}
function mergeOpenAIChatAssistantFields(
current: OpenAIChatAssistantFields | undefined,
incoming: OpenAIChatAssistantFields | undefined,
): OpenAIChatAssistantFields | undefined {
return mergeProviderContinuation(
{ openAIChatAssistantFields: current },
{ openAIChatAssistantFields: incoming },
)?.openAIChatAssistantFields;
}
function createOpenAIChatStreamFieldCapture(
requestContext?: ProviderRequestContext,
): (data: string) => void {
const assistantFields = requestContext?.getOpenAIChatAssistantFields?.();
if (!assistantFields) return () => undefined;
let streamFieldIndex: number | undefined;
let pendingFields: OpenAIChatAssistantFields | undefined;
const ensureStreamFieldSlot = (): number => {
if (streamFieldIndex !== undefined) return streamFieldIndex;
streamFieldIndex = assistantFields.length;
assistantFields.push(undefined);
return streamFieldIndex;
};
const flushPendingFields = (fieldIndex: number) => {
if (!pendingFields) return;
assistantFields[fieldIndex] = mergeOpenAIChatAssistantFields(
assistantFields[fieldIndex],
pendingFields,
);
pendingFields = undefined;
};
return (data: string) => {
const continuation = extractProviderContinuationFromRawChunk(data);
const fields = continuation?.openAIChatAssistantFields;
if (fields) {
pendingFields = mergeOpenAIChatAssistantFields(pendingFields, fields);
if (streamFieldIndex !== undefined) {
flushPendingFields(streamFieldIndex);
}
}
if (rawOpenAIChatChunkHasToolCalls(data)) {
flushPendingFields(ensureStreamFieldSlot());
}
};
}
function createOpenAIChatToolCallNormalizer(requestId: string): (data: string) => string {
const toolCallIdsByChoiceAndIndex = new Map<string, string>();
const toolCallNamesByChoiceAndIndex = new Map<string, string>();
const pendingToolCallsByChoiceAndIndex = new Map<string, Record<string, unknown>>();
// Full concatenation of every arguments fragment seen for a tool call key,
// so a tool call the SDK has not seen yet can be forwarded self-describing.
const argumentsSeenByKey = new Map<string, string>();
// Keys forwarded inside choices array position 0 — the only element the
// AI SDK reads. Tool calls living at position > 0 are invisible to it.
const sdkVisibleKeys = new Set<string>();
const requestIdToken = requestId.replace(/[^a-zA-Z0-9_-]/g, '_');
return (data: string): string => {
if (!data || data.trim() === '[DONE]') return data;
let parsed: unknown;
try {
parsed = JSON.parse(data);
} catch {
return data;
}
if (!parsed || typeof parsed !== 'object' || !Array.isArray((parsed as Record<string, unknown>).choices)) {
return data;
}
let changed = false;
const normalizedChoices = ((parsed as Record<string, unknown>).choices as unknown[]).map((choice, choicePosition) => {
if (!choice || typeof choice !== 'object') return choice;
const choiceRecord = choice as Record<string, unknown>;
const delta = choiceRecord.delta;
if (!delta || typeof delta !== 'object') return choice;
const deltaRecord = delta as Record<string, unknown>;
if (!Array.isArray(deltaRecord.tool_calls)) return choice;
const choiceIndex = typeof choiceRecord.index === 'number' ? choiceRecord.index : choicePosition;
let deltaChanged = false;
const normalizedToolCalls: unknown[] = [];
for (const [toolCallPosition, toolCall] of deltaRecord.tool_calls.entries()) {
if (!toolCall || typeof toolCall !== 'object') {
normalizedToolCalls.push(toolCall);
continue;
}
const toolCallRecord = toolCall as Record<string, unknown>;
const toolCallIndex = typeof toolCallRecord.index === 'number' ? toolCallRecord.index : toolCallPosition;
const key = `${choiceIndex}:${toolCallIndex}`;
const recordFn = toolCallRecord.function;
const recordArguments = recordFn && typeof recordFn === 'object'
? (recordFn as Record<string, unknown>).arguments
: undefined;
if (typeof recordArguments === 'string') {
argumentsSeenByKey.set(key, (argumentsSeenByKey.get(key) ?? '') + recordArguments);
}
const existingId = toolCallIdsByChoiceAndIndex.get(key);
const pendingToolCall = pendingToolCallsByChoiceAndIndex.get(key);
const candidateToolCall = pendingToolCall
? mergeOpenAIChatToolCallDeltas(pendingToolCall, toolCallRecord)
: toolCallRecord;
if (existingId) {
const rememberedName = toolCallNamesByChoiceAndIndex.get(key);
const needsFullArguments = choicePosition === 0 && !sdkVisibleKeys.has(key);
let normalizedToolCall = normalizeOpenAIChatToolCall(
toolCallRecord,
existingId,
rememberedName,
);
if (needsFullArguments) {
normalizedToolCall = injectFullOpenAIChatToolCallArguments(
normalizedToolCall,
argumentsSeenByKey.get(key),
);
}
if (choicePosition === 0) {
sdkVisibleKeys.add(key);
}
if (
normalizedToolCall.id === toolCallRecord.id &&
normalizedToolCall.type === toolCallRecord.type &&
normalizedToolCall.function === toolCallRecord.function &&
(!rememberedName || hasFunctionName(toolCallRecord))
) {
normalizedToolCalls.push(toolCall);
} else {
changed = true;
deltaChanged = true;
normalizedToolCalls.push(normalizedToolCall);
}
continue;
}
if (!hasFunctionName(candidateToolCall)) {
pendingToolCallsByChoiceAndIndex.set(key, candidateToolCall);
changed = true;
deltaChanged = true;
continue;
}
const toolCallId = typeof candidateToolCall.id === 'string' && candidateToolCall.id
? candidateToolCall.id
: `call_netcatty_${requestIdToken}_${choiceIndex}_${toolCallIndex}`;
toolCallIdsByChoiceAndIndex.set(key, toolCallId);
const candidateFunction = candidateToolCall.function;
if (candidateFunction && typeof candidateFunction === 'object') {
toolCallNamesByChoiceAndIndex.set(
key,
(candidateFunction as Record<string, unknown>).name as string,
);
}
pendingToolCallsByChoiceAndIndex.delete(key);
let normalizedToolCall = normalizeOpenAIChatToolCall(candidateToolCall, toolCallId);
if (choicePosition === 0 && !sdkVisibleKeys.has(key)) {
normalizedToolCall = injectFullOpenAIChatToolCallArguments(
normalizedToolCall,
argumentsSeenByKey.get(key),
);
}
if (choicePosition === 0) {
sdkVisibleKeys.add(key);
}
if (
candidateToolCall === toolCallRecord &&
toolCallId === toolCallRecord.id &&
normalizedToolCall.type === toolCallRecord.type &&
normalizedToolCall.function === toolCallRecord.function
) {
normalizedToolCalls.push(toolCall);
continue;
}
changed = true;
deltaChanged = true;
normalizedToolCalls.push(normalizedToolCall);
}
if (!deltaChanged) return choice;
return {
...choiceRecord,
delta: {
...deltaRecord,
tool_calls: normalizedToolCalls,
},
};
});
if (!changed) return data;
return JSON.stringify({
...(parsed as Record<string, unknown>),
choices: normalizedChoices,
});
};
}
function mergeOpenAIChatToolCallDeltas(
current: Record<string, unknown>,
incoming: Record<string, unknown>,
): Record<string, unknown> {
const currentFn = current.function;
const incomingFn = incoming.function;
const currentFunction = currentFn && typeof currentFn === 'object'
? currentFn as Record<string, unknown>
: undefined;
const incomingFunction = incomingFn && typeof incomingFn === 'object'
? incomingFn as Record<string, unknown>
: undefined;
const mergedFunction: Record<string, unknown> = {
...(currentFunction ?? {}),
...(incomingFunction ?? {}),
};
if (
typeof currentFunction?.name === 'string' &&
currentFunction.name &&
incomingFunction?.name === ''
) {
mergedFunction.name = currentFunction.name;
}
const currentArgs = currentFunction?.arguments;
const incomingArgs = incomingFunction?.arguments;
if (typeof currentArgs === 'string' && typeof incomingArgs === 'string') {
mergedFunction.arguments = currentArgs + incomingArgs;
}
return {
...current,
...incoming,
function: mergedFunction,
};
}
function normalizeOpenAIChatToolCall(
toolCall: Record<string, unknown>,
toolCallId: string,
rememberedName?: string,
): Record<string, unknown> {
const normalized = { ...toolCall, id: toolCallId };
if (
normalized.type === '' ||
normalized.type == null ||
(typeof normalized.type === 'string' && normalized.type !== 'function')
) {
normalized.type = 'function';
}
const fn = normalized.function;
if (fn && typeof fn === 'object') {
const fnRecord = fn as Record<string, unknown>;
if (!hasFunctionName({ function: fnRecord })) {
const normalizedFunction = { ...fnRecord };
if (rememberedName) {
normalizedFunction.name = rememberedName;
} else if (fnRecord.name === '' || fnRecord.name === null) {
delete normalizedFunction.name;
}
normalized.function = normalizedFunction;
}
} else if (rememberedName) {
normalized.function = { name: rememberedName };
}
return normalized;
}
function injectFullOpenAIChatToolCallArguments(
toolCall: Record<string, unknown>,
fullArguments: string | undefined,
): Record<string, unknown> {
if (fullArguments === undefined) return toolCall;
const fn = toolCall.function;
const fnRecord = fn && typeof fn === 'object' ? fn as Record<string, unknown> : undefined;
if (fnRecord?.arguments === fullArguments) return toolCall;
return {
...toolCall,
function: { ...(fnRecord ?? {}), arguments: fullArguments },
};
}
function hasFunctionName(toolCall: Record<string, unknown>): boolean {
const fn = toolCall.function;
return Boolean(
fn &&
typeof fn === 'object' &&
typeof (fn as Record<string, unknown>).name === 'string' &&
(fn as Record<string, unknown>).name,
);
}
/**
* Extract headers as a plain Record<string, string> from various header formats.
*/
function extractHeaders(headers?: HeadersInit): Record<string, string> {
const result: Record<string, string> = {};
if (!headers) return result;
if (headers instanceof Headers) {
headers.forEach((value, key) => {
result[key] = value;
});
} else if (Array.isArray(headers)) {
for (const [key, value] of headers) {
result[key] = value;
}
} else {
Object.assign(result, headers);
}
return result;
}
/**
* Create a fetch function compatible with the Vercel AI SDK that routes
* requests through the Electron IPC bridge to avoid CORS.
*
* - Non-streaming requests: uses `window.netcatty.aiFetch()` and returns a `Response`.
* - Streaming requests: uses `window.netcatty.aiChatStream()` and returns a
* `Response` with a `ReadableStream` body.
* - Falls back to `globalThis.fetch` if the bridge is unavailable.
*/
/** Placeholder API key used by the renderer; main process replaces it with the real key. */
export const API_KEY_PLACEHOLDER = '__IPC_SECURED__';
function toSafeStatusText(message: string, fallback: string): string {
const normalized = message
.replace(/[\r\n\t]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (!normalized) return fallback;
const byteStringSafe = Array.from(normalized, (char) => {
const code = char.charCodeAt(0);
if (code < 0x20 || code === 0x7f || code > 0xff) return '?';
return char;
}).join('');
return byteStringSafe.slice(0, 120) || fallback;
}
export function createBridgeFetchForSDK(
providerId?: string,
requestContext?: ProviderRequestContext,
): typeof globalThis.fetch {
return async (
input: string | URL | Request,
init?: RequestInit,
): Promise<Response> => {
const bridge = getBridge();
if (!bridge) {
return globalThis.fetch(input, init);
}
// Resolve URL string
let url: string;
let resolvedInit = init;
if (input instanceof Request) {
url = input.url;
// Merge Request properties with init overrides
if (!resolvedInit) {
resolvedInit = {
method: input.method,
headers: extractHeaders(input.headers),
body: input.body ? await new Response(input.body).text() : undefined,
};
}
} else {
url = input instanceof URL ? input.toString() : input;
}
const method = resolvedInit?.method || 'GET';
const headers = extractHeaders(resolvedInit?.headers);
const body =
resolvedInit?.body != null ? String(resolvedInit.body) : undefined;
const requestBody = body != null
? repairOpenAIChatToolResultPairsInBody(applyOpenAIChatContinuationToBody(
body,
requestContext?.getOpenAIChatAssistantFields?.() ?? [],
))
: undefined;
// Streaming path
if (isStreamingRequest(resolvedInit)) {
const requestId = `sdk_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const captureOpenAIChatFields = createOpenAIChatStreamFieldCapture(requestContext);
const normalizeOpenAIChatToolCalls = createOpenAIChatToolCallNormalizer(requestId);
// Set up IPC event listeners BEFORE starting the stream to avoid
// missing early events.
const encoder = new TextEncoder();
let streamController: ReadableStreamDefaultController<Uint8Array> | null = null;
const pendingChunks: Uint8Array[] = [];
let pendingClose = false;
let pendingError: Error | null = null;
let cleanedUp = false;
const enqueueChunk = (chunk: Uint8Array) => {
if (streamController) {
streamController.enqueue(chunk);
return;
}
pendingChunks.push(chunk);
};
const closeStream = () => {
if (streamController) {
try { streamController.close(); } catch { /* already closed */ }
return;
}
pendingClose = true;
};
const errorStream = (error: Error) => {
if (streamController) {
try { streamController.error(error); } catch { /* already errored */ }
return;
}
pendingError = error;
};
const flushPendingStreamEvents = (controller: ReadableStreamDefaultController<Uint8Array>) => {
for (const chunk of pendingChunks.splice(0)) {
controller.enqueue(chunk);
}
if (pendingError) {
controller.error(pendingError);
return;
}
if (pendingClose) {
controller.close();
}
};
const unsubData = bridge.onAiStreamData(requestId, (data: string) => {
const normalizedData = normalizeOpenAIChatToolCalls(data);
captureOpenAIChatFields(normalizedData);
// Re-wrap as SSE so the SDK can parse it
enqueueChunk(encoder.encode(`data: ${normalizedData}\n\n`));
});
const unsubEnd = bridge.onAiStreamEnd(requestId, () => {
closeStream();
cleanup();
});
const unsubError = bridge.onAiStreamError(
requestId,
(error: string) => {
errorStream(new Error(error));
cleanup();
},
);
const cleanup = () => {
if (cleanedUp) return;
cleanedUp = true;
unsubData();
unsubEnd();
unsubError();
};
// Handle abort
if (resolvedInit?.signal) {
resolvedInit.signal.addEventListener(
'abort',
() => {
bridge.aiChatCancel(requestId).catch(() => {});
errorStream(new DOMException('Aborted', 'AbortError'));
cleanup();
},
{ once: true },
);
}
// Start the stream — resolves once HTTP response headers arrive,
// returning the real status code.
const result = await bridge.aiChatStream(
requestId,
url,
headers,
requestBody || '',
providerId,
requestContext?.streamIdleTimeoutMs,
);
if (!result.ok) {
cleanup();
// Cancel during proxy lookup / request start must stay an AbortError,
// not a synthetic 502 that the AI SDK treats as a provider failure.
if (result.aborted || resolvedInit?.signal?.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
const errorMessage = result.error || 'Stream request failed';
const jsonBody = JSON.stringify({ error: { message: errorMessage } });
return new Response(jsonBody, {
status: 502,
statusText: toSafeStatusText(errorMessage, 'Bad Gateway'),
headers: { 'content-type': 'application/json' },
});
}
// If the server returned a non-2xx status, return the error details
// as a JSON body in OpenAI-compatible format so the AI SDK's
// failedResponseHandler can extract the message properly.
// Also set a safe ASCII statusText as fallback for non-OpenAI SDK providers.
const statusCode = result.statusCode ?? 200;
if (statusCode < 200 || statusCode >= 300) {
cleanup();
const errorDetail = result.statusText || `HTTP ${statusCode}`;
const jsonBody = JSON.stringify({ error: { message: errorDetail } });
return new Response(jsonBody, {
status: statusCode,
statusText: toSafeStatusText(errorDetail, `Error ${statusCode}`),
headers: { 'content-type': 'application/json' },
});
}
const stream = new ReadableStream<Uint8Array>({
start(controller) {
streamController = controller;
flushPendingStreamEvents(controller);
},
});
return new Response(stream, {
status: statusCode,
statusText: result.statusText ?? 'OK',
headers: { 'content-type': 'text/event-stream' },
});
}
// Non-streaming path
const result = await bridge.aiFetch(url, method, headers, requestBody, providerId);
return new Response(result.data, {
status: result.status,
statusText: result.ok ? 'OK' : 'Error',
headers: { 'content-type': 'application/json' },
});
};
}
/**
* Create a Vercel AI SDK model instance from a ProviderConfig.
*
* API keys are NOT sent to the SDK in plaintext. Instead, a placeholder
* token is used so the SDK builds proper auth headers, and the main
* process replaces the placeholder with the real decrypted key before
* making the HTTP request.
*/
/**
* Apply per-vendor URL and apiKey quirks on top of the style-based
* wire-protocol routing. Exported so it can be unit-tested without spinning
* up the Vercel AI SDK clients.
*
* The URL fallback fires regardless of style — the user picked this
* providerId for a reason, even if they overrode the wire format. The
* ollama `'ollama'` throwaway apiKey is only for unauthenticated local
* OpenAI-compat servers: Anthropic/Google need a real key, and Ollama
* Cloud must keep the IPC placeholder so the main process can inject
* the decrypted cloud key.
*/
export function resolveProviderEndpoint(
config: ProviderConfig,
style: ProviderStyle,
safeApiKey: string | undefined,
): { baseURL: string | undefined; apiKey: string | undefined } {
let baseURL = config.baseURL;
let apiKey = safeApiKey;
if (config.providerId === 'ollama') {
baseURL = normalizeOllamaSdkBaseURL(baseURL || 'http://localhost:11434/v1');
if (style === 'openai' && !apiKey) {
apiKey = 'ollama';
}
} else if (config.providerId === 'openrouter') {
baseURL = baseURL || 'https://openrouter.ai/api/v1';
}
// @ai-sdk/anthropic expects baseURL to include /v1 (then appends /messages).
// Bare Claude Code style hosts get /v1 so chat matches probe/discovery.
// Custom path prefixes (e.g. …/anthropic) are left alone — they already
// complete the SDK base and must not become …/anthropic/v1.
if (style === 'anthropic' && baseURL) {
baseURL = normalizeAnthropicSdkBaseURL(baseURL);
}
return { baseURL, apiKey };
}
export function createModelFromConfig(
config: ProviderConfig,
requestContext?: ProviderRequestContext,
) {
// Use placeholder API key — the main process will inject the real key
const safeApiKey = config.apiKey ? API_KEY_PLACEHOLDER : undefined;
const customFetch = createBridgeFetchForSDK(config.id, requestContext);
const modelId = config.defaultModel || '';
const style = resolveProviderStyle(config);
const { baseURL, apiKey } = resolveProviderEndpoint(config, style, safeApiKey);
switch (style) {
case 'openai': {
const openai = createOpenAI({
apiKey,
baseURL,
fetch: customFetch,
});
// Chat Completions stays the default so OpenAI-compatible proxies keep
// working. Responses is opt-in for relays that cache better on /v1/responses.
return resolveOpenAIApi(config) === 'responses'
? openai.responses(modelId)
: openai.chat(modelId);
}
case 'anthropic':
return createAnthropic({
apiKey,
baseURL,
fetch: customFetch,
})(modelId);
case 'google':
return createGoogle({
apiKey,
baseURL,
fetch: customFetch,
})(modelId);
default: {
const _exhaustive: never = style;
throw new Error(`Unsupported provider style: ${_exhaustive}`);
}
}
}