fix(providers): restore modelscope test + tighten openrouter ownsModel

Two findings from the latest /review pass that survived earlier rounds:

1. modelscope.test.ts was deleted in the move-from-CLI step (60 lines / 4 cases under packages/cli/src/auth/providers/thirdParty/) but never recreated in core's preset test folder. Re-added a 3-case suite (config shape, install plan with per-model metadata for known IDs, graceful fallback for unknown IDs) so the third-party preset coverage is symmetric again. Also exported modelscopeProvider from packages/core/src/providers/index.ts so the public API matches the other presets.

2. openrouter.ts ownsModel previously claimed any model on an openrouter.ai hostname, which would silently delete a user's hand-added entry that happened to route through openrouter.ai under a different envKey (e.g. a personal gateway). Now requires both model.envKey === OPENROUTER_ENV_KEY AND the openrouter.ai hostname match. Existing openrouter.test.ts updated and extended to cover: matching path, envKey mismatch path, host mismatch path, missing/malformed baseUrl.

The remaining findings in that /review were either already addressed in earlier rounds (custom provider visibility / resolveBaseUrl empty array / useAuth telemetry / TS4111 errors — verified 0 locally) or architectural concerns beyond this PR's scope (LoadedSettings.setValue's per-call saveSettings).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
pomelo-nwu 2026-05-19 16:57:22 +08:00
parent 8f94b018bd
commit 7228d73d80
4 changed files with 116 additions and 2 deletions

View file

@ -0,0 +1,69 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
// Re-import via the relative source path so this test exercises the
// in-tree implementation even before dist/ is rebuilt (the
// @qwen-code/qwen-code-core package main points at dist/ on a fresh
// branch). The provider was deleted from the CLI side in this PR and not
// rebuilt in core's test folder until now.
import { AuthType } from '../../../core/contentGenerator.js';
import { modelscopeProvider } from '../../presets/modelscope.js';
import { buildInstallPlan } from '../../provider-config.js';
describe('modelscopeProvider', () => {
it('has correct provider config', () => {
expect(modelscopeProvider).toMatchObject({
id: 'modelscope',
label: 'ModelScope API Key',
protocol: AuthType.USE_OPENAI,
baseUrl: 'https://api-inference.modelscope.cn/v1',
envKey: 'MODELSCOPE_API_KEY',
});
});
it('creates an install plan with per-model metadata for known IDs', () => {
const plan = buildInstallPlan(modelscopeProvider, {
baseUrl: 'https://api-inference.modelscope.cn/v1',
apiKey: 'sk-modelscope',
modelIds: ['deepseek-ai/DeepSeek-V4-Flash', 'Qwen/Qwen3.5-397B-A17B'],
});
const models = plan.modelProviders?.[0]?.models;
expect(models).toHaveLength(2);
expect(models?.[0]).toMatchObject({
id: 'deepseek-ai/DeepSeek-V4-Flash',
name: '[ModelScope] deepseek-ai/DeepSeek-V4-Flash',
generationConfig: { contextWindowSize: 1000000 },
});
expect(models?.[1]).toMatchObject({
id: 'Qwen/Qwen3.5-397B-A17B',
name: '[ModelScope] Qwen/Qwen3.5-397B-A17B',
generationConfig: { contextWindowSize: 1000000 },
});
});
it('falls back gracefully for unknown model IDs', () => {
const plan = buildInstallPlan(modelscopeProvider, {
baseUrl: 'https://api-inference.modelscope.cn/v1',
apiKey: 'sk-modelscope',
modelIds: ['deepseek-ai/DeepSeek-V4-Flash', 'some-new-model'],
});
const models = plan.modelProviders?.[0]?.models;
expect(models).toHaveLength(2);
// Known model: contextWindowSize is preserved, plus modelscope's
// enableThinking=true adds extra_body.enable_thinking.
expect(models?.[0]?.generationConfig).toMatchObject({
contextWindowSize: 1000000,
});
expect(models?.[1]).toMatchObject({
id: 'some-new-model',
name: '[ModelScope] some-new-model',
});
expect(models?.[1]?.generationConfig).toBeUndefined();
});
});

View file

@ -5,20 +5,59 @@
*/
import { describe, expect, it } from 'vitest';
import { openRouterProvider } from '@qwen-code/qwen-code-core';
// Re-import via the relative source path so the new ownsModel envKey gate
// is exercised even before dist/ is rebuilt (the @qwen-code/qwen-code-core
// package resolves to dist/ on a fresh branch).
import {
openRouterProvider,
OPENROUTER_ENV_KEY,
} from '../../presets/openrouter.js';
describe('openRouterProvider', () => {
it('owns models by OpenRouter base URL', () => {
it('owns models that match BOTH our envKey and an openrouter.ai host', () => {
expect(
openRouterProvider.ownsModel?.({
id: 'openrouter-model',
baseUrl: 'https://openrouter.ai/api/v1',
envKey: OPENROUTER_ENV_KEY,
}),
).toBe(true);
});
it('refuses ownership over a different envKey on the same host (user-added entry)', () => {
// A user wired their own gateway through openrouter.ai with a custom env
// var — re-install must not silently delete their model entry.
expect(
openRouterProvider.ownsModel?.({
id: 'user-added',
baseUrl: 'https://openrouter.ai/api/v1',
envKey: 'MY_PRIVATE_GATEWAY_KEY',
}),
).toBe(false);
});
it('refuses ownership over an unrelated host even with our envKey', () => {
expect(
openRouterProvider.ownsModel?.({
id: 'other-model',
baseUrl: 'https://api.example.com/v1',
envKey: OPENROUTER_ENV_KEY,
}),
).toBe(false);
});
it('refuses ownership when baseUrl is missing or malformed', () => {
expect(
openRouterProvider.ownsModel?.({
id: 'no-url',
envKey: OPENROUTER_ENV_KEY,
}),
).toBe(false);
expect(
openRouterProvider.ownsModel?.({
id: 'bad-url',
baseUrl: 'not a url',
envKey: OPENROUTER_ENV_KEY,
}),
).toBe(false);
});

View file

@ -47,6 +47,7 @@ export {
getAllProviderBaseUrls,
idealabProvider,
minimaxProvider,
modelscopeProvider,
openRouterProvider,
THIRD_PARTY_PROVIDERS,
tokenPlanProvider,

View file

@ -25,6 +25,11 @@ export const openRouterProvider: ProviderConfig = {
modelsEditable: true,
modelNamePrefix: 'OpenRouter',
ownsModel: (model) => {
// A user who manually added an OpenRouter-routed model under a custom
// envKey (e.g. their own gateway) shouldn't have their entry silently
// removed on re-install — require BOTH the hostname *and* our envKey to
// claim ownership.
if (model.envKey !== OPENROUTER_ENV_KEY) return false;
try {
const host = new URL(model.baseUrl ?? '').hostname;
return host === 'openrouter.ai' || host.endsWith('.openrouter.ai');