mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-23 12:44:02 +00:00
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:
parent
8f94b018bd
commit
7228d73d80
4 changed files with 116 additions and 2 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export {
|
|||
getAllProviderBaseUrls,
|
||||
idealabProvider,
|
||||
minimaxProvider,
|
||||
modelscopeProvider,
|
||||
openRouterProvider,
|
||||
THIRD_PARTY_PROVIDERS,
|
||||
tokenPlanProvider,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue