




























@@ -3,6 +3,18 @@ import { INTERNAL_RUNTIME_CONTEXT_BEGIN, INTERNAL_RUNTIME_CONTEXT_END } from "./
33import { LiveSessionModelSwitchError } from "./live-model-switch-error.js";
4455const state = vi.hoisted(() => ({
6+defaultRuntimeConfig: {
7+agents: {
8+defaults: {
9+models: {
10+"anthropic/claude": {},
11+"openai/claude": {},
12+"openai/gpt-5.4": {},
13+},
14+},
15+},
16+},
17+runtimeConfigMock: undefined as unknown,
618acpResolveSessionMock: vi.fn((..._args: unknown[]): unknown => null),
719acpRunTurnMock: vi.fn((..._args: unknown[]): unknown => undefined),
820buildAcpResultMock: vi.fn(),
@@ -22,6 +34,8 @@ const state = vi.hoisted(() => ({
2234trajectoryRecordEventMock: vi.fn(),
2335trajectoryFlushMock: vi.fn(async () => undefined),
2436clearSessionAuthProfileOverrideMock: vi.fn(),
37+isThinkingLevelSupportedMock: vi.fn((_args: unknown) => true),
38+resolveThinkingDefaultMock: vi.fn((_args: unknown) => "low"),
2539authProfileStoreMock: { profiles: {} } as { profiles: Record<string, unknown> },
2640sessionEntryMock: undefined as unknown,
2741sessionStoreMock: undefined as unknown,
@@ -109,7 +123,7 @@ vi.mock("../auto-reply/thinking.js", () => ({
109123formatXHighModelHint: () => "model-x",
110124normalizeThinkLevel: (v?: string) => v || undefined,
111125normalizeVerboseLevel: (v?: string) => v || undefined,
112-isThinkingLevelSupported: () => true,
126+isThinkingLevelSupported: (args: unknown) => state.isThinkingLevelSupportedMock(args),
113127resolveSupportedThinkingLevel: ({ level }: { level?: string }) => level,
114128supportsXHighThinking: () => false,
115129}));
@@ -134,39 +148,18 @@ vi.mock("../cli/deps.js", () => ({
134148}));
135149136150vi.mock("../config/io.js", () => ({
137-getRuntimeConfig: () => ({
138-agents: {
139-defaults: {
140-models: {
141-"anthropic/claude": {},
142-"openai/claude": {},
143-"openai/gpt-5.4": {},
144-},
145-},
146-},
147-}),
151+getRuntimeConfig: () => state.runtimeConfigMock ?? state.defaultRuntimeConfig,
148152readConfigFileSnapshotForWrite: async () => ({
149153snapshot: { valid: false },
150154}),
151155}));
152156153157vi.mock("./agent-runtime-config.js", () => {
154-const cfg = {
155-agents: {
156-defaults: {
157-models: {
158-"anthropic/claude": {},
159-"openai/claude": {},
160-"openai/gpt-5.4": {},
161-},
162-},
163-},
164-};
165158return {
166159resolveAgentRuntimeConfig: async () => ({
167-loadedRaw: cfg,
168-sourceConfig: cfg,
169- cfg,
160+loadedRaw: state.runtimeConfigMock ?? state.defaultRuntimeConfig,
161+sourceConfig: state.runtimeConfigMock ?? state.defaultRuntimeConfig,
162+cfg: state.runtimeConfigMock ?? state.defaultRuntimeConfig,
170163}),
171164};
172165});
@@ -311,12 +304,50 @@ vi.mock("./model-selection.js", () => ({
311304allowedCatalog: [],
312305allowAny: false,
313306}),
307+buildConfiguredModelCatalog: ({ cfg }: { cfg?: unknown }) => {
308+const providers = (cfg as { models?: { providers?: Record<string, { models?: unknown[] }> } })
309+?.models?.providers;
310+if (!providers) {
311+return [];
312+}
313+return Object.entries(providers).flatMap(([provider, entry]) =>
314+Array.isArray(entry?.models)
315+ ? entry.models
316+.filter(
317+(model): model is Record<string, unknown> => !!model && typeof model === "object",
318+)
319+.map((model) => {
320+const id = typeof model.id === "string" ? model.id : "";
321+return {
322+ provider,
323+ id,
324+name: typeof model.name === "string" ? model.name : id,
325+reasoning: typeof model.reasoning === "boolean" ? model.reasoning : undefined,
326+compat: model.compat,
327+};
328+})
329+.filter((model) => model.id)
330+ : [],
331+);
332+},
314333modelKey: (p: string, m: string) => `${p}/${m}`,
315334normalizeModelRef: (p: string, m: string) => ({ provider: p, model: m }),
316335parseModelRef: (m: string, p: string) => ({ provider: p, model: m }),
317-resolveConfiguredModelRef: () => ({ provider: "anthropic", model: "claude" }),
318-resolveDefaultModelForAgent: () => ({ provider: "anthropic", model: "claude" }),
319-resolveThinkingDefault: () => "low",
336+resolveConfiguredModelRef: ({ cfg }: { cfg?: unknown }) => {
337+const raw = (cfg as { agents?: { defaults?: { model?: string | { primary?: string } } } })
338+?.agents?.defaults?.model;
339+const primary = typeof raw === "string" ? raw : raw?.primary;
340+const [provider, ...modelParts] = (primary ?? "anthropic/claude").split("/");
341+return { provider, model: modelParts.join("/") || "claude" };
342+},
343+resolveDefaultModelForAgent: ({ cfg }: { cfg?: unknown }) => {
344+const raw = (cfg as { agents?: { defaults?: { model?: string | { primary?: string } } } })
345+?.agents?.defaults?.model;
346+const primary = typeof raw === "string" ? raw : raw?.primary;
347+const [provider, ...modelParts] = (primary ?? "anthropic/claude").split("/");
348+return { provider, model: modelParts.join("/") || "claude" };
349+},
350+resolveThinkingDefault: (args: unknown) => state.resolveThinkingDefaultMock(args),
320351}));
321352322353vi.mock("./provider-auth-aliases.js", () => ({
@@ -446,6 +477,9 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
446477state.resolveAcpAgentPolicyErrorMock.mockReturnValue(null);
447478state.resolveAcpDispatchPolicyErrorMock.mockReturnValue(null);
448479state.resolveAcpExplicitTurnPolicyErrorMock.mockReturnValue(null);
480+state.runtimeConfigMock = undefined;
481+state.isThinkingLevelSupportedMock.mockReturnValue(true);
482+state.resolveThinkingDefaultMock.mockReturnValue("low");
449483state.acpRunTurnMock.mockImplementation(async (params: unknown) => {
450484const onEvent = (params as { onEvent?: (event: unknown) => void }).onEvent;
451485onEvent?.({ type: "text_delta", stream: "output", text: "done" });
@@ -506,6 +540,62 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
506540expect(lifecycleEndCalls.length).toBeGreaterThanOrEqual(1);
507541});
508542543+it("validates explicit thinking against configured model compat without an allowlist", async () => {
544+state.runtimeConfigMock = {
545+agents: {
546+defaults: {
547+model: { primary: "gmn/gpt-5.4" },
548+},
549+},
550+models: {
551+providers: {
552+gmn: {
553+models: [
554+{
555+id: "gpt-5.4",
556+name: "GPT 5.4 via GMN",
557+reasoning: true,
558+compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
559+},
560+],
561+},
562+},
563+},
564+};
565+state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
566+const result = await params.run(params.provider, params.model);
567+return {
568+ result,
569+provider: params.provider,
570+model: params.model,
571+attempts: [],
572+};
573+});
574+state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("gmn", "gpt-5.4"));
575+576+await agentCommand({
577+message: "hello",
578+to: "+1234567890",
579+senderIsOwner: true,
580+thinking: "xhigh",
581+});
582+583+expect(state.isThinkingLevelSupportedMock).toHaveBeenCalledWith(
584+expect.objectContaining({
585+provider: "gmn",
586+model: "gpt-5.4",
587+level: "xhigh",
588+catalog: [
589+expect.objectContaining({
590+provider: "gmn",
591+id: "gpt-5.4",
592+compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
593+}),
594+],
595+}),
596+);
597+});
598+509599it("records fallback steps to the session trajectory runtime", async () => {
510600state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
511601await params.onFallbackStep?.({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。