Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions apps/desktop/src/main/__tests__/connections-ipc-main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,76 @@ describe('connection IPC credential boundary', () => {
assert.equal(persistedPatch?.baseUrl, 'https://chatgpt.com/backend-api/codex');
});

test('create passes relay model profiles through to the store unchanged', async () => {
let persistedInput: CreateConnectionInput | undefined;
const handlers = registerHandlers({
connectionStore: {
create: async (input: CreateConnectionInput) => {
persistedInput = input;
return {
...input,
defaultModel: 'my-reasoning-model',
enabled: true,
createdAt: 1,
updatedAt: 1,
};
},
remove: async () => {},
},
});

const create = handlers.get('connections:create');
assert.ok(create);
const relayModelProfiles = {
'my-reasoning-model': {
thinkingLevels: ['low', 'medium', 'high', 'max'],
vision: true,
},
} as const;
await create({}, {
slug: 'my-relay',
name: 'My Relay',
providerType: 'openai-compatible',
baseUrl: 'https://relay.example/v1',
defaultModel: 'my-reasoning-model',
relayModelProfiles,
});
assert.deepEqual(persistedInput?.relayModelProfiles, relayModelProfiles);
assert.equal(persistedInput?.extras, undefined);
});

test('update passes relay model profiles through to the store unchanged', async () => {
let persistedPatch: UpdateConnectionInput | undefined;
const existing: LlmConnection = {
slug: 'my-relay',
name: 'My Relay',
providerType: 'openai-compatible',
baseUrl: 'https://relay.example/v1',
defaultModel: 'my-reasoning-model',
enabled: true,
createdAt: 1,
updatedAt: 1,
};
const handlers = registerHandlers({
connectionStore: {
get: async () => existing,
update: async (_slug: string, patch: UpdateConnectionInput) => {
persistedPatch = patch;
return { ...existing, ...patch };
},
},
});

const update = handlers.get('connections:update');
assert.ok(update);
const relayModelProfiles = {
'my-reasoning-model': { thinkingLevels: ['low', 'high', 'max'] },
} as const;
await update({}, existing.slug, { relayModelProfiles });
assert.deepEqual(persistedPatch?.relayModelProfiles, relayModelProfiles);
assert.equal(persistedPatch?.extras, undefined);
});

