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
28 changes: 28 additions & 0 deletions apps/mobile/src/lib/use-session-model-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,34 @@ describe('buildSessionModelOptions', () => {
expect(result.options.some(option => option.name === 'Use session model')).toBe(false);
});

it('locks the chip to the observed model id when catalog parse never becomes v1', () => {
const result = buildSessionModelOptions({
activeSessionType: 'remote',
remoteModelState: {
ownerConnectionId: 'cli-owner',
protocol: 'unknown',
refresh: 'error',
error: 'Invalid remote model catalog',
},
observedModel: {
model: { providerID: 'kilo', modelID: 'muse-spark-1.3-contributor' },
},
remoteModelOverride: null,
gatewayModels,
gatewayModelsLoading: false,
organizationId: 'org-persisted',
});

expect(result.source).toBe('remote-unavailable');
expect(result.pickerDisabled).toBe(true);
expect(result.options).toEqual([
expect.objectContaining({
name: 'muse-spark-1.3-contributor',
unavailable: true,
}),
]);
});

it('disables model changes when remote discovery fails without exposing Gateway rows', () => {
const result = buildSessionModelOptions({
activeSessionType: 'remote',
Expand Down
32 changes: 32 additions & 0 deletions packages/cloud-agent-sdk/src/cli-live-transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,38 @@ describe('CliLiveTransport unified user web connection', () => {
transport.destroy();
});

it('publishes v1 when the CLI catalog uses interleaved.field=reasoning_text', async () => {
const catalog: RemoteModelCatalogWireV1 = structuredClone(WIRE_CATALOG);
catalog.all[0]!.models['claude-sonnet-4']!.capabilities.interleaved = {
field: 'reasoning_text',
};
const connection = createConnection();
jest
.mocked(connection.sendCommand)
.mockImplementation((_sessionId, command) =>
Promise.resolve(command === 'list_models' ? catalog : { ok: true })
);
const states: RemoteModelState[] = [];
const { transport } = createTransportWithSinks({
connection,
onRemoteModelStateChange: state => states.push(state),
});

transport.connect();
emitOwner(connection);
await Promise.resolve();
await Promise.resolve();

expect(states.at(-1)).toEqual({
ownerConnectionId: 'owner',
protocol: 'v1',
catalog: REMOTE_CATALOG,
refresh: 'idle',
});
expect(transport.canSend?.()).toBe(true);
transport.destroy();
});

it('keeps protocol unknown and owner send capability after a malformed initial catalog', async () => {
const connection = createConnection();
jest.mocked(connection.sendCommand).mockResolvedValueOnce({
Expand Down
48 changes: 40 additions & 8 deletions packages/cloud-agent-sdk/src/remote-model-catalog.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { z } from 'zod';

import {
REMOTE_MODEL_CATALOG_MAX_SERIALIZED_BYTES,
REMOTE_MODEL_IDENTITY_MAX_LENGTH,
Expand All @@ -9,9 +11,19 @@ import {
modelRefsEqual,
remoteModelCatalogV1Schema,
remoteModelCatalogWireV1Schema,
type RemoteModelCatalogWireV1,
} from './remote-model-catalog';

function createSdkModel(providerID: string, id: string, variants: string[] = [], name = id) {
// SdkModelFixture mirrors the wire schema's model shape, so interleaved keeps
// the boolean | { field } union the schema accepts.
type SdkModelFixture = RemoteModelCatalogWireV1['all'][number]['models'][string];

function createSdkModel(
providerID: string,
id: string,
variants: string[] = [],
name = id
): SdkModelFixture {
return {
id,
providerID,
Expand All @@ -36,13 +48,6 @@ function createSdkModel(providerID: string, id: string, variants: string[] = [],
};
}

type SdkModelFixture = ReturnType<typeof createSdkModel> & {
recommendedIndex?: number;
isFree?: boolean;
mayTrainOnYourPrompts?: boolean;
hasUserByokAvailable?: boolean;
};

function createSdkProvider(
id: string,
models: SdkModelFixture[] = [createSdkModel(id, `model-${id}`)]
Expand Down Expand Up @@ -146,6 +151,33 @@ function createUtf8OversizedCatalog() {
}

describe('remoteModelCatalogV1Schema', () => {
it('reproduces the old interleaved enum rejecting CLI catalogs', () => {
const oldInterleaved = z.union([
z.boolean(),
z.object({ field: z.enum(['reasoning_content', 'reasoning_details']) }).strict(),
]);

expect(oldInterleaved.safeParse({ field: 'reasoning_text' }).success).toBe(false);
expect(oldInterleaved.safeParse({ field: 'reasoning' }).success).toBe(false);
});

it('accepts CLI interleaved field names the picker does not use', () => {
const fields = ['reasoning', 'reasoning_content', 'reasoning_details', 'reasoning_text'];

for (const field of fields) {
const model = createSdkModel('kilo', 'muse-spark-1.3-contributor');
model.capabilities = { ...model.capabilities, interleaved: { field } };
const parsed = remoteModelCatalogV1Schema.safeParse(
createWireCatalog([createSdkProvider('kilo', [model])])
);

expect(parsed.success).toBe(true);
if (parsed.success) {
expect(parsed.data.providers[0]?.models[0]?.id).toBe('muse-spark-1.3-contributor');
}
}
});

it('normalizes the SDK ProviderListResponse shape without rewriting model identities', () => {
const model: SdkModelFixture = createSdkModel(
'custom/provider:v1',
Expand Down
5 changes: 1 addition & 4 deletions packages/cloud-agent-sdk/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,7 @@ const remoteSdkModelSchema = z
toolcall: z.boolean(),
input: remoteModelModalitiesSchema,
output: remoteModelModalitiesSchema,
interleaved: z.union([
z.boolean(),
z.object({ field: z.enum(['reasoning_content', 'reasoning_details']) }).strict(),
]),
interleaved: z.union([z.boolean(), z.object({ field: z.string().min(1) }).passthrough()]),
})
.strict(),
cost: z
Expand Down