feat: consume server model reasoning capabilities
This commit is contained in:
@@ -50,6 +50,20 @@ function envelope(
|
||||
}
|
||||
|
||||
describe('Conversation product contracts', () => {
|
||||
it('accepts max in the persisted model and available thinking-level contract', () => {
|
||||
const snapshot = createProductSnapshot();
|
||||
snapshot.conversation.model = {
|
||||
model: {
|
||||
accountId: 'account-max',
|
||||
modelId: 'deepseek-v4-pro',
|
||||
thinkingLevel: 'max',
|
||||
},
|
||||
modelResolution: 'resolved',
|
||||
availableThinkingLevels: ['off', 'low', 'high', 'max'],
|
||||
};
|
||||
expect(isConversationSnapshot(snapshot)).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts bounded capability envelopes for every Data Service operation', () => {
|
||||
const control = new Set(['configure', 'inspect', 'list_projects', 'remove_collection', 'reset', 'remove_project']);
|
||||
const operations = [
|
||||
|
||||
@@ -528,6 +528,61 @@ describe('PI-130 feature-complete Coding UI', () => {
|
||||
expect(screen.queryByRole('menu', { name: '选择推理强度' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the native max thinking level as 最高', async () => {
|
||||
const { CodingComposerRuntimeControls } = await import('@/pages/Chat/CodingComposerRuntimeControls');
|
||||
const conversation = {
|
||||
id: 'conversation-max-thinking',
|
||||
agentId: 'agent-1',
|
||||
title: 'Max thinking controls',
|
||||
archivedAt: null,
|
||||
unread: false,
|
||||
createdAt: '2026-08-28T00:00:00.000Z',
|
||||
updatedAt: '2026-08-28T00:00:00.000Z',
|
||||
model: { accountId: 'account-1', modelId: 'deepseek-v4-pro', thinkingLevel: 'max' as const },
|
||||
modelResolution: 'resolved' as const,
|
||||
};
|
||||
const snapshot: ConversationSnapshot = {
|
||||
schemaVersion: 1,
|
||||
conversation: {
|
||||
id: conversation.id,
|
||||
projectId: 'project-1',
|
||||
agentId: conversation.agentId,
|
||||
title: conversation.title,
|
||||
model: {
|
||||
model: conversation.model,
|
||||
modelResolution: 'resolved',
|
||||
availableThinkingLevels: ['off', 'low', 'high', 'max'],
|
||||
},
|
||||
},
|
||||
nodes: [],
|
||||
run: { status: 'idle' },
|
||||
queue: { items: [] },
|
||||
context: { usedTokens: 0, contextWindow: 0, compaction: 'idle' },
|
||||
pendingInteractions: [],
|
||||
worker: { status: 'ready', generation: 1 },
|
||||
cursor: { workerGeneration: 1, seq: 0 },
|
||||
};
|
||||
|
||||
render(
|
||||
<CodingComposerRuntimeControls
|
||||
conversation={conversation}
|
||||
snapshot={snapshot}
|
||||
onRefresh={vi.fn(async () => undefined)}
|
||||
/>,
|
||||
);
|
||||
|
||||
const settingsTrigger = screen.getByRole('button', {
|
||||
name: '模型与思考设置:deepseek-v4-pro,最高',
|
||||
});
|
||||
expect(settingsTrigger).toHaveTextContent('最高');
|
||||
fireEvent.pointerDown(settingsTrigger, { button: 0, ctrlKey: false });
|
||||
const thinkingRow = await screen.findByRole('menuitem', { name: '推理强度 最高' });
|
||||
fireEvent.click(thinkingRow);
|
||||
expect(screen.getByRole('menuitemradio', { name: '最高' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('menuitemradio', { name: '极简' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('menuitemradio', { name: '中等' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('blocks overlapping mutations but keeps abort and recover available while confirmation is uncertain', async () => {
|
||||
interactionApi.abort.mockResolvedValue(undefined);
|
||||
const recover = vi.fn(async () => undefined);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { atomicWriteJson } from '../../electron/coding-projects/atomic-json';
|
||||
import {
|
||||
createCodingProjectAgent,
|
||||
createCodingProjectConfigV2,
|
||||
normalizeProductModelRef,
|
||||
readCodingProjectConfigV2,
|
||||
} from '../../electron/coding-projects/project-config';
|
||||
import {
|
||||
@@ -54,6 +55,18 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe('coding project schema v2', () => {
|
||||
it('accepts the native max thinking level in a project model reference', () => {
|
||||
expect(normalizeProductModelRef({
|
||||
accountId: 'account-local',
|
||||
modelId: 'deepseek-v4-pro',
|
||||
thinkingLevel: 'max',
|
||||
})).toEqual({
|
||||
accountId: 'account-local',
|
||||
modelId: 'deepseek-v4-pro',
|
||||
thinkingLevel: 'max',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates project, Agent, and empty Conversation metadata without a runtime child', async () => {
|
||||
const projectPath = await makeProjectPath();
|
||||
const storage = createMemoryCodingProjectStorage();
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
estimateImportedModelVisionTokens,
|
||||
getImportedModelProfile,
|
||||
} from '../../shared/imported-model-profile';
|
||||
import { normalizeImportedModelCapabilities } from '../../shared/user-model-config';
|
||||
import {
|
||||
getKnownModelCapabilityKind,
|
||||
getModelCapabilityLabel,
|
||||
@@ -10,6 +11,74 @@ import {
|
||||
} from '../../shared/model-capabilities';
|
||||
|
||||
describe('getImportedModelProfile', () => {
|
||||
it('returns native DeepSeek V4 Pro thinking levels without generic placeholders', () => {
|
||||
expect(getImportedModelProfile('deepseek-v4-pro')).toMatchObject({
|
||||
modalities: {
|
||||
input: ['text'],
|
||||
output: ['text'],
|
||||
},
|
||||
limit: {
|
||||
context: 1_000_000,
|
||||
output: 384_000,
|
||||
},
|
||||
pi: {
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {
|
||||
minimal: null,
|
||||
low: 'low',
|
||||
medium: null,
|
||||
high: 'high',
|
||||
max: 'max',
|
||||
},
|
||||
compat: {
|
||||
thinkingFormat: 'deepseek',
|
||||
supportsReasoningEffort: true,
|
||||
requiresReasoningContentOnAssistantMessages: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes Works model capabilities by normalized model id and supported effort', () => {
|
||||
expect(normalizeImportedModelCapabilities({
|
||||
'deepseek/deepseek-v4-pro': {
|
||||
reasoning_efforts: ['max', 'low', 'low', 'medium', 'unsupported'],
|
||||
reasoning_can_disable: true,
|
||||
},
|
||||
'qwen3.8-max': {
|
||||
reasoning_efforts: ['high'],
|
||||
reasoning_can_disable: false,
|
||||
},
|
||||
'unknown-model': {
|
||||
reasoning_efforts: ['low'],
|
||||
reasoning_can_disable: 'yes',
|
||||
},
|
||||
}, ['deepseek-v4-pro', 'qwen3.8-max'])).toEqual({
|
||||
'deepseek-v4-pro': {
|
||||
reasoningEfforts: ['low', 'max'],
|
||||
reasoningCanDisable: true,
|
||||
},
|
||||
'qwen3.8-max': {
|
||||
reasoningEfforts: ['high'],
|
||||
reasoningCanDisable: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves an explicit empty effort list so the server can disable local reasoning metadata', () => {
|
||||
expect(normalizeImportedModelCapabilities({
|
||||
'deepseek-v4-pro': {
|
||||
reasoning_efforts: [],
|
||||
reasoning_can_disable: false,
|
||||
},
|
||||
}, ['deepseek-v4-pro'])).toEqual({
|
||||
'deepseek-v4-pro': {
|
||||
reasoningEfforts: [],
|
||||
reasoningCanDisable: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
'qwen3.6-plus',
|
||||
'qwen3.6-plus-2026-04-02',
|
||||
|
||||
@@ -2,6 +2,8 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { streamSimple } from '@earendil-works/pi-ai/api/openai-completions';
|
||||
import type { Model } from '@earendil-works/pi-ai';
|
||||
import type { ProviderAccount } from '@electron/shared/providers/types';
|
||||
import {
|
||||
buildPiProviderCatalog,
|
||||
@@ -269,11 +271,11 @@ describe('Pi Provider catalog', () => {
|
||||
requiresReasoningContentOnAssistantMessages: true,
|
||||
},
|
||||
thinkingLevelMap: {
|
||||
off: null,
|
||||
minimal: null,
|
||||
low: null,
|
||||
low: 'low',
|
||||
medium: null,
|
||||
high: 'high',
|
||||
max: 'max',
|
||||
},
|
||||
});
|
||||
expect(written).toMatchObject({
|
||||
@@ -283,6 +285,210 @@ describe('Pi Provider catalog', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('uses server reasoning capabilities while retaining local model metadata', () => {
|
||||
const provider = account({
|
||||
id: 'niancode-user-models',
|
||||
vendorId: 'custom',
|
||||
apiProtocol: 'openai-completions',
|
||||
baseUrl: 'http://127.0.0.1:54321/api/ai-proxy/v1',
|
||||
model: 'qwen3.8-max',
|
||||
metadata: {
|
||||
worksSquareCredentialMode: 'works_square_ai_gateway_proxy',
|
||||
customModels: ['qwen3.8-max'],
|
||||
worksSquareModelCapabilities: {
|
||||
'qwen3.8-max': {
|
||||
reasoningEfforts: ['low'],
|
||||
reasoningCanDisable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const descriptor = buildPiProviderCatalog({ accounts: [provider] }).descriptors[0]!.models[0]!;
|
||||
|
||||
expect(descriptor).toMatchObject({
|
||||
id: 'qwen3.8-max',
|
||||
input: ['text', 'image'],
|
||||
reasoning: true,
|
||||
contextWindow: 1_000_000,
|
||||
maxOutputTokens: 131_072,
|
||||
thinkingLevelMap: {
|
||||
off: null,
|
||||
minimal: null,
|
||||
low: 'low',
|
||||
medium: null,
|
||||
high: null,
|
||||
max: null,
|
||||
},
|
||||
compat: {
|
||||
thinkingFormat: 'qwen',
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: true,
|
||||
supportsStore: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('enables reasoning-effort serialization for a server model without a local profile', () => {
|
||||
const provider = account({
|
||||
id: 'niancode-user-models',
|
||||
vendorId: 'custom',
|
||||
apiProtocol: 'openai-completions',
|
||||
baseUrl: 'http://127.0.0.1:54321/api/ai-proxy/v1',
|
||||
model: 'future-reasoning-model',
|
||||
metadata: {
|
||||
worksSquareCredentialMode: 'works_square_ai_gateway_proxy',
|
||||
customModels: ['future-reasoning-model'],
|
||||
worksSquareModelCapabilities: {
|
||||
'future-reasoning-model': {
|
||||
reasoningEfforts: ['low', 'high', 'max'],
|
||||
reasoningCanDisable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const descriptor = buildPiProviderCatalog({ accounts: [provider] }).descriptors[0]!.models[0]!;
|
||||
|
||||
expect(descriptor).toMatchObject({
|
||||
id: 'future-reasoning-model',
|
||||
reasoning: true,
|
||||
compat: {
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: true,
|
||||
},
|
||||
thinkingLevelMap: {
|
||||
minimal: null,
|
||||
low: 'low',
|
||||
medium: null,
|
||||
high: 'high',
|
||||
max: 'max',
|
||||
},
|
||||
});
|
||||
expect(descriptor.thinkingLevelMap).not.toHaveProperty('off');
|
||||
});
|
||||
|
||||
it('lets an explicit empty server effort list override a locally reasoning model', () => {
|
||||
const provider = account({
|
||||
id: 'niancode-user-models',
|
||||
vendorId: 'custom',
|
||||
apiProtocol: 'openai-completions',
|
||||
baseUrl: 'http://127.0.0.1:54321/api/ai-proxy/v1',
|
||||
model: 'deepseek-v4-pro',
|
||||
metadata: {
|
||||
worksSquareCredentialMode: 'works_square_ai_gateway_proxy',
|
||||
customModels: ['deepseek-v4-pro'],
|
||||
worksSquareModelCapabilities: {
|
||||
'deepseek-v4-pro': {
|
||||
reasoningEfforts: [],
|
||||
reasoningCanDisable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const descriptor = buildPiProviderCatalog({ accounts: [provider] }).descriptors[0]!.models[0]!;
|
||||
|
||||
expect(descriptor.reasoning).toBe(false);
|
||||
expect(descriptor.thinkingLevelMap).toEqual({
|
||||
off: null,
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
high: null,
|
||||
max: null,
|
||||
});
|
||||
expect(descriptor.compat).toMatchObject({
|
||||
thinkingFormat: 'deepseek',
|
||||
supportsReasoningEffort: true,
|
||||
requiresReasoningContentOnAssistantMessages: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('serializes DeepSeek off without sending a reasoning effort', async () => {
|
||||
const payloads: unknown[] = [];
|
||||
const model: Model<'openai-completions'> = {
|
||||
id: 'deepseek-v4-pro',
|
||||
name: 'DeepSeek V4 Pro',
|
||||
api: 'openai-completions',
|
||||
provider: 'deepseek',
|
||||
baseUrl: 'https://gateway.test/v1',
|
||||
reasoning: true,
|
||||
input: ['text'],
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 384_000,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
thinkingLevelMap: {
|
||||
minimal: null,
|
||||
low: 'low',
|
||||
medium: null,
|
||||
high: 'high',
|
||||
max: 'max',
|
||||
},
|
||||
compat: {
|
||||
thinkingFormat: 'deepseek',
|
||||
supportsReasoningEffort: true,
|
||||
},
|
||||
};
|
||||
|
||||
const stream = streamSimple(model, { messages: [] }, {
|
||||
apiKey: 'test-key',
|
||||
reasoning: 'off',
|
||||
onPayload(payload) {
|
||||
payloads.push(payload);
|
||||
throw new Error('stop before network');
|
||||
},
|
||||
});
|
||||
const result = await stream.result();
|
||||
|
||||
expect(result.stopReason).toBe('error');
|
||||
expect(payloads[0]).toMatchObject({ thinking: { type: 'disabled' } });
|
||||
expect(payloads[0]).not.toHaveProperty('reasoning_effort');
|
||||
});
|
||||
|
||||
it.each(['low', 'high', 'max'] as const)('serializes DeepSeek %s with its reasoning effort', async (level) => {
|
||||
const payloads: unknown[] = [];
|
||||
const model: Model<'openai-completions'> = {
|
||||
id: 'deepseek-v4-pro',
|
||||
name: 'DeepSeek V4 Pro',
|
||||
api: 'openai-completions',
|
||||
provider: 'deepseek',
|
||||
baseUrl: 'https://gateway.test/v1',
|
||||
reasoning: true,
|
||||
input: ['text'],
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 384_000,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
thinkingLevelMap: {
|
||||
minimal: null,
|
||||
low: 'low',
|
||||
medium: null,
|
||||
high: 'high',
|
||||
max: 'max',
|
||||
},
|
||||
compat: {
|
||||
thinkingFormat: 'deepseek',
|
||||
supportsReasoningEffort: true,
|
||||
},
|
||||
};
|
||||
|
||||
const stream = streamSimple(model, { messages: [] }, {
|
||||
apiKey: 'test-key',
|
||||
reasoning: level,
|
||||
onPayload(payload) {
|
||||
payloads.push(payload);
|
||||
throw new Error('stop before network');
|
||||
},
|
||||
});
|
||||
const result = await stream.result();
|
||||
|
||||
expect(result.stopReason).toBe('error');
|
||||
expect(payloads[0]).toMatchObject({
|
||||
thinking: { type: 'enabled' },
|
||||
reasoning_effort: level,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not replace an existing catalog when model selection is unavailable', async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-provider-'));
|
||||
temporaryRoots.push(root);
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { handleProviderRoutes } from '@electron/api/routes/providers';
|
||||
import {
|
||||
handleProviderRoutes,
|
||||
importCurrentUserModelConfig,
|
||||
normalizeImportedUserModelConfig,
|
||||
} from '@electron/api/routes/providers';
|
||||
import type { ProviderAccount } from '@electron/shared/providers/types';
|
||||
|
||||
const providerServiceMock = vi.hoisted(() => ({
|
||||
getAccount: vi.fn(),
|
||||
getAccountApiKey: vi.fn(),
|
||||
getDefaultAccountId: vi.fn(),
|
||||
updateAccount: vi.fn(),
|
||||
setDefaultAccount: vi.fn(),
|
||||
}));
|
||||
const proxyAwareFetchMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@electron/services/providers/provider-service', () => ({
|
||||
getProviderService: () => providerServiceMock,
|
||||
}));
|
||||
|
||||
vi.mock('@electron/utils/proxy-fetch', () => ({
|
||||
proxyAwareFetch: (...args: unknown[]) => proxyAwareFetchMock(...args),
|
||||
}));
|
||||
|
||||
function createRequest(method: string, body?: unknown): IncomingMessage {
|
||||
const request = new EventEmitter();
|
||||
Object.assign(request, {
|
||||
@@ -66,6 +76,7 @@ describe('provider host api routes', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
providerServiceMock.getAccount.mockResolvedValue(account());
|
||||
providerServiceMock.getAccountApiKey.mockResolvedValue('stored-key');
|
||||
providerServiceMock.updateAccount.mockResolvedValue(account({
|
||||
updatedAt: '2026-08-24T01:00:00.000Z',
|
||||
}));
|
||||
@@ -73,6 +84,139 @@ describe('provider host api routes', () => {
|
||||
providerServiceMock.setDefaultAccount.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('normalizes optional Works model capabilities before storing the account', () => {
|
||||
expect(normalizeImportedUserModelConfig({
|
||||
label: 'Makelore Models',
|
||||
base_url: 'https://gateway.test/v1',
|
||||
api_key: 'gateway-key',
|
||||
credential_mode: 'works_square_ai_gateway',
|
||||
models: ['deepseek/deepseek-v4-pro', 'qwen3.8-max'],
|
||||
model_capabilities: {
|
||||
'deepseek/deepseek-v4-pro': {
|
||||
reasoning_efforts: ['max', 'low', 'medium', 'low'],
|
||||
reasoning_can_disable: true,
|
||||
},
|
||||
'qwen3.8-max': {
|
||||
reasoning_efforts: ['high'],
|
||||
reasoning_can_disable: false,
|
||||
},
|
||||
'not-provisioned': {
|
||||
reasoning_efforts: ['low'],
|
||||
reasoning_can_disable: true,
|
||||
},
|
||||
},
|
||||
})).toMatchObject({
|
||||
models: ['deepseek-v4-pro', 'qwen3.8-max'],
|
||||
modelCapabilities: {
|
||||
'deepseek-v4-pro': {
|
||||
reasoningEfforts: ['low', 'max'],
|
||||
reasoningCanDisable: true,
|
||||
},
|
||||
'qwen3.8-max': {
|
||||
reasoningEfforts: ['high'],
|
||||
reasoningCanDisable: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('persists server capabilities and invalidates the runtime when they change', async () => {
|
||||
const existing = account({
|
||||
id: 'niancode-user-models',
|
||||
vendorId: 'custom',
|
||||
model: 'deepseek-v4-pro',
|
||||
metadata: {
|
||||
customModels: ['deepseek-v4-pro'],
|
||||
worksSquareModelCapabilities: {
|
||||
'deepseek-v4-pro': {
|
||||
reasoningEfforts: ['high'],
|
||||
reasoningCanDisable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
providerServiceMock.getAccount.mockResolvedValue(existing);
|
||||
providerServiceMock.updateAccount.mockImplementation(async (_accountId: string, patch: Partial<ProviderAccount>) => ({
|
||||
...existing,
|
||||
...patch,
|
||||
}));
|
||||
proxyAwareFetchMock.mockResolvedValue(new Response(JSON.stringify({
|
||||
label: 'Makelore Models',
|
||||
base_url: 'https://gateway.test/v1',
|
||||
api_key: 'gateway-key',
|
||||
credential_mode: 'api_key',
|
||||
models: ['deepseek/deepseek-v4-pro'],
|
||||
model_capabilities: {
|
||||
'deepseek/deepseek-v4-pro': {
|
||||
reasoning_efforts: ['low', 'high', 'max'],
|
||||
reasoning_can_disable: true,
|
||||
},
|
||||
},
|
||||
}), { status: 200, headers: { 'content-type': 'application/json' } }));
|
||||
|
||||
const imported = await importCurrentUserModelConfig(context, 'access-token');
|
||||
|
||||
expect(imported.account.metadata?.worksSquareModelCapabilities).toEqual({
|
||||
'deepseek-v4-pro': {
|
||||
reasoningEfforts: ['low', 'high', 'max'],
|
||||
reasoningCanDisable: true,
|
||||
},
|
||||
});
|
||||
expect(providerServiceMock.updateAccount).toHaveBeenCalledWith(
|
||||
'niancode-user-models',
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
worksSquareModelCapabilities: {
|
||||
'deepseek-v4-pro': {
|
||||
reasoningEfforts: ['low', 'high', 'max'],
|
||||
reasoningCanDisable: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
'gateway-key',
|
||||
);
|
||||
expect(markProviderStale).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('clears a previous server capability override when the optional field is absent', async () => {
|
||||
const existing = account({
|
||||
id: 'niancode-user-models',
|
||||
vendorId: 'custom',
|
||||
model: 'deepseek-v4-pro',
|
||||
metadata: {
|
||||
customModels: ['deepseek-v4-pro'],
|
||||
worksSquareModelCapabilities: {
|
||||
'deepseek-v4-pro': {
|
||||
reasoningEfforts: ['max'],
|
||||
reasoningCanDisable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
providerServiceMock.getAccount.mockResolvedValue(existing);
|
||||
providerServiceMock.updateAccount.mockImplementation(async (_accountId: string, patch: Partial<ProviderAccount>) => ({
|
||||
...existing,
|
||||
...patch,
|
||||
}));
|
||||
proxyAwareFetchMock.mockResolvedValue(new Response(JSON.stringify({
|
||||
base_url: 'https://gateway.test/v1',
|
||||
api_key: 'gateway-key',
|
||||
models: ['deepseek/deepseek-v4-pro'],
|
||||
}), { status: 200, headers: { 'content-type': 'application/json' } }));
|
||||
|
||||
const imported = await importCurrentUserModelConfig(context, 'access-token');
|
||||
|
||||
expect(imported.account.metadata).not.toHaveProperty('worksSquareModelCapabilities');
|
||||
expect(providerServiceMock.updateAccount).toHaveBeenCalledWith(
|
||||
'niancode-user-models',
|
||||
expect.objectContaining({
|
||||
metadata: expect.not.objectContaining({ worksSquareModelCapabilities: expect.anything() }),
|
||||
}),
|
||||
'gateway-key',
|
||||
);
|
||||
});
|
||||
|
||||
it('marks Pi provider input stale after an account credential update', async () => {
|
||||
const result = createResponse();
|
||||
const handled = await handleProviderRoutes(
|
||||
|
||||
Reference in New Issue
Block a user