Skip to content
Open
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
23 changes: 17 additions & 6 deletions apps/desktop/src/main/__tests__/relay-profile-draft.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,26 @@ test('the draft seed sanitizes a hand-edited saved table', () => {
// 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 },
}),
relayProfileDraftSeed(
{
reasoner: { thinkingLevels: 'low' as never, contextWindow: '128000' as never },
ghost: { thinkingLevels: ['off', 'low'] },
visual: { vision: true },
},
'openai-compatible',
),
{
ghost: { thinkingLevels: ['low'] },
visual: { vision: true },
},
);
assert.deepEqual(relayProfileDraftSeed(undefined), {});
assert.deepEqual(relayProfileDraftSeed(undefined, 'openai-compatible'), {});
// An Anthropic-protocol relay keeps `off`: its wire has a true disable.
assert.deepEqual(
relayProfileDraftSeed(
{ ghost: { thinkingLevels: ['off', 'low'] } },
'anthropic-compatible',
),
{ ghost: { thinkingLevels: ['off', 'low'] } },
);
});
4 changes: 2 additions & 2 deletions apps/desktop/src/main/__tests__/relay-thinking-bulk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
bulkThinkingLevelStates,
relayProfileWithThinkingLevels,
} from '../../renderer/settings/relay-thinking-bulk.js';
import { DECLARABLE_RELAY_THINKING_LEVELS } from '@maka/core/model-thinking';
import { declarableRelayThinkingLevels } from '@maka/core/model-thinking';
import type { RelayModelProfile } from '@maka/core/model-thinking';

