@@ -10,6 +10,7 @@ import {
1010 buildFoundryConnectionTest ,
1111 isValidTenantIdentifier ,
1212 promptApiKeyEndpointAndModel ,
13+ promptEndpointAndModelManually ,
1314 selectFoundryDeployment ,
1415} from "./onboard.js" ;
1516import { resetFoundryRuntimeAuthCaches } from "./runtime.js" ;
@@ -20,8 +21,8 @@ import {
2021 isAnthropicFoundryDeployment ,
2122 isFoundryMaiImageModel ,
2223 normalizeFoundryEndpoint ,
23- partitionFoundryDeployments ,
2424 requiresFoundryMaxCompletionTokens ,
25+ requiresFoundryEntraIdClaudeAuth ,
2526 supportsFoundryReasoningContent ,
2627 supportsFoundryReasoningEffort ,
2728 supportsFoundryImageInput ,
@@ -895,6 +896,105 @@ describe("microsoft-foundry plugin", () => {
895896 expect ( result . defaultModel ) . toBeUndefined ( ) ;
896897 } ) ;
897898
899+ it ( "keeps API-key manual setup defaulted to chat completions for GPT deployments" , async ( ) => {
900+ const text = vi
901+ . fn ( )
902+ . mockResolvedValueOnce ( "https://example.services.ai.azure.com" )
903+ . mockResolvedValueOnce ( "gpt-4o" ) ;
904+ const select = vi
905+ . fn ( )
906+ . mockImplementationOnce ( async ( params : { initialValue ?: string } ) => {
907+ expect ( params . initialValue ) . toBe ( "other-chat" ) ;
908+ return "other-chat" ;
909+ } )
910+ . mockImplementationOnce ( async ( params : { initialValue ?: string } ) => {
911+ expect ( params . initialValue ) . toBe ( "openai-completions" ) ;
912+ return "openai-completions" ;
913+ } ) ;
914+
915+ const selection = await promptApiKeyEndpointAndModel ( {
916+ prompter : {
917+ text,
918+ select,
919+ } ,
920+ } as never ) ;
921+
922+ expect ( selection ) . toEqual ( {
923+ endpoint : "https://example.services.ai.azure.com" ,
924+ modelId : "gpt-4o" ,
925+ api : "openai-completions" ,
926+ } ) ;
927+ } ) ;
928+
929+ it ( "rejects Entra-only Claude Mythos deployments during API-key manual setup" , async ( ) => {
930+ const text = vi . fn (
931+ async ( params : { message : string ; validate ?: ( value : string ) => string | undefined } ) => {
932+ if ( params . message === "Microsoft Foundry endpoint URL" ) {
933+ return "https://example.services.ai.azure.com" ;
934+ }
935+ if ( params . message === "Default model/deployment name" ) {
936+ return "prod-mythos" ;
937+ }
938+ if ( params . message === "Claude base model" ) {
939+ expect ( params . validate ?.( "claude-fable-5" ) ) . toBeUndefined ( ) ;
940+ expect ( params . validate ?.( "claude-mythos-preview" ) ) . toContain ( "Entra ID auth" ) ;
941+ return "claude-fable-5" ;
942+ }
943+ throw new Error ( `unexpected prompt: ${ params . message } ` ) ;
944+ } ,
945+ ) ;
946+ const select = vi . fn ( ) . mockResolvedValueOnce ( "claude" ) ;
947+
948+ const selection = await promptApiKeyEndpointAndModel ( {
949+ prompter : {
950+ text,
951+ select,
952+ } ,
953+ } as never ) ;
954+
955+ expect ( selection ) . toEqual ( {
956+ endpoint : "https://example.services.ai.azure.com" ,
957+ modelId : "prod-mythos" ,
958+ modelNameHint : "claude-fable-5" ,
959+ api : "anthropic-messages" ,
960+ } ) ;
961+ expect ( requiresFoundryEntraIdClaudeAuth ( "claude-mythos-preview" ) ) . toBe ( true ) ;
962+ expect ( requiresFoundryEntraIdClaudeAuth ( "claude-fable-5" ) ) . toBe ( false ) ;
963+ } ) ;
964+
965+ it ( "allows Entra-only Claude Mythos deployments during Entra manual setup" , async ( ) => {
966+ const text = vi . fn (
967+ async ( params : { message : string ; validate ?: ( value : string ) => string | undefined } ) => {
968+ if ( params . message === "Microsoft Foundry endpoint URL" ) {
969+ return "https://example.services.ai.azure.com" ;
970+ }
971+ if ( params . message === "Default model/deployment name" ) {
972+ return "prod-mythos" ;
973+ }
974+ if ( params . message === "Claude base model" ) {
975+ expect ( params . validate ?.( "claude-mythos-preview" ) ) . toBeUndefined ( ) ;
976+ return "claude-mythos-preview" ;
977+ }
978+ throw new Error ( `unexpected prompt: ${ params . message } ` ) ;
979+ } ,
980+ ) ;
981+ const select = vi . fn ( ) . mockResolvedValueOnce ( "claude" ) ;
982+
983+ const selection = await promptEndpointAndModelManually ( {
984+ prompter : {
985+ text,
986+ select,
987+ } ,
988+ } as never ) ;
989+
990+ expect ( selection ) . toEqual ( {
991+ endpoint : "https://example.services.ai.azure.com" ,
992+ modelId : "prod-mythos" ,
993+ modelNameHint : "claude-mythos-preview" ,
994+ api : "anthropic-messages" ,
995+ } ) ;
996+ } ) ;
997+
898998 it ( "uses discovered deployment metadata for MAI image defaults" , ( ) => {
899999 const result = buildFoundryAuthResult ( {
9001000 profileId : "microsoft-foundry:entra" ,
@@ -1128,6 +1228,85 @@ describe("microsoft-foundry plugin", () => {
11281228 expect ( provider ?. models [ 0 ] ?. compat ?. maxTokensField ) . toBe ( "max_tokens" ) ;
11291229 } ) ;
11301230
1231+ it ( "routes Claude deployments through Foundry Anthropic Messages" , ( ) => {
1232+ const result = buildFoundryAuthResult ( {
1233+ profileId : "microsoft-foundry:entra" ,
1234+ apiKey : "__entra_id_dynamic__" ,
1235+ endpoint : "https://example.services.ai.azure.com/openai/v1" ,
1236+ modelId : "prod-fable" ,
1237+ modelNameHint : "claude-fable-5" ,
1238+ api : "anthropic-messages" ,
1239+ authMethod : "entra-id" ,
1240+ } ) ;
1241+
1242+ const provider = result . configPatch ?. models ?. providers ?. [ "microsoft-foundry" ] ;
1243+ expect ( provider ?. baseUrl ) . toBe ( "https://example.services.ai.azure.com/anthropic" ) ;
1244+ expect ( provider ?. api ) . toBe ( "anthropic-messages" ) ;
1245+ expect ( provider ?. authHeader ) . toBe ( true ) ;
1246+ expect ( provider ?. models [ 0 ] ) . toMatchObject ( {
1247+ id : "prod-fable" ,
1248+ name : "claude-fable-5" ,
1249+ api : "anthropic-messages" ,
1250+ reasoning : true ,
1251+ input : [ "text" , "image" ] ,
1252+ contextWindow : 1_000_000 ,
1253+ maxTokens : 128_000 ,
1254+ thinkingLevelMap : { xhigh : "xhigh" , max : "max" } ,
1255+ } ) ;
1256+ expect ( provider ?. models [ 0 ] ?. compat ) . toBeUndefined ( ) ;
1257+ } ) ;
1258+
1259+ it . each ( [
1260+ "claude-mythos-preview" ,
1261+ "claude-fable-5" ,
1262+ "claude-opus-4.8" ,
1263+ "claude-opus-4.7" ,
1264+ "claude-opus-4.6" ,
1265+ "claude-sonnet-4.6" ,
1266+ ] ) ( "preserves Foundry Claude 1M token limits for %s" , ( modelNameHint ) => {
1267+ const result = buildFoundryAuthResult ( {
1268+ profileId : "microsoft-foundry:entra" ,
1269+ apiKey : "__entra_id_dynamic__" ,
1270+ endpoint : "https://example.services.ai.azure.com" ,
1271+ modelId : `prod-${ modelNameHint . replaceAll ( "." , "-" ) } ` ,
1272+ modelNameHint,
1273+ api : "anthropic-messages" ,
1274+ authMethod : "entra-id" ,
1275+ } ) ;
1276+
1277+ expect ( result . configPatch ?. models ?. providers ?. [ "microsoft-foundry" ] ?. models [ 0 ] ) . toMatchObject ( {
1278+ name : modelNameHint ,
1279+ api : "anthropic-messages" ,
1280+ contextWindow : 1_000_000 ,
1281+ maxTokens : 128_000 ,
1282+ } ) ;
1283+ } ) ;
1284+
1285+ it ( "keeps older Foundry Claude deployments out of Fable-class thinking limits" , ( ) => {
1286+ const result = buildFoundryAuthResult ( {
1287+ profileId : "microsoft-foundry:entra" ,
1288+ apiKey : "__entra_id_dynamic__" ,
1289+ endpoint : "https://example.services.ai.azure.com" ,
1290+ modelId : "prod-claude-35" ,
1291+ modelNameHint : "claude-3.5-sonnet" ,
1292+ api : "anthropic-messages" ,
1293+ authMethod : "entra-id" ,
1294+ } ) ;
1295+
1296+ const model = result . configPatch ?. models ?. providers ?. [ "microsoft-foundry" ] ?. models [ 0 ] ;
1297+ expect ( model ) . toMatchObject ( {
1298+ id : "prod-claude-35" ,
1299+ name : "claude-3.5-sonnet" ,
1300+ api : "anthropic-messages" ,
1301+ reasoning : false ,
1302+ input : [ "text" , "image" ] ,
1303+ contextWindow : 128_000 ,
1304+ maxTokens : 16_384 ,
1305+ } ) ;
1306+ expect ( model ?. thinkingLevelMap ) . toBeUndefined ( ) ;
1307+ expect ( model ?. compat ) . toBeUndefined ( ) ;
1308+ } ) ;
1309+
11311310 it ( "keeps Foundry chat reasoning_effort enabled for GPT-5 reasoning deployments" , ( ) => {
11321311 const result = buildFoundryAuthResult ( {
11331312 profileId : "microsoft-foundry:default" ,
@@ -1366,6 +1545,22 @@ describe("microsoft-foundry plugin", () => {
13661545 expect ( testRequest . body . max_tokens ) . toBe ( 1 ) ;
13671546 } ) ;
13681547
1548+ it ( "builds Anthropic Messages connection tests for Claude deployments" , ( ) => {
1549+ const testRequest = buildFoundryConnectionTest ( {
1550+ endpoint : "https://example.services.ai.azure.com/openai/v1" ,
1551+ modelId : "prod-fable" ,
1552+ modelNameHint : "claude-fable-5" ,
1553+ api : "anthropic-messages" ,
1554+ } ) ;
1555+
1556+ expect ( testRequest . url ) . toBe ( "https://example.services.ai.azure.com/anthropic/v1/messages" ) ;
1557+ expect ( testRequest . body ) . toEqual ( {
1558+ model : "prod-fable" ,
1559+ messages : [ { role : "user" , content : "hi" } ] ,
1560+ max_tokens : 1 ,
1561+ } ) ;
1562+ } ) ;
1563+
13691564 it ( "returns actionable Azure CLI login errors" , async ( ) => {
13701565 mockAzureCliLoginFailure ( ) ;
13711566
@@ -1475,49 +1670,6 @@ describe("microsoft-foundry plugin", () => {
14751670 } ) ;
14761671} ) ;
14771672
1478- describe ( "partitionFoundryDeployments" , ( ) => {
1479- it ( "keeps OpenAI-compatible deployments and skips Claude in mixed resources" , ( ) => {
1480- const { supported, anthropic } = partitionFoundryDeployments ( [
1481- { name : "prod-gpt" , modelName : "gpt-5.4" } ,
1482- { name : "prod-claude" , modelName : "claude-opus-4-6" } ,
1483- { name : "prod-mini" , modelName : "gpt-4o-mini" } ,
1484- ] ) ;
1485-
1486- expect ( supported . map ( ( deployment ) => deployment . name ) ) . toEqual ( [ "prod-gpt" , "prod-mini" ] ) ;
1487- expect ( anthropic . map ( ( deployment ) => deployment . name ) ) . toEqual ( [ "prod-claude" ] ) ;
1488- } ) ;
1489-
1490- it ( "returns no supported deployments when only Anthropic deployments exist" , ( ) => {
1491- const { supported, anthropic } = partitionFoundryDeployments ( [
1492- { name : "only-claude" , modelName : "claude-3.5-sonnet" } ,
1493- ] ) ;
1494-
1495- expect ( supported ) . toEqual ( [ ] ) ;
1496- expect ( anthropic . map ( ( deployment ) => deployment . name ) ) . toEqual ( [ "only-claude" ] ) ;
1497- } ) ;
1498-
1499- it ( "is a no-op for all-OpenAI resources" , ( ) => {
1500- const deployments = [
1501- { name : "prod-gpt" , modelName : "gpt-5.4" } ,
1502- { name : "prod-mini" , modelName : "gpt-4o-mini" } ,
1503- ] ;
1504- const { supported, anthropic } = partitionFoundryDeployments ( deployments ) ;
1505-
1506- expect ( supported ) . toEqual ( deployments ) ;
1507- expect ( anthropic ) . toEqual ( [ ] ) ;
1508- } ) ;
1509-
1510- it ( "classifies by deployment name when modelName is missing" , ( ) => {
1511- const { supported, anthropic } = partitionFoundryDeployments ( [
1512- { name : "claude-opus-4-6" } ,
1513- { name : "gpt-5.4-prod" } ,
1514- ] ) ;
1515-
1516- expect ( supported . map ( ( deployment ) => deployment . name ) ) . toEqual ( [ "gpt-5.4-prod" ] ) ;
1517- expect ( anthropic . map ( ( deployment ) => deployment . name ) ) . toEqual ( [ "claude-opus-4-6" ] ) ;
1518- } ) ;
1519- } ) ;
1520-
15211673describe ( "selectFoundryDeployment" , ( ) => {
15221674 function makeCtx ( overrides : { selectValue ?: string } = { } ) {
15231675 const noteCalls : Array < { message : string ; title : string } > = [ ] ;
@@ -1545,7 +1697,7 @@ describe("selectFoundryDeployment", () => {
15451697 projects : [ ] ,
15461698 } ;
15471699
1548- it ( "offers and returns only supported deployments for mixed GPT and Claude resources" , async ( ) => {
1700+ it ( "offers and returns Claude deployments alongside GPT resources" , async ( ) => {
15491701 const { ctx, selectCalls, noteCalls } = makeCtx ( { selectValue : "prod-gpt" } ) ;
15501702 const result = await selectFoundryDeployment ( ctx , fakeResource , [
15511703 { name : "prod-gpt" , modelName : "gpt-5.4" , state : "Succeeded" } ,
@@ -1555,25 +1707,30 @@ describe("selectFoundryDeployment", () => {
15551707
15561708 expect ( result . supported . map ( ( deployment ) => deployment . name ) ) . toEqual ( [
15571709 "prod-gpt" ,
1710+ "prod-claude" ,
15581711 "prod-mini" ,
15591712 ] ) ;
15601713 expect ( result . selected . name ) . toBe ( "prod-gpt" ) ;
15611714 expect ( selectCalls [ 0 ] ?. options . map ( ( option ) => option . value ) ) . toEqual ( [
15621715 "prod-gpt" ,
1716+ "prod-claude" ,
15631717 "prod-mini" ,
15641718 ] ) ;
1565- expect ( noteCalls . some ( ( call ) => call . title === "Unsupported Deployments" ) ) . toBe ( true ) ;
1719+ expect ( noteCalls . some ( ( call ) => call . title === "Unsupported Deployments" ) ) . toBe ( false ) ;
15661720 } ) ;
15671721
1568- it ( "throws an actionable error when only Anthropic deployments exist " , async ( ) => {
1722+ it ( "uses Anthropic- only deployment resources directly " , async ( ) => {
15691723 const { ctx, noteCalls } = makeCtx ( ) ;
15701724
1571- await expect (
1572- selectFoundryDeployment ( ctx , fakeResource , [
1573- { name : "only-claude" , modelName : "claude-3.5-sonnet" , state : "Succeeded" } ,
1574- ] ) ,
1575- ) . rejects . toThrow ( / O n l y A n t h r o p i c d e p l o y m e n t s / ) ;
1576- expect ( noteCalls . some ( ( call ) => call . title === "Unsupported Deployments" ) ) . toBe ( true ) ;
1725+ const result = await selectFoundryDeployment ( ctx , fakeResource , [
1726+ { name : "only-claude" , modelName : "claude-3.5-sonnet" , state : "Succeeded" } ,
1727+ ] ) ;
1728+
1729+ expect ( result ) . toEqual ( {
1730+ selected : { name : "only-claude" , modelName : "claude-3.5-sonnet" , state : "Succeeded" } ,
1731+ supported : [ { name : "only-claude" , modelName : "claude-3.5-sonnet" , state : "Succeeded" } ] ,
1732+ } ) ;
1733+ expect ( noteCalls . some ( ( call ) => call . title === "Unsupported Deployments" ) ) . toBe ( false ) ;
15771734 } ) ;
15781735
15791736 it ( "leaves all-OpenAI resources unchanged" , async ( ) => {
0 commit comments