






















@@ -2,28 +2,104 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
22import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
33import { createProviderRuntimeTestMock } from "./model.provider-runtime.test-support.js";
445-vi.mock("../model-suppression.js", () => ({
6-shouldSuppressBuiltInModel: ({ provider, id }: { provider?: string; id?: string }) =>
7-((provider === "openai" ||
8-provider === "azure-openai-responses" ||
9-provider === "openai-codex") &&
10-id?.trim().toLowerCase() === "gpt-5.3-codex-spark") ||
11-(provider === "openai-codex" && id?.trim().toLowerCase() === "gpt-5.4-mini"),
12-buildSuppressedBuiltInModelError: ({ provider, id }: { provider?: string; id?: string }) => {
13-if (provider === "openai-codex" && id?.trim().toLowerCase() === "gpt-5.4-mini") {
14-return "Unknown model: openai-codex/gpt-5.4-mini. gpt-5.4-mini is not supported by the OpenAI Codex OAuth route. Use openai/gpt-5.4-mini with an OpenAI API key or openai-codex/gpt-5.5 with Codex OAuth.";
5+vi.mock("../model-suppression.js", () => {
6+// Mirrors the canonical manifest-driven suppression in
7+// extensions/qwen/openclaw.plugin.json and src/plugins/manifest-model-suppression.ts.
8+function isQwenCodingPlanBaseUrl(value: string | undefined): boolean {
9+const trimmed = value?.trim();
10+if (!trimmed) {
11+return false;
1512}
16-if (
17-(provider !== "openai" &&
18-provider !== "azure-openai-responses" &&
19-provider !== "openai-codex") ||
20-id?.trim().toLowerCase() !== "gpt-5.3-codex-spark"
21-) {
13+try {
14+const hostname = new URL(trimmed).hostname.toLowerCase().replace(/\.+$/, "");
15+return (
16+hostname === "coding.dashscope.aliyuncs.com" ||
17+hostname === "coding-intl.dashscope.aliyuncs.com"
18+);
19+} catch {
20+return false;
21+}
22+}
23+24+function resolveConfiguredQwenBaseUrl(config: unknown): string | undefined {
25+const providers = (config as { models?: { providers?: Record<string, { baseUrl?: string }> } })
26+?.models?.providers;
27+if (!providers) {
2228return undefined;
2329}
24-return `Unknown model: ${provider}/gpt-5.3-codex-spark. gpt-5.3-codex-spark is no longer exposed by the OpenAI or Codex catalogs. Use openai/gpt-5.5.`;
25-},
26-}));
30+for (const [provider, entry] of Object.entries(providers)) {
31+const normalizedProvider = provider.trim().toLowerCase();
32+if (normalizedProvider !== "qwen" && normalizedProvider !== "modelstudio") {
33+continue;
34+}
35+const baseUrl = entry?.baseUrl?.trim();
36+if (baseUrl) {
37+return baseUrl;
38+}
39+}
40+return undefined;
41+}
42+43+return {
44+shouldSuppressBuiltInModel: ({
45+ provider,
46+ id,
47+ baseUrl,
48+ config,
49+}: {
50+provider?: string;
51+id?: string;
52+baseUrl?: string;
53+config?: unknown;
54+}) => {
55+if (
56+(provider === "openai" ||
57+provider === "azure-openai-responses" ||
58+provider === "openai-codex") &&
59+id?.trim().toLowerCase() === "gpt-5.3-codex-spark"
60+) {
61+return true;
62+}
63+if (provider === "openai-codex" && id?.trim().toLowerCase() === "gpt-5.4-mini") {
64+return true;
65+}
66+return (
67+(provider === "qwen" || provider === "modelstudio") &&
68+id?.trim().toLowerCase() === "qwen3.6-plus" &&
69+isQwenCodingPlanBaseUrl(baseUrl ?? resolveConfiguredQwenBaseUrl(config))
70+);
71+},
72+buildSuppressedBuiltInModelError: ({
73+ provider,
74+ id,
75+ config,
76+}: {
77+provider?: string;
78+id?: string;
79+config?: unknown;
80+}) => {
81+if (
82+(provider === "qwen" || provider === "modelstudio") &&
83+id?.trim().toLowerCase() === "qwen3.6-plus" &&
84+isQwenCodingPlanBaseUrl(resolveConfiguredQwenBaseUrl(config))
85+) {
86+return "Unknown model: qwen/qwen3.6-plus. qwen3.6-plus is not supported on the Qwen Coding Plan endpoint; use a Standard pay-as-you-go Qwen endpoint or choose qwen/qwen3.5-plus.";
87+}
88+if (provider === "openai-codex" && id?.trim().toLowerCase() === "gpt-5.4-mini") {
89+return "Unknown model: openai-codex/gpt-5.4-mini. gpt-5.4-mini is not supported by the OpenAI Codex OAuth route. Use openai/gpt-5.4-mini with an OpenAI API key or openai-codex/gpt-5.5 with Codex OAuth.";
90+}
91+if (
92+(provider === "openai" ||
93+provider === "azure-openai-responses" ||
94+provider === "openai-codex") &&
95+id?.trim().toLowerCase() === "gpt-5.3-codex-spark"
96+) {
97+return `Unknown model: ${provider}/gpt-5.3-codex-spark. gpt-5.3-codex-spark is no longer exposed by the OpenAI or Codex catalogs. Use openai/gpt-5.5.`;
98+}
99+return undefined;
100+},
101+};
102+});
2710328104vi.mock("../pi-model-discovery.js", () => ({
29105discoverAuthStorage: vi.fn(() => ({ mocked: true })),
@@ -222,6 +298,63 @@ describe("resolveModel", () => {
222298expect(getModelProviderRequestTransport(result.model ?? {})).toBeUndefined();
223299});
224300301+it("resolves explicitly configured qwen3.6-plus before Coding Plan built-in suppression", () => {
302+const cfg = {
303+models: {
304+providers: {
305+qwen: {
306+baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
307+api: "openai-completions",
308+models: [
309+{
310+id: "qwen3.6-plus",
311+name: "qwen3.6-plus",
312+input: ["text", "image"],
313+reasoning: false,
314+contextWindow: 1_000_000,
315+maxTokens: 65_536,
316+},
317+],
318+},
319+},
320+},
321+} as unknown as OpenClawConfig;
322+323+const result = resolveModelForTest("qwen", "qwen3.6-plus", "/tmp/agent", cfg);
324+325+expect(result.error).toBeUndefined();
326+expect(result.model).toMatchObject({
327+provider: "qwen",
328+id: "qwen3.6-plus",
329+api: "openai-completions",
330+baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
331+input: ["text", "image"],
332+contextWindow: 1_000_000,
333+maxTokens: 65_536,
334+});
335+});
336+337+it("keeps unconfigured qwen3.6-plus suppressed on Coding Plan endpoints", () => {
338+const cfg = {
339+models: {
340+providers: {
341+qwen: {
342+baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
343+api: "openai-completions",
344+models: [],
345+},
346+},
347+},
348+} as unknown as OpenClawConfig;
349+350+const result = resolveModelForTest("qwen", "qwen3.6-plus", "/tmp/agent", cfg);
351+352+expect(result.model).toBeUndefined();
353+expect(result.error).toBe(
354+"Unknown model: qwen/qwen3.6-plus. qwen3.6-plus is not supported on the Qwen Coding Plan endpoint; use a Standard pay-as-you-go Qwen endpoint or choose qwen/qwen3.5-plus.",
355+);
356+});
357+225358it("normalizes Google fallback baseUrls for custom providers", () => {
226359const cfg = {
227360models: {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。