const MODELS = ['alpha', 'beta', 'gamma'];
Expand Down Expand Up @@ -63,7 +63,7 @@ test('a repeated model id is one model, not two', () => {
test('an empty selection ticks nothing rather than reading as fully covered', () => {
// 0 === 0 is the trap: `declaredCount === total` is true of an empty
// selection, which would present every level as declared everywhere.
for (const state of bulkThinkingLevelStates([], {}, DECLARABLE_RELAY_THINKING_LEVELS)) {
for (const state of bulkThinkingLevelStates([], {}, declarableRelayThinkingLevels('openai-compatible'))) {
assert.equal(state.checked, false);
assert.equal(state.total, 0);
}
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/main/connections-ipc-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,14 @@ export function normalizeCreateConnectionInputForIpc(value: unknown): CreateConn
? undefined
: normalizeConnectionApiKeyForIpc(input.apiKey, 'apiKey');
const slug = normalizeConnectionSlugForIpc(input.slug, 'connection slug');
// providerType is validated against PROVIDER_DEFAULTS above, so the
// declaration vocabulary can key off it (off is legal only where the
// provider has a true disable wire).
const providerType = input.providerType;
const relayModelProfiles =
input.relayModelProfiles === undefined
? undefined
: normalizeRelayModelProfiles(input.relayModelProfiles);
: normalizeRelayModelProfiles(input.relayModelProfiles, providerType);
const requestHeaders =
input.requestHeaders === undefined ? undefined : normalizeRequestHeaders(input.requestHeaders);
const requestBodyOverlay =
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/main/runtime-host-connections-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,11 @@ export function registerRuntimeHostConnectionsIpc(
// entirely, which the store reads as "leave the table alone".
...(patch.relayModelProfiles === undefined
? {}
: { relayModelProfiles: normalizeRelayModelProfiles(patch.relayModelProfiles) ?? null }),
: {
relayModelProfiles:
normalizeRelayModelProfiles(patch.relayModelProfiles, current.providerType) ??
null,
}),
...(patch.requestBodyOverlay === undefined
? {}
: { requestBodyOverlay: patch.requestBodyOverlay }),
Expand Down
44 changes: 23 additions & 21 deletions apps/desktop/src/renderer/settings/provider-connection-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
import { isRelayProviderType, PROVIDER_DEFAULTS } from '@maka/core/llm-connections';
import { hasModelMetadata } from '@maka/core/model-metadata';
import {
DECLARABLE_RELAY_THINKING_LEVELS,
declarableRelayThinkingLevels,
THINKING_LEVELS,
supportsRelayFastServiceTier,
type RelayModelProfile,
Expand Down Expand Up @@ -631,15 +631,16 @@ function ConnectionDetailInner(props: ConnectionDetailProps) {
hasChevron
menuWidth={240}
>
{/* The declarable vocabulary, which is the whole of what
a draft can hold: the seed sanitizes through
`normalizeRelayModelProfiles`, so `off` — a disable
wire no generic relay is presumed to speak — cannot
reach a row here either. */}
{/* The per-provider declarable vocabulary, which is the
whole of what a draft can hold: the seed sanitizes
through `normalizeRelayModelProfiles` with the same
provider, so `off` — legal only where the provider
has a true disable wire — cannot reach an OpenAI
relay row here either. */}
{bulkThinkingLevelStates(
capabilityModelIds,
relayProfileDraft,
DECLARABLE_RELAY_THINKING_LEVELS,
declarableRelayThinkingLevels(connection.providerType),
).map((state) => (
<DropdownMenuCheckboxItem
key={state.level}
Expand Down Expand Up @@ -689,16 +690,15 @@ function ConnectionDetailInner(props: ConnectionDetailProps) {
connection.providerType,
modelId,
);
// The menu offers the five declarable levels PLUS anything
// the stored table already claims — a level saved while it
// was still declarable (or hand-written into the document)
// must stay visible and un-checkable, never an invisible
// selection the trigger counts but the menu cannot show.
// The menu offers the provider's declarable levels PLUS
// anything the stored table already claims — a level saved
// while it was still declarable (or hand-written into the
// document) must stay visible and un-checkable, never an
// invisible selection the trigger counts but the menu
// cannot show.
const declarableLevels = declarableRelayThinkingLevels(connection.providerType);
const menuLevels: readonly ThinkingLevel[] = THINKING_LEVELS.filter(
(level) =>
(DECLARABLE_RELAY_THINKING_LEVELS as readonly ThinkingLevel[]).includes(
level,
) || draftLevels.includes(level),
(level) => declarableLevels.includes(level) || draftLevels.includes(level),
);
return (
<VStack key={modelId} gap={3}>
Expand All @@ -708,11 +708,13 @@ function ConnectionDetailInner(props: ConnectionDetailProps) {
left, one compact control on the right (the 模型功能
row language). A CheckboxList wall was the reason this
section looked like a form from a different app. */}
{/* Relay-only, like 快速模式 below: a declared level encodes
into `reasoning_effort`, a wire field only the
OpenAI-compatible relays accept. The catalog codec
refuses to persist one elsewhere, so offering the
control would promise an edit that cannot be saved. */}
{/* Relay-only, like 快速模式 below: a declared level
encodes into the relay's thinking wire —
`reasoning_effort` on the OpenAI relays, the
Anthropic `thinking`/`effort` controls on the
Anthropic-protocol relay. The catalog codec refuses
to persist one elsewhere, so offering the control
would promise an edit that cannot be saved. */}
{isRelay && (
<CapabilityRow label={copy.thinkingEffort} description={copy.thinkingEffortHelp}>
{/* DropdownMenu, not MultiSelector: levels have a
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/renderer/settings/relay-profile-draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
type RelayModelProfile,
type RelayModelProfiles,
} from '@maka/core/model-thinking';
import type { ProviderType } from '@maka/core/llm-connections';

/**
* Reseed decision for the relay-profile editor's local draft. The editor is
Expand Down Expand Up @@ -66,6 +67,7 @@ export function relayProfileDraftReseedPlan(
*/
export function relayProfileDraftSeed(
profiles: RelayModelProfiles | undefined,
providerType: ProviderType,
): Record<string, RelayModelProfile> {
return normalizeRelayModelProfiles(profiles) ?? {};
return normalizeRelayModelProfiles(profiles, providerType) ?? {};
}
8 changes: 6 additions & 2 deletions apps/desktop/src/renderer/settings/use-connection-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ export function useConnectionDetail(props: ConnectionDetailProps) {
// removes its unsaved declaration too (the store prunes the SAVED table the
// same way on write).
const [relayProfileDrafts, setRelayProfileDrafts] = useState<Record<string, RelayModelProfile>>(
() => relayProfileDraftSeed(connection.relayModelProfiles),
() => relayProfileDraftSeed(connection.relayModelProfiles, connection.providerType),
);
const [relayProfilesDirty, setRelayProfilesDirty] = useState(false);
// The dirty flag names a slug: the same instance continues across the
Expand Down Expand Up @@ -465,9 +465,11 @@ export function useConnectionDetail(props: ConnectionDetailProps) {
// path applies, so a reordered-but-equal draft doesn't keep 保存 lit.
const savedRelayProfiles = normalizeRelayModelProfiles(
pruneRelayModelProfiles(connection.relayModelProfiles, enabledModelIds) ?? {},
connection.providerType,
);
const draftedRelayProfiles = normalizeRelayModelProfiles(
pruneRelayModelProfiles(relayProfileDrafts, enabledModelIds) ?? {},
connection.providerType,
);
const hasRelayProfileChanges = !relayProfilesEqual(draftedRelayProfiles, savedRelayProfiles);

Expand All @@ -482,7 +484,9 @@ export function useConnectionDetail(props: ConnectionDetailProps) {
);
relayProfileDraftOwnerRef.current = connection.slug;
if (plan.reseed) {
setRelayProfileDrafts(relayProfileDraftSeed(connection.relayModelProfiles));
setRelayProfileDrafts(
relayProfileDraftSeed(connection.relayModelProfiles, connection.providerType),
);
}
if (plan.clearDirty) {
setRelayProfilesDirty(false);
Expand Down
71 changes: 65 additions & 6 deletions packages/core/src/__tests__/model-thinking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
type ConnectionThinkingContext,
declarableRelayThinkingLevels,
normalizeRelayModelProfiles,
relayModelProfile,
resolveThinkingLevel,
Expand All @@ -32,18 +33,75 @@ import {
} from '../model-thinking.js';
import { isRelayProviderType } from '../llm-connections.js';

test('declarable relay levels are every intensity tier but off', () => {
// `off` is a disable-wire encoding (reasoning_effort 'none'), not an
// intensity tier — a hybrid UI/data contract keeps it out of declarations.
test('declarable relay levels are per provider: Anthropic relays may declare off', () => {
// OpenAI relays keep `off` out: it is a disable-wire encoding
// (reasoning_effort 'none') no generic relay is presumed to speak.
// Anthropic-protocol relays have a true disable wire
// (`thinking: { type: 'disabled' }`), so their declarations may carry it.
assert.deepEqual(declarableRelayThinkingLevels('openai-compatible'), [
'minimal',
'low',
'medium',
'high',
'xhigh',
'max',
]);
assert.deepEqual(declarableRelayThinkingLevels('openai-responses-compatible'), [
'minimal',
'low',
'medium',
'high',
'xhigh',
'max',
]);
assert.deepEqual(declarableRelayThinkingLevels('anthropic-compatible'), [
'off',
'minimal',
'low',
'medium',
'high',
'xhigh',
'max',
]);
});

test('normalize filters off per provider and is lenient without one', () => {
// Explicit provider: the openai vocabulary drops `off`...
assert.deepEqual(
normalizeRelayModelProfiles({ m: { thinkingLevels: ['off', 'low'] } }, 'openai-compatible'),
{ m: { thinkingLevels: ['low'] } },
);
assert.equal(
normalizeRelayModelProfiles({ m: { thinkingLevels: ['off'] } }, 'openai-compatible'),
undefined,
);
// ...while an anthropic-compatible declaration keeps it.
assert.deepEqual(
normalizeRelayModelProfiles(
{ m: { thinkingLevels: ['off', 'high'] } },
'anthropic-compatible',
),
{ m: { thinkingLevels: ['off', 'high'] } },
);
// Without a provider (host-wire decode fallback) the sanitizer is
// provider-blind: the canonical store's codec has already validated
// provider fit, so decode keeps the full vocabulary and only drops junk.
assert.deepEqual(normalizeRelayModelProfiles({ m: { thinkingLevels: ['off', 'low'] } }), {
m: { thinkingLevels: ['low'] },
m: { thinkingLevels: ['off', 'low'] },
});
assert.equal(normalizeRelayModelProfiles({ m: { thinkingLevels: ['off'] } }), undefined);
});

test('anthropic-compatible declared levels surface through the read seam', () => {
const declaredOff = {
providerType: 'openai-compatible',
relayModelProfiles: { m: { thinkingLevels: ['off', 'low'] } },
} as const;
assert.deepEqual([...thinkingVariantsForConnection(declaredOff, 'm')], ['low']);
const anthropicRelay = {
providerType: 'anthropic-compatible',
relayModelProfiles: { m: { thinkingLevels: ['off', 'high'] } },
} as const;
assert.deepEqual([...thinkingVariantsForConnection(anthropicRelay, 'm')], ['off', 'high']);
});

test('relay profiles preserve the fast service tier declaration', () => {
Expand Down Expand Up @@ -148,9 +206,10 @@ test('relayModelProfile honours a declaration on any provider', () => {
);
});

test('isRelayProviderType only accepts the two custom OpenAI relay providers', () => {
test('isRelayProviderType accepts the three custom relay providers', () => {
assert.equal(isRelayProviderType('openai-compatible'), true);
assert.equal(isRelayProviderType('openai-responses-compatible'), true);
assert.equal(isRelayProviderType('anthropic-compatible'), true);
assert.equal(isRelayProviderType('openai'), false);
assert.equal(isRelayProviderType('anthropic'), false);
});
Expand Down
Loading