@vellumai/assistant 0.10.7-dev.202607082329.50e70cf → 0.10.7-dev.202607090141.ed32fa2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/attachments-store.test.ts +3 -1
- package/src/__tests__/openai-provider.test.ts +11 -0
- package/src/__tests__/openrouter-provider-only.test.ts +11 -0
- package/src/__tests__/vercel-ai-gateway-provider.test.ts +12 -0
- package/src/cli/commands/plugins.ts +2 -1
- package/src/config/default-profile-catalog.ts +4 -3
- package/src/providers/__tests__/retry-callsite.test.ts +86 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
- package/src/providers/openai/chat-completions-provider.ts +58 -6
- package/src/providers/retry.ts +22 -1
package/package.json
CHANGED
|
@@ -192,6 +192,8 @@ describe("uploadAttachment", () => {
|
|
|
192
192
|
expect(() => uploadAttachment("ok.txt", "text/plain", "AAA")).not.toThrow();
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
+
// Inserting the 100 MB payload into SQLite can take several seconds on
|
|
196
|
+
// slow CI runners, so this test needs more than the default 5s timeout.
|
|
195
197
|
test("accepts payload exactly at MAX_UPLOAD_BYTES", () => {
|
|
196
198
|
// MAX_UPLOAD_BYTES (100 MB) is divisible by 3, so (MAX/3)*4 base64 chars
|
|
197
199
|
// decodes to exactly MAX bytes with no padding.
|
|
@@ -201,7 +203,7 @@ describe("uploadAttachment", () => {
|
|
|
201
203
|
expect(() =>
|
|
202
204
|
uploadAttachment("exact.bin", "application/octet-stream", exactData),
|
|
203
205
|
).not.toThrow();
|
|
204
|
-
});
|
|
206
|
+
}, 30_000);
|
|
205
207
|
});
|
|
206
208
|
|
|
207
209
|
// ---------------------------------------------------------------------------
|
|
@@ -1622,6 +1622,17 @@ describe("OpenRouterProvider reasoning", () => {
|
|
|
1622
1622
|
expect(lastCreateParams!.reasoning).toBeUndefined();
|
|
1623
1623
|
});
|
|
1624
1624
|
|
|
1625
|
+
test("disabled thinking with effort none sends the flat opt-out and no nested reasoning", async () => {
|
|
1626
|
+
const provider = new OpenRouterProvider("or-key", "x-ai/grok-4");
|
|
1627
|
+
await provider.sendMessage([userMsg("hi")], {
|
|
1628
|
+
config: { thinking: { type: "disabled" }, effort: "none" },
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
expect(lastCreateParams).toBeTruthy();
|
|
1632
|
+
expect(lastCreateParams!.reasoning).toBeUndefined();
|
|
1633
|
+
expect(lastCreateParams!.reasoning_effort).toBe("none");
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1625
1636
|
test("omits reasoning when thinking config is absent", async () => {
|
|
1626
1637
|
const provider = new OpenRouterProvider("or-key", "x-ai/grok-4");
|
|
1627
1638
|
await provider.sendMessage([userMsg("hi")], {
|
|
@@ -193,6 +193,17 @@ describe("OpenRouter provider.only plumbing", () => {
|
|
|
193
193
|
expect(extras.reasoning).toBe(undefined);
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
+
test("disabled thinking with effort none leaves the opt-out to the flat reasoning_effort field", () => {
|
|
197
|
+
const provider = new ProbeOpenRouterProvider(
|
|
198
|
+
"fake-key",
|
|
199
|
+
"x-ai/grok-4.20-beta",
|
|
200
|
+
);
|
|
201
|
+
const extras = provider.probeExtras({
|
|
202
|
+
config: { thinking: { type: "disabled" }, effort: "none" },
|
|
203
|
+
});
|
|
204
|
+
expect(extras).toEqual({});
|
|
205
|
+
});
|
|
206
|
+
|
|
196
207
|
test("omitting reasoning avoids 400 from reasoning-only models like DeepSeek R1", () => {
|
|
197
208
|
const provider = new ProbeOpenRouterProvider(
|
|
198
209
|
"fake-key",
|
|
@@ -190,6 +190,18 @@ describe("VercelAIGatewayProvider", () => {
|
|
|
190
190
|
).toEqual({});
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
test("disabled thinking with effort none nests an explicit reasoning opt-out", () => {
|
|
194
|
+
const provider = new ProbeVercelAIGatewayProvider(
|
|
195
|
+
"fake-key",
|
|
196
|
+
"openai/gpt-5.5",
|
|
197
|
+
);
|
|
198
|
+
expect(
|
|
199
|
+
provider.probeExtras({
|
|
200
|
+
config: { thinking: { type: "disabled" }, effort: "none" },
|
|
201
|
+
}),
|
|
202
|
+
).toEqual({ reasoning: { effort: "none" } });
|
|
203
|
+
});
|
|
204
|
+
|
|
193
205
|
test("final wire params carry reasoning object and no flat reasoning_effort", async () => {
|
|
194
206
|
const provider = new VercelAIGatewayProvider(
|
|
195
207
|
"fake-key",
|
|
@@ -85,7 +85,8 @@ export function registerPluginsCommand(program: Command): void {
|
|
|
85
85
|
registerCommand(program, {
|
|
86
86
|
name: "plugins",
|
|
87
87
|
transport: "local",
|
|
88
|
-
description:
|
|
88
|
+
description:
|
|
89
|
+
"List, search, install, and manage external plugins (`list` shows what is installed, `search` queries the marketplace)",
|
|
89
90
|
build: (plugins) => {
|
|
90
91
|
plugins.addHelpText(
|
|
91
92
|
"after",
|
|
@@ -95,9 +95,10 @@ const VELLUM_PROFILE_IMPLS: Record<DefaultProfileKey, DefaultProfileTemplate> =
|
|
|
95
95
|
label: "Speed",
|
|
96
96
|
description: "Fastest responses at lower cost (DeepSeek V4 Flash)",
|
|
97
97
|
maxTokens: 8192,
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
98
|
+
// Explicit reasoning opt-out. OpenAI-compat APIs default reasoning to
|
|
99
|
+
// "medium" when the field is omitted, and effort-driven providers encode
|
|
100
|
+
// disabled thinking through this same knob (see
|
|
101
|
+
// DISABLED_THINKING_USES_EFFORT_PROVIDERS in providers/retry.ts).
|
|
101
102
|
effort: "none",
|
|
102
103
|
thinking: { enabled: false, streamThinking: false },
|
|
103
104
|
contextWindow: {
|
|
@@ -543,6 +543,92 @@ describe("RetryProvider — callSite resolution", () => {
|
|
|
543
543
|
expect(config.effort).toBe("none");
|
|
544
544
|
});
|
|
545
545
|
|
|
546
|
+
test("disabled thinking forces effort none for OpenRouter and keeps the wire thinking config", async () => {
|
|
547
|
+
setLlmConfig({
|
|
548
|
+
default: {
|
|
549
|
+
provider: "openrouter",
|
|
550
|
+
model: "x-ai/grok-4",
|
|
551
|
+
effort: "high",
|
|
552
|
+
thinking: { enabled: false, streamThinking: false },
|
|
553
|
+
},
|
|
554
|
+
callSites: { mainAgent: {} },
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
let seen: SendMessageOptions | undefined;
|
|
558
|
+
const wrapped = new RetryProvider(
|
|
559
|
+
makeProvider("openrouter", (options) => {
|
|
560
|
+
seen = options;
|
|
561
|
+
}),
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
await wrapped.sendMessage(DUMMY_MESSAGES, {
|
|
565
|
+
config: { callSite: "mainAgent" },
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
const config = seen?.config as Record<string, unknown>;
|
|
569
|
+
expect(config.effort).toBe("none");
|
|
570
|
+
// OpenRouter is thinking-aware: the wire-shape config still flows down so
|
|
571
|
+
// the client can decide its nested `reasoning` emission.
|
|
572
|
+
expect(config.thinking).toEqual({ type: "disabled" });
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("disabled thinking forces effort none for the Vercel AI Gateway", async () => {
|
|
576
|
+
setLlmConfig({
|
|
577
|
+
default: {
|
|
578
|
+
provider: "vercel-ai-gateway",
|
|
579
|
+
model: "xai/grok-4",
|
|
580
|
+
effort: "max",
|
|
581
|
+
thinking: { enabled: false, streamThinking: false },
|
|
582
|
+
},
|
|
583
|
+
callSites: { mainAgent: {} },
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
let seen: SendMessageOptions | undefined;
|
|
587
|
+
const wrapped = new RetryProvider(
|
|
588
|
+
makeProvider("vercel-ai-gateway", (options) => {
|
|
589
|
+
seen = options;
|
|
590
|
+
}),
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
await wrapped.sendMessage(DUMMY_MESSAGES, {
|
|
594
|
+
config: { callSite: "mainAgent" },
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const config = seen?.config as Record<string, unknown>;
|
|
598
|
+
expect(config.effort).toBe("none");
|
|
599
|
+
expect(config.thinking).toEqual({ type: "disabled" });
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test("disabled thinking keeps effort for gateway-delegated anthropic/* models", async () => {
|
|
603
|
+
setLlmConfig({
|
|
604
|
+
default: {
|
|
605
|
+
provider: "openrouter",
|
|
606
|
+
model: "anthropic/claude-opus-4-8",
|
|
607
|
+
effort: "high",
|
|
608
|
+
thinking: { enabled: false, streamThinking: false },
|
|
609
|
+
},
|
|
610
|
+
callSites: { mainAgent: {} },
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
let seen: SendMessageOptions | undefined;
|
|
614
|
+
const wrapped = new RetryProvider(
|
|
615
|
+
makeProvider("openrouter", (options) => {
|
|
616
|
+
seen = options;
|
|
617
|
+
}),
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
await wrapped.sendMessage(DUMMY_MESSAGES, {
|
|
621
|
+
config: { callSite: "mainAgent" },
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// The Anthropic Messages delegate honors a disabled `thinking` natively;
|
|
625
|
+
// `effort` keeps its Anthropic meaning and must match the direct
|
|
626
|
+
// `anthropic` provider's behavior.
|
|
627
|
+
const config = seen?.config as Record<string, unknown>;
|
|
628
|
+
expect(config.effort).toBe("high");
|
|
629
|
+
expect(config.thinking).toEqual({ type: "disabled" });
|
|
630
|
+
});
|
|
631
|
+
|
|
546
632
|
test("preserves thinking + level for Gemini provider", async () => {
|
|
547
633
|
setLlmConfig({
|
|
548
634
|
default: {
|
|
@@ -528,3 +528,115 @@ describe("OpenAIChatCompletionsProvider reasoning parsing", () => {
|
|
|
528
528
|
expect(params.messages[0].reasoning_content).toBe("deepseek thinking");
|
|
529
529
|
});
|
|
530
530
|
});
|
|
531
|
+
|
|
532
|
+
describe("reasoning opt-out rejection fallback", () => {
|
|
533
|
+
function stubProviderWithErrors(
|
|
534
|
+
errors: unknown[],
|
|
535
|
+
chunks: MockChunk[],
|
|
536
|
+
): { provider: OpenAIChatCompletionsProvider; requests: unknown[] } {
|
|
537
|
+
const provider = new OpenAIChatCompletionsProvider(
|
|
538
|
+
"test-key",
|
|
539
|
+
"test-model",
|
|
540
|
+
);
|
|
541
|
+
const requests: unknown[] = [];
|
|
542
|
+
const pending = [...errors];
|
|
543
|
+
(provider as unknown as { client: unknown }).client = {
|
|
544
|
+
chat: {
|
|
545
|
+
completions: {
|
|
546
|
+
create: async (params: unknown) => {
|
|
547
|
+
// Snapshot: the fallback mutates `params` between attempts.
|
|
548
|
+
requests.push(JSON.parse(JSON.stringify(params)));
|
|
549
|
+
const error = pending.shift();
|
|
550
|
+
if (error !== undefined) {
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
return makeStream(chunks);
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
return { provider, requests };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const okChunks: MockChunk[] = [
|
|
562
|
+
{
|
|
563
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
564
|
+
usage: { prompt_tokens: 1, completion_tokens: 1 },
|
|
565
|
+
},
|
|
566
|
+
];
|
|
567
|
+
|
|
568
|
+
function rejection(message: string, status = 400): Error {
|
|
569
|
+
return Object.assign(new Error(message), { status });
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
test("retries once without reasoning params when a model rejects the explicit opt-out", async () => {
|
|
573
|
+
const { provider, requests } = stubProviderWithErrors(
|
|
574
|
+
[rejection("reasoning_effort 'none' is not supported for this model")],
|
|
575
|
+
okChunks,
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const response = await provider.sendMessage(
|
|
579
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
580
|
+
{ config: { effort: "none" } },
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
expect(requests).toHaveLength(2);
|
|
584
|
+
const first = requests[0] as { reasoning_effort?: string };
|
|
585
|
+
const second = requests[1] as {
|
|
586
|
+
reasoning_effort?: string;
|
|
587
|
+
reasoning?: unknown;
|
|
588
|
+
};
|
|
589
|
+
expect(first.reasoning_effort).toBe("none");
|
|
590
|
+
expect(second.reasoning_effort).toBeUndefined();
|
|
591
|
+
expect(second.reasoning).toBeUndefined();
|
|
592
|
+
const text = response.content.find((b) => b.type === "text") as
|
|
593
|
+
| { type: "text"; text: string }
|
|
594
|
+
| undefined;
|
|
595
|
+
expect(text?.text).toBe("ok");
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("does not retry when the request did not opt out of reasoning", async () => {
|
|
599
|
+
const { provider, requests } = stubProviderWithErrors(
|
|
600
|
+
[rejection("reasoning_effort is invalid")],
|
|
601
|
+
okChunks,
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
await expect(
|
|
605
|
+
provider.sendMessage(
|
|
606
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
607
|
+
{ config: { effort: "high" } },
|
|
608
|
+
),
|
|
609
|
+
).rejects.toThrow();
|
|
610
|
+
expect(requests).toHaveLength(1);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test("does not retry a 4xx that does not name reasoning", async () => {
|
|
614
|
+
const { provider, requests } = stubProviderWithErrors(
|
|
615
|
+
[rejection("invalid api key")],
|
|
616
|
+
okChunks,
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
await expect(
|
|
620
|
+
provider.sendMessage(
|
|
621
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
622
|
+
{ config: { effort: "none" } },
|
|
623
|
+
),
|
|
624
|
+
).rejects.toThrow();
|
|
625
|
+
expect(requests).toHaveLength(1);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("does not retry server errors", async () => {
|
|
629
|
+
const { provider, requests } = stubProviderWithErrors(
|
|
630
|
+
[rejection("reasoning backend unavailable", 500)],
|
|
631
|
+
okChunks,
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
await expect(
|
|
635
|
+
provider.sendMessage(
|
|
636
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
637
|
+
{ config: { effort: "none" } },
|
|
638
|
+
),
|
|
639
|
+
).rejects.toThrow();
|
|
640
|
+
expect(requests).toHaveLength(1);
|
|
641
|
+
});
|
|
642
|
+
});
|
|
@@ -3,6 +3,7 @@ import OpenAI from "openai";
|
|
|
3
3
|
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../prompts/cache-boundary.js";
|
|
4
4
|
import { isAbortReason } from "../../util/abort-reasons.js";
|
|
5
5
|
import { ProviderError } from "../../util/errors.js";
|
|
6
|
+
import { getLogger } from "../../util/logger.js";
|
|
6
7
|
import { extractRetryAfterMs } from "../../util/retry.js";
|
|
7
8
|
import { escapeXmlAttr } from "../../util/xml.js";
|
|
8
9
|
import { base64Source, resolveMediaReferences } from "../media-resolve.js";
|
|
@@ -150,6 +151,8 @@ export interface OpenAIChatCompletionsProviderOptions {
|
|
|
150
151
|
coerceObjectArgsToJsonString?: boolean;
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
const log = getLogger("chat-completions");
|
|
155
|
+
|
|
153
156
|
/** Wire-level reasoning_effort values. The OpenAI SDK type doesn't include
|
|
154
157
|
* `"max"`, but Fireworks accepts it for DeepSeek V4; the assignment to
|
|
155
158
|
* `params.reasoning_effort` casts through this union. */
|
|
@@ -188,6 +191,35 @@ export function clampReasoningEffort(
|
|
|
188
191
|
: value;
|
|
189
192
|
}
|
|
190
193
|
|
|
194
|
+
/**
|
|
195
|
+
* True when the request carried an explicit reasoning opt-out (`"none"` sent
|
|
196
|
+
* as flat `reasoning_effort` or nested `reasoning.effort`) and the provider
|
|
197
|
+
* rejected it with a 4xx that names the reasoning field. Reasoning-only
|
|
198
|
+
* models (e.g. DeepSeek R1) reject the opt-out rather than ignore it; that
|
|
199
|
+
* one case is worth a single retry with the reasoning params stripped —
|
|
200
|
+
* model-default reasoning beats a hard failure.
|
|
201
|
+
*/
|
|
202
|
+
function isReasoningOptOutRejection(error: unknown, params: unknown): boolean {
|
|
203
|
+
const p = params as {
|
|
204
|
+
reasoning_effort?: unknown;
|
|
205
|
+
reasoning?: { effort?: unknown } | null;
|
|
206
|
+
};
|
|
207
|
+
const optedOut =
|
|
208
|
+
p.reasoning_effort === "none" ||
|
|
209
|
+
(typeof p.reasoning === "object" &&
|
|
210
|
+
p.reasoning !== null &&
|
|
211
|
+
p.reasoning.effort === "none");
|
|
212
|
+
if (!optedOut) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
const status = (error as { status?: unknown }).status;
|
|
216
|
+
if (typeof status !== "number" || status < 400 || status >= 500) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
220
|
+
return /reasoning/i.test(message);
|
|
221
|
+
}
|
|
222
|
+
|
|
191
223
|
/**
|
|
192
224
|
* Translate the neutral (Anthropic-shaped) `tool_choice` carried on the call
|
|
193
225
|
* config into the OpenAI chat-completions wire format. Callers express
|
|
@@ -477,12 +509,32 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
477
509
|
...this.requestHeaders,
|
|
478
510
|
...(usageAttributionHeaders ?? {}),
|
|
479
511
|
};
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
512
|
+
const createStream = () =>
|
|
513
|
+
this.client.chat.completions.create(params, {
|
|
514
|
+
signal: timeoutSignal,
|
|
515
|
+
...(Object.keys(requestHeaders).length > 0
|
|
516
|
+
? { headers: requestHeaders }
|
|
517
|
+
: {}),
|
|
518
|
+
});
|
|
519
|
+
let stream: Awaited<ReturnType<typeof createStream>>;
|
|
520
|
+
try {
|
|
521
|
+
stream = await createStream();
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (!isReasoningOptOutRejection(error, params)) {
|
|
524
|
+
throw error;
|
|
525
|
+
}
|
|
526
|
+
log.warn(
|
|
527
|
+
{
|
|
528
|
+
provider: this.name,
|
|
529
|
+
model: modelOverride ?? this.model,
|
|
530
|
+
error: error instanceof Error ? error.message : String(error),
|
|
531
|
+
},
|
|
532
|
+
"Model rejected the explicit reasoning opt-out; retrying without reasoning params",
|
|
533
|
+
);
|
|
534
|
+
delete params.reasoning_effort;
|
|
535
|
+
delete (params as unknown as Record<string, unknown>).reasoning;
|
|
536
|
+
stream = await createStream();
|
|
537
|
+
}
|
|
486
538
|
|
|
487
539
|
for await (const chunk of stream) {
|
|
488
540
|
const choice = chunk.choices[0];
|
package/src/providers/retry.ts
CHANGED
|
@@ -59,8 +59,29 @@ const DISABLED_THINKING_USES_EFFORT_PROVIDERS = new Set([
|
|
|
59
59
|
"openai",
|
|
60
60
|
"fireworks",
|
|
61
61
|
"together",
|
|
62
|
+
"openrouter",
|
|
63
|
+
"vercel-ai-gateway",
|
|
62
64
|
]);
|
|
63
65
|
|
|
66
|
+
// Whether a disabled `thinking` config must be encoded as `effort: "none"`
|
|
67
|
+
// for this provider/model. Gateway calls that delegate `anthropic/*` models
|
|
68
|
+
// to the Anthropic Messages API are excluded: the delegate honors a disabled
|
|
69
|
+
// `thinking` natively and `effort` keeps its Anthropic meaning there, so
|
|
70
|
+
// forcing it would diverge from the direct `anthropic` provider.
|
|
71
|
+
function disabledThinkingForcesEffortNone(
|
|
72
|
+
providerName: string,
|
|
73
|
+
model: unknown,
|
|
74
|
+
): boolean {
|
|
75
|
+
if (!DISABLED_THINKING_USES_EFFORT_PROVIDERS.has(providerName)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return !(
|
|
79
|
+
isAnthropicDelegatingGateway(providerName) &&
|
|
80
|
+
typeof model === "string" &&
|
|
81
|
+
isAnthropicModel(model)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
/**
|
|
65
86
|
* Providers that consume the `thinking` config. Anthropic uses it directly on
|
|
66
87
|
* the wire; OpenRouter forwards it on its Anthropic delegate path and
|
|
@@ -386,7 +407,7 @@ function normalizeSendMessageOptions(
|
|
|
386
407
|
|
|
387
408
|
if (
|
|
388
409
|
isThinkingConfigDisabled(nextConfig.thinking) &&
|
|
389
|
-
|
|
410
|
+
disabledThinkingForcesEffortNone(providerName, nextConfig.model)
|
|
390
411
|
) {
|
|
391
412
|
nextConfig.effort = "none";
|
|
392
413
|
}
|