test('hasSecret uses the read-only credential probe', async () => {
const connection = {
slug: 'openai-codex',
Expand Down
49 changes: 49 additions & 0 deletions apps/desktop/src/main/__tests__/relay-profile-draft.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
relayProfileDraftReseedPlan,
relayProfileDraftSeed,
} from '../../renderer/settings/relay-profile-draft.js';

test('a clean draft reseeds on every reload of its own connection', () => {
assert.deepEqual(relayProfileDraftReseedPlan({ slug: 'relay-a', dirty: false }, 'relay-a'), {
reseed: true,
clearDirty: false,
});
});

test('a dirty draft survives same-connection reloads', () => {
assert.deepEqual(relayProfileDraftReseedPlan({ slug: 'relay-a', dirty: true }, 'relay-a'), {
reseed: false,
clearDirty: false,
});
});

test('a connection switch reseeds regardless of unsaved edits — and owns the result', () => {
// Dirty belongs to the slug that produced it: A's unsaved declarations
// must neither render under B nor be saved into B.
for (const dirty of [true, false]) {
assert.deepEqual(relayProfileDraftReseedPlan({ slug: 'relay-a', dirty }, 'relay-b'), {
reseed: true,
clearDirty: true,
});
}
});

test('the draft seed sanitizes a hand-edited saved table', () => {
// Runtime reads sanitize through relayModelProfile; the editor must show
// the same canonical view — a malformed local file degrades to no
// declaration, not to UI state TypeScript does not model.
assert.deepEqual(
relayProfileDraftSeed({
reasoner: { thinkingLevels: 'low' as never, contextWindow: '128000' as never },
ghost: { thinkingLevels: ['off', 'low'] },
visual: { vision: true },
}),
{
ghost: { thinkingLevels: ['low'] },
visual: { vision: true },
},
);
assert.deepEqual(relayProfileDraftSeed(undefined), {});
});
106 changes: 106 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-config-ipc-main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import type {
ConnectionCatalogEntry,
ConnectionCatalogSnapshot,
} from '@maka/core/runtime-policy';
import type { LlmConnection } from '@maka/core/llm-connections';
import { saveConnection } from '../runtime-host-config-ipc-main.js';

function existingConnection(overrides: Partial<ConnectionCatalogEntry> = {}): ConnectionCatalogEntry {
return {
connectionId: 'connection-1',
revision: 4,
slug: 'my-relay',
name: 'Relay',
providerType: 'openai-compatible',
baseUrl: 'https://relay.example/v1',
enabled: true,
enabledModelIds: ['model-1'],
models: [{ id: 'model-1' }],
...overrides,
};
}

function snapshot(connections: ConnectionCatalogEntry[]): ConnectionCatalogSnapshot {
return { revision: 7, defaultTarget: null, connections };
}

function fakeClient(existing: ConnectionCatalogEntry) {
const updatePatches: Record<string, unknown>[] = [];
let connections = [existing];
const client = {
async loadConnectionCatalog() {
return snapshot(connections);
},
async updateConnection(
expected: { connectionId: string; revision: number },
patch: Record<string, unknown>,
) {
updatePatches.push(patch);
connections = [
{
...existing,
revision: existing.revision + 1,
...(patch.relayModelProfiles === null || patch.relayModelProfiles === undefined
? {}
: {
relayModelProfiles: patch.relayModelProfiles as ConnectionCatalogEntry['relayModelProfiles'],
}),
},
];
return { kind: 'committed' as const };
},
};
return { client, updatePatches };
}

test('import overwrite with a profile-free snapshot CLEARS existing relay profiles', async () => {
// Importing is snapshot replacement: the Host update contract treats an
// ABSENT relayModelProfiles as "untouched", which would resurrect the old
// declarations after a "no profiles here" backup.
const { client, updatePatches } = fakeClient(
existingConnection({ relayModelProfiles: { 'model-1': { vision: true } } }),
);
const incoming: LlmConnection = {
slug: 'my-relay',
name: 'Relay',
providerType: 'openai-compatible',
baseUrl: 'https://relay.example/v1',
defaultModel: 'model-1',
enabled: true,
enabledModelIds: ['model-1'],
createdAt: 0,
updatedAt: 0,
};

await saveConnection(client as never, incoming);

assert.equal(updatePatches.length, 1);
assert.equal(updatePatches[0]?.relayModelProfiles, null);
});

test('import overwrite with profiles REPLACES the existing table', async () => {
const { client, updatePatches } = fakeClient(
existingConnection({ relayModelProfiles: { 'model-1': { vision: true } } }),
);
const incoming: LlmConnection = {
slug: 'my-relay',
name: 'Relay',
providerType: 'openai-compatible',
baseUrl: 'https://relay.example/v1',
defaultModel: 'model-1',
enabled: true,
enabledModelIds: ['model-1'],
createdAt: 0,
updatedAt: 0,
relayModelProfiles: { 'model-1': { contextWindow: 64_000 } },
};

await saveConnection(client as never, incoming);

assert.equal(updatePatches.length, 1);
assert.deepEqual(updatePatches[0]?.relayModelProfiles, {
'model-1': { contextWindow: 64_000 },
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@ test('imports a local GitHub credential through the shared Host account path', a
connectionId: current.connectionId,
revision: current.revision,
});
const { relayModelProfiles, ...restChanges } = changes;
const updated: ConnectionCatalogEntry = {
...current,
...changes,
...restChanges,
...(relayModelProfiles === null ? {} : { relayModelProfiles }),
revision: current.revision + 1,
};
catalog = {
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/main/connections-ipc-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type UpdateConnectionInput,
} from '@maka/core';
import { PROVIDER_DEFAULTS } from '@maka/core/llm-connections';
import { normalizeRelayModelProfiles } from '@maka/core/model-thinking';

const IPC_CONNECTION_SLUG_MAX_LENGTH = 64;
const IPC_CONNECTION_SECRET_MAX_LENGTH = 4096;
Expand Down Expand Up @@ -51,10 +52,15 @@ export function normalizeCreateConnectionInputForIpc(value: unknown): CreateConn
? undefined
: normalizeConnectionApiKeyForIpc(input.apiKey, 'apiKey');
const slug = normalizeConnectionSlugForIpc(input.slug, 'connection slug');
const relayModelProfiles =
input.relayModelProfiles === undefined
? undefined
: normalizeRelayModelProfiles(input.relayModelProfiles);
const normalized = {
...input,
slug,
...(apiKey === undefined ? {} : { apiKey }),
...(relayModelProfiles === undefined ? {} : { relayModelProfiles }),
} as CreateConnectionInput;
return normalizeConnectionBaseUrlForIpc(normalized);
}
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/main/desktop-backend-tool-surface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
activePlanExecution,
relayModelProfile,
DEFAULT_SESSION_NAME,
defaultWebSearchSettings,
isDeepResearchSession,
Expand Down Expand Up @@ -348,7 +349,12 @@ function replaceParentAgentTools(
}

function modelSupportsVision(connection: LlmConnection, model: string): boolean {
return resolveModelVisionSupport(connection.providerType, connection.models, model);
return resolveModelVisionSupport(
connection.providerType,
connection.models,
model,
relayModelProfile(connection, model)?.vision,
);
}

function resolveDurableChildTools(
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/runtime-host-account-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ function accountConnectionChanges(
...(connection.baseUrl === undefined ? {} : { baseUrl: connection.baseUrl }),
enabled,
enabledModelIds: [...enabledModelIds],
// Account (OAuth) connections never declare relay capabilities, and the
// omission is the point: with tri-state update semantics an absent key
// leaves the stored table untouched, so this path can never clobber
// declarations another writer made.
};
}

Expand Down
10 changes: 9 additions & 1 deletion apps/desktop/src/main/runtime-host-config-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ function runtimeHostTransferDeps(
};
}

async function saveConnection(
// Exported for the import-overwrite tests: this adapter is where snapshot
// semantics meet the Host's tri-state update contract.
export async function saveConnection(
client: DesktopRuntimeHostClient,
connection: LlmConnection,
): Promise<LlmConnection> {
Expand All @@ -198,19 +200,25 @@ async function saveConnection(
...(connection.baseUrl ? { baseUrl: connection.baseUrl } : {}),
enabled: connection.enabled,
enabledModelIds: [...(connection.enabledModelIds ?? [])],
// Import-overwrite is snapshot replacement: absent in the snapshot
// must CLEAR, not inherit — the update contract's "absent means
// untouched" would otherwise resurrect the old profiles.
relayModelProfiles: connection.relayModelProfiles ?? null,
},
);
if (updated.kind !== 'committed') {
throw new Error(`Unable to update imported Connection: ${updated.kind}`);
}
} else {
const importedProfiles = connection.relayModelProfiles;
const created = await client.createConnection(catalog.revision, {
slug: connection.slug,
name: connection.name,
providerType: connection.providerType,
...(connection.baseUrl ? { baseUrl: connection.baseUrl } : {}),
enabled: connection.enabled,
enabledModelIds: [...(connection.enabledModelIds ?? [])],
...(importedProfiles === undefined ? {} : { relayModelProfiles: importedProfiles }),
});
if (created.kind !== 'committed') {
throw new Error(`Unable to create imported Connection: ${created.kind}`);
Expand Down
14 changes: 14 additions & 0 deletions apps/desktop/src/main/runtime-host-connections-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
PROVIDER_DEFAULTS,
providerAuthRequiresSecret,
} from '@maka/core/llm-connections';
import { normalizeRelayModelProfiles } from '@maka/core/model-thinking';
import type {
ConnectionCatalogEntry,
ConnectionCatalogSnapshot,
Expand Down Expand Up @@ -86,13 +87,17 @@ export function registerRuntimeHostConnectionsIpc(
deps.ipcMain.handle('connections:create', async (_event, raw: unknown) => {
const input = normalizeCreateInput(raw);
const catalog = await snapshot();
// Profiles ride as the typed field end to end — nothing free-form
// crosses to the host.
const relayModelProfiles = input.relayModelProfiles;
const created = await deps.client.createConnection(catalog.revision, {
slug: input.slug,
name: input.name,
providerType: input.providerType,
...(input.baseUrl === undefined ? {} : { baseUrl: input.baseUrl }),
enabled: true,
enabledModelIds: input.defaultModel ? [input.defaultModel] : [],
...(relayModelProfiles === undefined ? {} : { relayModelProfiles }),
});
if (created.kind !== 'committed') {
throw new Error(`Unable to create Connection: ${created.kind}`);
Expand Down Expand Up @@ -129,6 +134,12 @@ export function registerRuntimeHostConnectionsIpc(
: { baseUrl: patch.baseUrl }),
enabled: patch.enabled ?? current.enabled,
enabledModelIds: patch.enabledModelIds ?? current.enabledModelIds,
// Tri-state: a patch that mentions profiles re-normalizes them (empty
// normalization = clear); a patch without profiles omits the key
// entirely, which the store reads as "leave the table alone".
...(patch.relayModelProfiles === undefined
? {}
: { relayModelProfiles: normalizeRelayModelProfiles(patch.relayModelProfiles) ?? null }),
},
);
if (updated.kind !== 'committed') {
Expand Down Expand Up @@ -229,6 +240,9 @@ export function projectHostConnections(catalog: ConnectionCatalogSnapshot): LlmC
defaultModel,
enabledModelIds: [...connection.enabledModelIds],
models: [...connection.models],
...(connection.relayModelProfiles === undefined
? {}
: { relayModelProfiles: connection.relayModelProfiles }),
...(connection.modelSource === undefined ? {} : { modelSource: connection.modelSource }),
...(connection.modelsFetchedAt === undefined
? {}
Expand Down
Loading
Loading