mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-19 21:55:06 +00:00
fix(cli): scope provider version to template updates
Keep ordinary install metadata tied to the models in the install plan, while confirmed provider updates persist the detected built-in template version.
This commit is contained in:
parent
1df9c1d0fa
commit
71bd8f1e0f
5 changed files with 32 additions and 122 deletions
|
|
@ -222,7 +222,7 @@ describe('useProviderUpdates', () => {
|
||||||
expect(entry?.diff.added).toContain(addedModelId);
|
expect(entry?.diff.added).toContain(addedModelId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('preserves user-added custom models when executing an update', async () => {
|
it('persists the template version and preserves custom models', async () => {
|
||||||
const customModel = {
|
const customModel = {
|
||||||
id: 'my-custom-model',
|
id: 'my-custom-model',
|
||||||
baseUrl: CODING_PLAN_CHINA_BASE_URL,
|
baseUrl: CODING_PLAN_CHINA_BASE_URL,
|
||||||
|
|
@ -264,6 +264,11 @@ describe('useProviderUpdates', () => {
|
||||||
expect.objectContaining({ id: 'my-custom-model' }),
|
expect.objectContaining({ id: 'my-custom-model' }),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
`${PROVIDER_METADATA_NS}.${METADATA_KEY}.version`,
|
||||||
|
chinaVersion,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('executes update when user confirms with "update"', async () => {
|
it('executes update when user confirms with "update"', async () => {
|
||||||
|
|
@ -303,11 +308,6 @@ describe('useProviderUpdates', () => {
|
||||||
expect(mockSettings.setValue).toHaveBeenCalled();
|
expect(mockSettings.setValue).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
`${PROVIDER_METADATA_NS}.${METADATA_KEY}.version`,
|
|
||||||
chinaVersion,
|
|
||||||
);
|
|
||||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
`${PROVIDER_METADATA_NS}.${METADATA_KEY}.baseUrl`,
|
`${PROVIDER_METADATA_NS}.${METADATA_KEY}.baseUrl`,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ import {
|
||||||
ALL_PROVIDERS,
|
ALL_PROVIDERS,
|
||||||
applyProviderInstallPlan,
|
applyProviderInstallPlan,
|
||||||
buildInstallPlan,
|
buildInstallPlan,
|
||||||
computeProviderTemplateVersion,
|
buildProviderTemplate,
|
||||||
|
computeModelListVersion,
|
||||||
getDefaultModelIds,
|
getDefaultModelIds,
|
||||||
PROVIDER_METADATA_NS,
|
PROVIDER_METADATA_NS,
|
||||||
providerMatchesCredentials,
|
providerMatchesCredentials,
|
||||||
|
|
@ -204,7 +205,8 @@ function findAllPendingUpdates(
|
||||||
if (!metadata.version) continue;
|
if (!metadata.version) continue;
|
||||||
|
|
||||||
const baseUrl = metadata.baseUrl || resolveBaseUrl(provider);
|
const baseUrl = metadata.baseUrl || resolveBaseUrl(provider);
|
||||||
const currentVersion = computeProviderTemplateVersion(provider, baseUrl);
|
const currentTemplate = buildProviderTemplate(provider, baseUrl);
|
||||||
|
const currentVersion = computeModelListVersion(currentTemplate);
|
||||||
|
|
||||||
if (metadata.version === currentVersion) continue;
|
if (metadata.version === currentVersion) continue;
|
||||||
if (metadata.ignoredVersion === currentVersion) continue;
|
if (metadata.ignoredVersion === currentVersion) continue;
|
||||||
|
|
@ -253,9 +255,10 @@ export function useProviderUpdates(
|
||||||
const migrated = useRef(false);
|
const migrated = useRef(false);
|
||||||
|
|
||||||
const executeUpdate = useCallback(
|
const executeUpdate = useCallback(
|
||||||
async (providerCfg: ProviderConfig, baseUrl?: string) => {
|
async (pending: PendingUpdate) => {
|
||||||
try {
|
try {
|
||||||
const resolved = resolveBaseUrl(providerCfg, baseUrl);
|
const providerCfg = pending.provider;
|
||||||
|
const resolved = resolveBaseUrl(providerCfg, pending.baseUrl);
|
||||||
// An update only refreshes built-in models — user-added custom IDs
|
// An update only refreshes built-in models — user-added custom IDs
|
||||||
// must be carried through so they are not deleted by the
|
// must be carried through so they are not deleted by the
|
||||||
// prepend-and-remove-owned merge.
|
// prepend-and-remove-owned merge.
|
||||||
|
|
@ -268,6 +271,9 @@ export function useProviderUpdates(
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
modelIds: [...defaultIds, ...customIds],
|
modelIds: [...defaultIds, ...customIds],
|
||||||
});
|
});
|
||||||
|
installPlan.providerState![
|
||||||
|
`${PROVIDER_METADATA_NS}.${pending.metadataKey}`
|
||||||
|
]!['version'] = pending.currentVersion;
|
||||||
delete installPlan.env;
|
delete installPlan.env;
|
||||||
const previousModel = config.getModel();
|
const previousModel = config.getModel();
|
||||||
const activeConfig = config.getContentGeneratorConfig();
|
const activeConfig = config.getContentGeneratorConfig();
|
||||||
|
|
@ -384,7 +390,7 @@ export function useProviderUpdates(
|
||||||
setUpdateRequest(undefined);
|
setUpdateRequest(undefined);
|
||||||
if (choice === 'update') {
|
if (choice === 'update') {
|
||||||
for (const p of pendingList) {
|
for (const p of pendingList) {
|
||||||
await executeUpdate(p.provider, p.baseUrl);
|
await executeUpdate(p);
|
||||||
}
|
}
|
||||||
} else if (choice === 'skip') {
|
} else if (choice === 'skip') {
|
||||||
const persistScope = getPersistScopeForModelSelection(settings);
|
const persistScope = getPersistScopeForModelSelection(settings);
|
||||||
|
|
|
||||||
|
|
@ -13,20 +13,11 @@ import {
|
||||||
findExistingProviderModels,
|
findExistingProviderModels,
|
||||||
findProviderByCredentials,
|
findProviderByCredentials,
|
||||||
getDefaultModelIds,
|
getDefaultModelIds,
|
||||||
PROVIDER_METADATA_NS,
|
|
||||||
resolveBaseUrl,
|
resolveBaseUrl,
|
||||||
shouldShowStep,
|
shouldShowStep,
|
||||||
providerMatchesCredentials,
|
providerMatchesCredentials,
|
||||||
type ProviderConfig,
|
type ProviderConfig,
|
||||||
} from '@qwen-code/qwen-code-core';
|
} from '@qwen-code/qwen-code-core';
|
||||||
// Imported from source: the invariant below must be checked against the
|
|
||||||
// current implementation, not a previously built `dist`.
|
|
||||||
import {
|
|
||||||
buildInstallPlan as buildInstallPlanSrc,
|
|
||||||
computeProviderTemplateVersion,
|
|
||||||
resolveMetadataKey,
|
|
||||||
} from '../provider-config.js';
|
|
||||||
import { ALL_PROVIDERS as ALL_PROVIDERS_SRC } from '../all-providers.js';
|
|
||||||
import {
|
import {
|
||||||
TOKEN_PLAN_CHINA_BASE_URL,
|
TOKEN_PLAN_CHINA_BASE_URL,
|
||||||
TOKEN_PLAN_ENV_KEY,
|
TOKEN_PLAN_ENV_KEY,
|
||||||
|
|
@ -71,8 +62,14 @@ describe('buildInstallPlan', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('builds a plan with editable models and unknown IDs', () => {
|
it('builds a plan with editable models and unknown IDs', () => {
|
||||||
const config = makeConfig({ modelsEditable: true });
|
const config = makeConfig({
|
||||||
const plan = buildInstallPlan(config, {
|
modelsEditable: true,
|
||||||
|
models: [
|
||||||
|
{ id: 'model-a', contextWindowSize: 8192, enableThinking: true },
|
||||||
|
{ id: 'model-b' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const plan = buildInstallPlanSrc(config, {
|
||||||
baseUrl: 'https://api.test.com/v1',
|
baseUrl: 'https://api.test.com/v1',
|
||||||
apiKey: 'sk-test',
|
apiKey: 'sk-test',
|
||||||
modelIds: ['model-a', 'unknown-model'],
|
modelIds: ['model-a', 'unknown-model'],
|
||||||
|
|
@ -86,6 +83,9 @@ describe('buildInstallPlan', () => {
|
||||||
name: '[Test] unknown-model',
|
name: '[Test] unknown-model',
|
||||||
});
|
});
|
||||||
expect(models?.[1]?.generationConfig).toBeUndefined();
|
expect(models?.[1]?.generationConfig).toBeUndefined();
|
||||||
|
expect(plan.providerState?.['providerMetadata.test']?.['version']).toBe(
|
||||||
|
computeModelListVersion(models ?? []),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies advancedConfig to editable unknown model IDs only', () => {
|
it('applies advancedConfig to editable unknown model IDs only', () => {
|
||||||
|
|
@ -683,6 +683,7 @@ import {
|
||||||
getAllProviderBaseUrls as getAllProviderBaseUrlsSrc,
|
getAllProviderBaseUrls as getAllProviderBaseUrlsSrc,
|
||||||
} from '../all-providers.js';
|
} from '../all-providers.js';
|
||||||
import {
|
import {
|
||||||
|
buildInstallPlan as buildInstallPlanSrc,
|
||||||
resolveBaseUrl as resolveBaseUrlSrc,
|
resolveBaseUrl as resolveBaseUrlSrc,
|
||||||
providerMatchesCredentials as providerMatchesCredentialsSrc,
|
providerMatchesCredentials as providerMatchesCredentialsSrc,
|
||||||
} from '../provider-config.js';
|
} from '../provider-config.js';
|
||||||
|
|
@ -925,84 +926,3 @@ describe('resolveMetadataKey dotted-id guard', () => {
|
||||||
expect(() => resolveMetadataKeySrc(config)).toThrow(/must not contain/);
|
expect(() => resolveMetadataKeySrc(config)).toThrow(/must not contain/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('stored provider version matches the launch-time check', () => {
|
|
||||||
// The prompt in useProviderUpdates clears only when the version recorded by
|
|
||||||
// an install/update equals the one recomputed at launch from the built-in
|
|
||||||
// template. Assert that invariant for every built-in provider, with a custom
|
|
||||||
// model installed — the case that used to make the two diverge forever.
|
|
||||||
const providersWithMetadata = ALL_PROVIDERS_SRC.filter((provider) =>
|
|
||||||
resolveMetadataKey(provider),
|
|
||||||
);
|
|
||||||
|
|
||||||
it('covers every built-in provider that records metadata', () => {
|
|
||||||
expect(providersWithMetadata.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const provider of providersWithMetadata) {
|
|
||||||
const metadataKey = resolveMetadataKey(provider)!;
|
|
||||||
|
|
||||||
it(`holds for ${metadataKey} when a custom model is installed`, () => {
|
|
||||||
const baseUrl = resolveBaseUrl(provider);
|
|
||||||
const defaultIds = getDefaultModelIds(provider);
|
|
||||||
const plan = buildInstallPlanSrc(provider, {
|
|
||||||
baseUrl,
|
|
||||||
apiKey: 'sk-test',
|
|
||||||
// A user-added id plus a built-in that upstream renamed away: both
|
|
||||||
// reach the plan as "custom" ids and must not affect the version.
|
|
||||||
modelIds: [...defaultIds, 'user-added-model', 'renamed-away-builtin'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const stored =
|
|
||||||
plan.providerState?.[`${PROVIDER_METADATA_NS}.${metadataKey}`];
|
|
||||||
const launchVersion = computeProviderTemplateVersion(provider, baseUrl);
|
|
||||||
|
|
||||||
expect(stored?.['version']).toBe(launchVersion);
|
|
||||||
// The plan still carries the custom ids — only the version ignores them.
|
|
||||||
expect(plan.modelProviders?.[0]?.models.map((m) => m.id)).toEqual(
|
|
||||||
expect.arrayContaining(['user-added-model', 'renamed-away-builtin']),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('computeProviderTemplateVersion', () => {
|
|
||||||
it('equals hashing the built-in template by hand', () => {
|
|
||||||
const config = makeConfig();
|
|
||||||
const baseUrl = resolveBaseUrl(config);
|
|
||||||
expect(computeProviderTemplateVersion(config, baseUrl)).toBe(
|
|
||||||
computeModelListVersion(buildProviderTemplate(config, baseUrl)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ignores installed custom models', () => {
|
|
||||||
const config = makeConfig();
|
|
||||||
const baseUrl = resolveBaseUrl(config);
|
|
||||||
const withCustom = buildInstallPlanSrc(config, {
|
|
||||||
baseUrl,
|
|
||||||
apiKey: 'sk-test',
|
|
||||||
modelIds: [...getDefaultModelIds(config), 'extra-model'],
|
|
||||||
});
|
|
||||||
const withoutCustom = buildInstallPlanSrc(config, {
|
|
||||||
baseUrl,
|
|
||||||
apiKey: 'sk-test',
|
|
||||||
modelIds: getDefaultModelIds(config),
|
|
||||||
});
|
|
||||||
const key = `${PROVIDER_METADATA_NS}.${resolveMetadataKey(config)}`;
|
|
||||||
expect(withCustom.providerState?.[key]?.['version']).toBe(
|
|
||||||
withoutCustom.providerState?.[key]?.['version'],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('changes when the built-in template changes', () => {
|
|
||||||
const baseUrl = 'https://api.test.com/v1';
|
|
||||||
const before = computeProviderTemplateVersion(makeConfig(), baseUrl);
|
|
||||||
const after = computeProviderTemplateVersion(
|
|
||||||
makeConfig({
|
|
||||||
models: [{ id: 'model-a-renamed', contextWindowSize: 8192 }],
|
|
||||||
}),
|
|
||||||
baseUrl,
|
|
||||||
);
|
|
||||||
expect(after).not.toBe(before);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ export {
|
||||||
buildInstallPlan,
|
buildInstallPlan,
|
||||||
buildProviderTemplate,
|
buildProviderTemplate,
|
||||||
computeModelListVersion,
|
computeModelListVersion,
|
||||||
computeProviderTemplateVersion,
|
|
||||||
findExistingProviderModels,
|
findExistingProviderModels,
|
||||||
getDefaultBaseUrlForProtocol,
|
getDefaultBaseUrlForProtocol,
|
||||||
getDefaultModelIds,
|
getDefaultModelIds,
|
||||||
|
|
|
||||||
|
|
@ -229,12 +229,13 @@ export const PROVIDER_METADATA_NS = 'providerMetadata';
|
||||||
function resolveProviderState(
|
function resolveProviderState(
|
||||||
config: ProviderConfig,
|
config: ProviderConfig,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
|
models: ProviderModelConfig[],
|
||||||
): ProviderInstallState | undefined {
|
): ProviderInstallState | undefined {
|
||||||
const key = resolveMetadataKey(config);
|
const key = resolveMetadataKey(config);
|
||||||
if (key) {
|
if (key) {
|
||||||
return {
|
return {
|
||||||
[`${PROVIDER_METADATA_NS}.${key}`]: {
|
[`${PROVIDER_METADATA_NS}.${key}`]: {
|
||||||
version: computeProviderTemplateVersion(config, baseUrl),
|
version: computeModelListVersion(models),
|
||||||
baseUrl,
|
baseUrl,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -286,7 +287,7 @@ export function buildInstallPlan(
|
||||||
...(ownsModel ? { ownsModel } : {}),
|
...(ownsModel ? { ownsModel } : {}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
providerState: resolveProviderState(config, inputs.baseUrl),
|
providerState: resolveProviderState(config, inputs.baseUrl, models),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,22 +299,6 @@ export function computeModelListVersion(models: ProviderModelConfig[]): string {
|
||||||
return createHash('sha256').update(JSON.stringify(models)).digest('hex');
|
return createHash('sha256').update(JSON.stringify(models)).digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Version of a provider's built-in template (default models only).
|
|
||||||
*
|
|
||||||
* The version recorded at install time and the one recomputed at launch to
|
|
||||||
* detect a pending update must hash the same input, or the update prompt can
|
|
||||||
* never clear. Both sides call this instead of composing
|
|
||||||
* `buildProviderTemplate` + `computeModelListVersion` by hand, so the two
|
|
||||||
* cannot drift apart again.
|
|
||||||
*/
|
|
||||||
export function computeProviderTemplateVersion(
|
|
||||||
config: ProviderConfig,
|
|
||||||
baseUrl: string,
|
|
||||||
): string {
|
|
||||||
return computeModelListVersion(buildProviderTemplate(config, baseUrl));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default base URLs per protocol, used as placeholder/fallback when the user
|
* Default base URLs per protocol, used as placeholder/fallback when the user
|
||||||
* doesn't supply one for a custom provider. Kept in core so the CLI flow
|
* doesn't supply one for a custom provider. Kept in core so the CLI flow
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue