@@ -12,6 +12,7 @@ import type { AIService } from "./aiService";
1212import type { SendMessageError } from "@/common/types/errors" ;
1313import { NAME_GEN_MAX_OUTPUT_TOKENS } from "@/common/constants/nameGeneration" ;
1414import type { ThinkingLevel } from "@/common/types/thinking" ;
15+ import type { MuxProviderOptions } from "@/common/types/providerOptions" ;
1516import { attachLanguageModelCleanup } from "./languageModelCleanup" ;
1617
1718afterEach ( ( ) => {
@@ -85,7 +86,8 @@ function createTitleModel(modelId = "title-model"): LanguageModel {
8586function pinnedOptionsFor (
8687 modelString : string ,
8788 model : LanguageModel ,
88- _opts ?: { thinkingLevel ?: ThinkingLevel ; agentInitiated ?: boolean }
89+ _opts ?: { thinkingLevel ?: ThinkingLevel ; agentInitiated ?: boolean } ,
90+ muxProviderOptions : MuxProviderOptions = { }
8991) {
9092 return Ok ( {
9193 model,
@@ -94,7 +96,7 @@ function pinnedOptionsFor(
9496 metadataModel : modelString ,
9597 optionsModelString : modelString ,
9698 optionsProvidersConfig : { } ,
97- optionsMuxProviderOptions : { } ,
99+ optionsMuxProviderOptions : muxProviderOptions ,
98100 } ) ;
99101}
100102
@@ -374,6 +376,82 @@ describe("generateWorkspaceIdentity candidate settings", () => {
374376 }
375377 ) ;
376378
379+ const forcedProposeName = { type : "tool" , toolName : "propose_name" } as const ;
380+ test . each ( [
381+ // Fallback default "off" serializes no Anthropic thinking block.
382+ { name : "Haiku default" , model : "anthropic:claude-haiku-4-5" , forced : true } ,
383+ {
384+ name : "Haiku off" ,
385+ model : "anthropic:claude-haiku-4-5" ,
386+ thinkingLevel : "off" as const ,
387+ forced : true ,
388+ } ,
389+ // Luna "off" serializes reasoningEffort "none".
390+ { name : "Luna default" , model : "openai:gpt-6-luna" , forced : true } ,
391+ // Chat Completions pins Luna to effort "none" at any level.
392+ {
393+ name : "Luna high on Chat Completions" ,
394+ model : "openai:gpt-6-luna" ,
395+ thinkingLevel : "high" as const ,
396+ chat : true ,
397+ forced : true ,
398+ } ,
399+ {
400+ name : "Haiku medium" ,
401+ model : "anthropic:claude-haiku-4-5" ,
402+ thinkingLevel : "medium" as const ,
403+ forced : false ,
404+ } ,
405+ // The per-model floor turns thinking on.
406+ {
407+ name : "Haiku off with low floor" ,
408+ model : "anthropic:claude-haiku-4-5" ,
409+ thinkingLevel : "off" as const ,
410+ minThinkingLevel : "low" as const ,
411+ forced : false ,
412+ } ,
413+ // Policy keeps "off", but this route serializes adaptive thinking.
414+ {
415+ name : "Sonnet 5.5 off" ,
416+ model : "anthropic:claude-sonnet-5-5" ,
417+ thinkingLevel : "off" as const ,
418+ forced : false ,
419+ } ,
420+ {
421+ name : "Luna off with high floor" ,
422+ model : "openai:gpt-6-luna" ,
423+ thinkingLevel : "off" as const ,
424+ minThinkingLevel : "high" as const ,
425+ forced : false ,
426+ } ,
427+ ] ) (
428+ "forces propose_name only when the request serializes thinking off ($name)" ,
429+ async ( { model, thinkingLevel, minThinkingLevel, chat, forced } ) => {
430+ const aiService = titleAIService ( ( modelString ) =>
431+ Promise . resolve (
432+ pinnedOptionsFor (
433+ modelString ,
434+ createTitleModel ( modelString ) ,
435+ undefined ,
436+ chat ? { openai : { wireFormat : "chatCompletions" } } : { }
437+ )
438+ )
439+ ) ;
440+ const streamTextSpy = spyOn ( aiSdk , "streamText" ) . mockReturnValue ( proposeNameStream ) ;
441+
442+ const result = await generateWorkspaceIdentity (
443+ "Add setting" ,
444+ [ { model, thinkingLevel, minThinkingLevel } ] ,
445+ aiService
446+ ) ;
447+
448+ expect ( result . success ) . toBe ( true ) ;
449+ expect ( streamTextSpy . mock . calls [ 0 ] ?. [ 0 ] . toolChoice ) . toEqual (
450+ forced ? forcedProposeName : undefined
451+ ) ;
452+ }
453+ ) ;
454+
377455 test ( "skips a candidate whose model cannot be created and reports the last failure" , async ( ) => {
378456 const createModelWithPinnedOptions = mock ( ( modelString : string ) =>
379457 Promise . resolve (
0 commit comments