diff --git a/DemoApp/Files/packages.lock.json b/DemoApp/Files/packages.lock.json index 8b1628f9..39dadc37 100644 --- a/DemoApp/Files/packages.lock.json +++ b/DemoApp/Files/packages.lock.json @@ -1,95 +1,96 @@ -{ - "version": 2, - "dependencies": { - "net8.0": { - "Google.Apis": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", - "dependencies": { - "Google.Apis.Core": "1.69.0" - } - }, - "Google.Apis.Core": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", - "dependencies": { - "Newtonsoft.Json": "13.0.3" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" - }, - "System.Management": { - "type": "Transitive", - "resolved": "7.0.2", - "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", - "dependencies": { - "System.CodeDom": "7.0.0" - } - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" - }, - "google.genai": { - "type": "Project", - "dependencies": { - "Google.Apis.Auth": "[1.69.0, )", - "Microsoft.Extensions.AI.Abstractions": "[10.4.0, )", - "MimeTypes": "[2.5.2, )" - } - }, - "Google.Apis.Auth": { - "type": "CentralTransitive", - "requested": "[1.69.0, )", - "resolved": "1.69.0", - "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", - "dependencies": { - "Google.Apis": "1.69.0", - "Google.Apis.Core": "1.69.0", - "System.Management": "7.0.2" - } - }, - "Microsoft.Extensions.AI.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "t3S2H4do4YeNheIfE3GEl3MnKIrnxpbLu7a88spfApYR3in9ddhIq/GMtxgMaFjn/PUMTCFv5YH7Y6Q91dsDXQ==", - "dependencies": { - "System.Text.Json": "10.0.4" - } - }, - "MimeTypes": { - "type": "CentralTransitive", - "requested": "[2.5.2, )", - "resolved": "2.5.2", - "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" - }, - "System.Text.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", - "dependencies": { - "System.IO.Pipelines": "10.0.4", - "System.Text.Encodings.Web": "10.0.4" - } - } - } - } +{ + "version": 2, + "dependencies": { + "net8.0": { + "Google.Apis": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", + "dependencies": { + "Google.Apis.Core": "1.69.0" + } + }, + "Google.Apis.Core": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", + "dependencies": { + "System.CodeDom": "7.0.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" + }, + "google.genai": { + "type": "Project", + "dependencies": { + "Google.Apis.Auth": "[1.69.0, )", + "Microsoft.Extensions.AI.Abstractions": "[10.4.1, )", + "MimeTypes": "[2.5.2, )", + "System.Text.Json": "[10.0.4, )" + } + }, + "Google.Apis.Auth": { + "type": "CentralTransitive", + "requested": "[1.69.0, )", + "resolved": "1.69.0", + "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", + "dependencies": { + "Google.Apis": "1.69.0", + "Google.Apis.Core": "1.69.0", + "System.Management": "7.0.2" + } + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.4.1, )", + "resolved": "10.4.1", + "contentHash": "ZxhU/wg9BOc3ohibhLl18toPLWm96ysQoE+3OhCgrZ0TUPZd7bsUmGteeatz08yweyuPIEhtyUzEZTF+3bMWEQ==", + "dependencies": { + "System.Text.Json": "10.0.4" + } + }, + "MimeTypes": { + "type": "CentralTransitive", + "requested": "[2.5.2, )", + "resolved": "2.5.2", + "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" + }, + "System.Text.Json": { + "type": "CentralTransitive", + "requested": "[10.0.4, )", + "resolved": "10.0.4", + "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", + "dependencies": { + "System.IO.Pipelines": "10.0.4", + "System.Text.Encodings.Web": "10.0.4" + } + } + } + } } \ No newline at end of file diff --git a/DemoApp/GenerateContentSimpleText/packages.lock.json b/DemoApp/GenerateContentSimpleText/packages.lock.json index 8b1628f9..39dadc37 100644 --- a/DemoApp/GenerateContentSimpleText/packages.lock.json +++ b/DemoApp/GenerateContentSimpleText/packages.lock.json @@ -1,95 +1,96 @@ -{ - "version": 2, - "dependencies": { - "net8.0": { - "Google.Apis": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", - "dependencies": { - "Google.Apis.Core": "1.69.0" - } - }, - "Google.Apis.Core": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", - "dependencies": { - "Newtonsoft.Json": "13.0.3" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" - }, - "System.Management": { - "type": "Transitive", - "resolved": "7.0.2", - "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", - "dependencies": { - "System.CodeDom": "7.0.0" - } - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" - }, - "google.genai": { - "type": "Project", - "dependencies": { - "Google.Apis.Auth": "[1.69.0, )", - "Microsoft.Extensions.AI.Abstractions": "[10.4.0, )", - "MimeTypes": "[2.5.2, )" - } - }, - "Google.Apis.Auth": { - "type": "CentralTransitive", - "requested": "[1.69.0, )", - "resolved": "1.69.0", - "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", - "dependencies": { - "Google.Apis": "1.69.0", - "Google.Apis.Core": "1.69.0", - "System.Management": "7.0.2" - } - }, - "Microsoft.Extensions.AI.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "t3S2H4do4YeNheIfE3GEl3MnKIrnxpbLu7a88spfApYR3in9ddhIq/GMtxgMaFjn/PUMTCFv5YH7Y6Q91dsDXQ==", - "dependencies": { - "System.Text.Json": "10.0.4" - } - }, - "MimeTypes": { - "type": "CentralTransitive", - "requested": "[2.5.2, )", - "resolved": "2.5.2", - "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" - }, - "System.Text.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", - "dependencies": { - "System.IO.Pipelines": "10.0.4", - "System.Text.Encodings.Web": "10.0.4" - } - } - } - } +{ + "version": 2, + "dependencies": { + "net8.0": { + "Google.Apis": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", + "dependencies": { + "Google.Apis.Core": "1.69.0" + } + }, + "Google.Apis.Core": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", + "dependencies": { + "System.CodeDom": "7.0.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" + }, + "google.genai": { + "type": "Project", + "dependencies": { + "Google.Apis.Auth": "[1.69.0, )", + "Microsoft.Extensions.AI.Abstractions": "[10.4.1, )", + "MimeTypes": "[2.5.2, )", + "System.Text.Json": "[10.0.4, )" + } + }, + "Google.Apis.Auth": { + "type": "CentralTransitive", + "requested": "[1.69.0, )", + "resolved": "1.69.0", + "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", + "dependencies": { + "Google.Apis": "1.69.0", + "Google.Apis.Core": "1.69.0", + "System.Management": "7.0.2" + } + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.4.1, )", + "resolved": "10.4.1", + "contentHash": "ZxhU/wg9BOc3ohibhLl18toPLWm96ysQoE+3OhCgrZ0TUPZd7bsUmGteeatz08yweyuPIEhtyUzEZTF+3bMWEQ==", + "dependencies": { + "System.Text.Json": "10.0.4" + } + }, + "MimeTypes": { + "type": "CentralTransitive", + "requested": "[2.5.2, )", + "resolved": "2.5.2", + "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" + }, + "System.Text.Json": { + "type": "CentralTransitive", + "requested": "[10.0.4, )", + "resolved": "10.0.4", + "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", + "dependencies": { + "System.IO.Pipelines": "10.0.4", + "System.Text.Encodings.Web": "10.0.4" + } + } + } + } } \ No newline at end of file diff --git a/DemoApp/GenerateContentStreamSimpleText/packages.lock.json b/DemoApp/GenerateContentStreamSimpleText/packages.lock.json index 8b1628f9..39dadc37 100644 --- a/DemoApp/GenerateContentStreamSimpleText/packages.lock.json +++ b/DemoApp/GenerateContentStreamSimpleText/packages.lock.json @@ -1,95 +1,96 @@ -{ - "version": 2, - "dependencies": { - "net8.0": { - "Google.Apis": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", - "dependencies": { - "Google.Apis.Core": "1.69.0" - } - }, - "Google.Apis.Core": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", - "dependencies": { - "Newtonsoft.Json": "13.0.3" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" - }, - "System.Management": { - "type": "Transitive", - "resolved": "7.0.2", - "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", - "dependencies": { - "System.CodeDom": "7.0.0" - } - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" - }, - "google.genai": { - "type": "Project", - "dependencies": { - "Google.Apis.Auth": "[1.69.0, )", - "Microsoft.Extensions.AI.Abstractions": "[10.4.0, )", - "MimeTypes": "[2.5.2, )" - } - }, - "Google.Apis.Auth": { - "type": "CentralTransitive", - "requested": "[1.69.0, )", - "resolved": "1.69.0", - "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", - "dependencies": { - "Google.Apis": "1.69.0", - "Google.Apis.Core": "1.69.0", - "System.Management": "7.0.2" - } - }, - "Microsoft.Extensions.AI.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "t3S2H4do4YeNheIfE3GEl3MnKIrnxpbLu7a88spfApYR3in9ddhIq/GMtxgMaFjn/PUMTCFv5YH7Y6Q91dsDXQ==", - "dependencies": { - "System.Text.Json": "10.0.4" - } - }, - "MimeTypes": { - "type": "CentralTransitive", - "requested": "[2.5.2, )", - "resolved": "2.5.2", - "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" - }, - "System.Text.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", - "dependencies": { - "System.IO.Pipelines": "10.0.4", - "System.Text.Encodings.Web": "10.0.4" - } - } - } - } +{ + "version": 2, + "dependencies": { + "net8.0": { + "Google.Apis": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", + "dependencies": { + "Google.Apis.Core": "1.69.0" + } + }, + "Google.Apis.Core": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", + "dependencies": { + "System.CodeDom": "7.0.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" + }, + "google.genai": { + "type": "Project", + "dependencies": { + "Google.Apis.Auth": "[1.69.0, )", + "Microsoft.Extensions.AI.Abstractions": "[10.4.1, )", + "MimeTypes": "[2.5.2, )", + "System.Text.Json": "[10.0.4, )" + } + }, + "Google.Apis.Auth": { + "type": "CentralTransitive", + "requested": "[1.69.0, )", + "resolved": "1.69.0", + "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", + "dependencies": { + "Google.Apis": "1.69.0", + "Google.Apis.Core": "1.69.0", + "System.Management": "7.0.2" + } + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.4.1, )", + "resolved": "10.4.1", + "contentHash": "ZxhU/wg9BOc3ohibhLl18toPLWm96ysQoE+3OhCgrZ0TUPZd7bsUmGteeatz08yweyuPIEhtyUzEZTF+3bMWEQ==", + "dependencies": { + "System.Text.Json": "10.0.4" + } + }, + "MimeTypes": { + "type": "CentralTransitive", + "requested": "[2.5.2, )", + "resolved": "2.5.2", + "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" + }, + "System.Text.Json": { + "type": "CentralTransitive", + "requested": "[10.0.4, )", + "resolved": "10.0.4", + "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", + "dependencies": { + "System.IO.Pipelines": "10.0.4", + "System.Text.Encodings.Web": "10.0.4" + } + } + } + } } \ No newline at end of file diff --git a/DemoApp/JsonParser/packages.lock.json b/DemoApp/JsonParser/packages.lock.json index 8b1628f9..39dadc37 100644 --- a/DemoApp/JsonParser/packages.lock.json +++ b/DemoApp/JsonParser/packages.lock.json @@ -1,95 +1,96 @@ -{ - "version": 2, - "dependencies": { - "net8.0": { - "Google.Apis": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", - "dependencies": { - "Google.Apis.Core": "1.69.0" - } - }, - "Google.Apis.Core": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", - "dependencies": { - "Newtonsoft.Json": "13.0.3" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" - }, - "System.Management": { - "type": "Transitive", - "resolved": "7.0.2", - "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", - "dependencies": { - "System.CodeDom": "7.0.0" - } - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" - }, - "google.genai": { - "type": "Project", - "dependencies": { - "Google.Apis.Auth": "[1.69.0, )", - "Microsoft.Extensions.AI.Abstractions": "[10.4.0, )", - "MimeTypes": "[2.5.2, )" - } - }, - "Google.Apis.Auth": { - "type": "CentralTransitive", - "requested": "[1.69.0, )", - "resolved": "1.69.0", - "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", - "dependencies": { - "Google.Apis": "1.69.0", - "Google.Apis.Core": "1.69.0", - "System.Management": "7.0.2" - } - }, - "Microsoft.Extensions.AI.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "t3S2H4do4YeNheIfE3GEl3MnKIrnxpbLu7a88spfApYR3in9ddhIq/GMtxgMaFjn/PUMTCFv5YH7Y6Q91dsDXQ==", - "dependencies": { - "System.Text.Json": "10.0.4" - } - }, - "MimeTypes": { - "type": "CentralTransitive", - "requested": "[2.5.2, )", - "resolved": "2.5.2", - "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" - }, - "System.Text.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", - "dependencies": { - "System.IO.Pipelines": "10.0.4", - "System.Text.Encodings.Web": "10.0.4" - } - } - } - } +{ + "version": 2, + "dependencies": { + "net8.0": { + "Google.Apis": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", + "dependencies": { + "Google.Apis.Core": "1.69.0" + } + }, + "Google.Apis.Core": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", + "dependencies": { + "System.CodeDom": "7.0.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" + }, + "google.genai": { + "type": "Project", + "dependencies": { + "Google.Apis.Auth": "[1.69.0, )", + "Microsoft.Extensions.AI.Abstractions": "[10.4.1, )", + "MimeTypes": "[2.5.2, )", + "System.Text.Json": "[10.0.4, )" + } + }, + "Google.Apis.Auth": { + "type": "CentralTransitive", + "requested": "[1.69.0, )", + "resolved": "1.69.0", + "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", + "dependencies": { + "Google.Apis": "1.69.0", + "Google.Apis.Core": "1.69.0", + "System.Management": "7.0.2" + } + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.4.1, )", + "resolved": "10.4.1", + "contentHash": "ZxhU/wg9BOc3ohibhLl18toPLWm96ysQoE+3OhCgrZ0TUPZd7bsUmGteeatz08yweyuPIEhtyUzEZTF+3bMWEQ==", + "dependencies": { + "System.Text.Json": "10.0.4" + } + }, + "MimeTypes": { + "type": "CentralTransitive", + "requested": "[2.5.2, )", + "resolved": "2.5.2", + "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" + }, + "System.Text.Json": { + "type": "CentralTransitive", + "requested": "[10.0.4, )", + "resolved": "10.0.4", + "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", + "dependencies": { + "System.IO.Pipelines": "10.0.4", + "System.Text.Encodings.Web": "10.0.4" + } + } + } + } } \ No newline at end of file diff --git a/DemoApp/LiveAudioToAudioRealtimeInput/packages.lock.json b/DemoApp/LiveAudioToAudioRealtimeInput/packages.lock.json index 8b1628f9..39dadc37 100644 --- a/DemoApp/LiveAudioToAudioRealtimeInput/packages.lock.json +++ b/DemoApp/LiveAudioToAudioRealtimeInput/packages.lock.json @@ -1,95 +1,96 @@ -{ - "version": 2, - "dependencies": { - "net8.0": { - "Google.Apis": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", - "dependencies": { - "Google.Apis.Core": "1.69.0" - } - }, - "Google.Apis.Core": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", - "dependencies": { - "Newtonsoft.Json": "13.0.3" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" - }, - "System.Management": { - "type": "Transitive", - "resolved": "7.0.2", - "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", - "dependencies": { - "System.CodeDom": "7.0.0" - } - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" - }, - "google.genai": { - "type": "Project", - "dependencies": { - "Google.Apis.Auth": "[1.69.0, )", - "Microsoft.Extensions.AI.Abstractions": "[10.4.0, )", - "MimeTypes": "[2.5.2, )" - } - }, - "Google.Apis.Auth": { - "type": "CentralTransitive", - "requested": "[1.69.0, )", - "resolved": "1.69.0", - "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", - "dependencies": { - "Google.Apis": "1.69.0", - "Google.Apis.Core": "1.69.0", - "System.Management": "7.0.2" - } - }, - "Microsoft.Extensions.AI.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "t3S2H4do4YeNheIfE3GEl3MnKIrnxpbLu7a88spfApYR3in9ddhIq/GMtxgMaFjn/PUMTCFv5YH7Y6Q91dsDXQ==", - "dependencies": { - "System.Text.Json": "10.0.4" - } - }, - "MimeTypes": { - "type": "CentralTransitive", - "requested": "[2.5.2, )", - "resolved": "2.5.2", - "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" - }, - "System.Text.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", - "dependencies": { - "System.IO.Pipelines": "10.0.4", - "System.Text.Encodings.Web": "10.0.4" - } - } - } - } +{ + "version": 2, + "dependencies": { + "net8.0": { + "Google.Apis": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", + "dependencies": { + "Google.Apis.Core": "1.69.0" + } + }, + "Google.Apis.Core": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", + "dependencies": { + "System.CodeDom": "7.0.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" + }, + "google.genai": { + "type": "Project", + "dependencies": { + "Google.Apis.Auth": "[1.69.0, )", + "Microsoft.Extensions.AI.Abstractions": "[10.4.1, )", + "MimeTypes": "[2.5.2, )", + "System.Text.Json": "[10.0.4, )" + } + }, + "Google.Apis.Auth": { + "type": "CentralTransitive", + "requested": "[1.69.0, )", + "resolved": "1.69.0", + "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", + "dependencies": { + "Google.Apis": "1.69.0", + "Google.Apis.Core": "1.69.0", + "System.Management": "7.0.2" + } + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.4.1, )", + "resolved": "10.4.1", + "contentHash": "ZxhU/wg9BOc3ohibhLl18toPLWm96ysQoE+3OhCgrZ0TUPZd7bsUmGteeatz08yweyuPIEhtyUzEZTF+3bMWEQ==", + "dependencies": { + "System.Text.Json": "10.0.4" + } + }, + "MimeTypes": { + "type": "CentralTransitive", + "requested": "[2.5.2, )", + "resolved": "2.5.2", + "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" + }, + "System.Text.Json": { + "type": "CentralTransitive", + "requested": "[10.0.4, )", + "resolved": "10.0.4", + "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", + "dependencies": { + "System.IO.Pipelines": "10.0.4", + "System.Text.Encodings.Web": "10.0.4" + } + } + } + } } \ No newline at end of file diff --git a/DemoApp/LiveTextToTextClientContent/packages.lock.json b/DemoApp/LiveTextToTextClientContent/packages.lock.json index 8b1628f9..39dadc37 100644 --- a/DemoApp/LiveTextToTextClientContent/packages.lock.json +++ b/DemoApp/LiveTextToTextClientContent/packages.lock.json @@ -1,95 +1,96 @@ -{ - "version": 2, - "dependencies": { - "net8.0": { - "Google.Apis": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", - "dependencies": { - "Google.Apis.Core": "1.69.0" - } - }, - "Google.Apis.Core": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", - "dependencies": { - "Newtonsoft.Json": "13.0.3" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" - }, - "System.Management": { - "type": "Transitive", - "resolved": "7.0.2", - "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", - "dependencies": { - "System.CodeDom": "7.0.0" - } - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" - }, - "google.genai": { - "type": "Project", - "dependencies": { - "Google.Apis.Auth": "[1.69.0, )", - "Microsoft.Extensions.AI.Abstractions": "[10.4.0, )", - "MimeTypes": "[2.5.2, )" - } - }, - "Google.Apis.Auth": { - "type": "CentralTransitive", - "requested": "[1.69.0, )", - "resolved": "1.69.0", - "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", - "dependencies": { - "Google.Apis": "1.69.0", - "Google.Apis.Core": "1.69.0", - "System.Management": "7.0.2" - } - }, - "Microsoft.Extensions.AI.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "t3S2H4do4YeNheIfE3GEl3MnKIrnxpbLu7a88spfApYR3in9ddhIq/GMtxgMaFjn/PUMTCFv5YH7Y6Q91dsDXQ==", - "dependencies": { - "System.Text.Json": "10.0.4" - } - }, - "MimeTypes": { - "type": "CentralTransitive", - "requested": "[2.5.2, )", - "resolved": "2.5.2", - "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" - }, - "System.Text.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", - "dependencies": { - "System.IO.Pipelines": "10.0.4", - "System.Text.Encodings.Web": "10.0.4" - } - } - } - } +{ + "version": 2, + "dependencies": { + "net8.0": { + "Google.Apis": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", + "dependencies": { + "Google.Apis.Core": "1.69.0" + } + }, + "Google.Apis.Core": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", + "dependencies": { + "System.CodeDom": "7.0.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" + }, + "google.genai": { + "type": "Project", + "dependencies": { + "Google.Apis.Auth": "[1.69.0, )", + "Microsoft.Extensions.AI.Abstractions": "[10.4.1, )", + "MimeTypes": "[2.5.2, )", + "System.Text.Json": "[10.0.4, )" + } + }, + "Google.Apis.Auth": { + "type": "CentralTransitive", + "requested": "[1.69.0, )", + "resolved": "1.69.0", + "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", + "dependencies": { + "Google.Apis": "1.69.0", + "Google.Apis.Core": "1.69.0", + "System.Management": "7.0.2" + } + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.4.1, )", + "resolved": "10.4.1", + "contentHash": "ZxhU/wg9BOc3ohibhLl18toPLWm96ysQoE+3OhCgrZ0TUPZd7bsUmGteeatz08yweyuPIEhtyUzEZTF+3bMWEQ==", + "dependencies": { + "System.Text.Json": "10.0.4" + } + }, + "MimeTypes": { + "type": "CentralTransitive", + "requested": "[2.5.2, )", + "resolved": "2.5.2", + "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" + }, + "System.Text.Json": { + "type": "CentralTransitive", + "requested": "[10.0.4, )", + "resolved": "10.0.4", + "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", + "dependencies": { + "System.IO.Pipelines": "10.0.4", + "System.Text.Encodings.Web": "10.0.4" + } + } + } + } } \ No newline at end of file diff --git a/DemoApp/LiveToolCall/packages.lock.json b/DemoApp/LiveToolCall/packages.lock.json index 8b1628f9..39dadc37 100644 --- a/DemoApp/LiveToolCall/packages.lock.json +++ b/DemoApp/LiveToolCall/packages.lock.json @@ -1,95 +1,96 @@ -{ - "version": 2, - "dependencies": { - "net8.0": { - "Google.Apis": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", - "dependencies": { - "Google.Apis.Core": "1.69.0" - } - }, - "Google.Apis.Core": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", - "dependencies": { - "Newtonsoft.Json": "13.0.3" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" - }, - "System.Management": { - "type": "Transitive", - "resolved": "7.0.2", - "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", - "dependencies": { - "System.CodeDom": "7.0.0" - } - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" - }, - "google.genai": { - "type": "Project", - "dependencies": { - "Google.Apis.Auth": "[1.69.0, )", - "Microsoft.Extensions.AI.Abstractions": "[10.4.0, )", - "MimeTypes": "[2.5.2, )" - } - }, - "Google.Apis.Auth": { - "type": "CentralTransitive", - "requested": "[1.69.0, )", - "resolved": "1.69.0", - "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", - "dependencies": { - "Google.Apis": "1.69.0", - "Google.Apis.Core": "1.69.0", - "System.Management": "7.0.2" - } - }, - "Microsoft.Extensions.AI.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "t3S2H4do4YeNheIfE3GEl3MnKIrnxpbLu7a88spfApYR3in9ddhIq/GMtxgMaFjn/PUMTCFv5YH7Y6Q91dsDXQ==", - "dependencies": { - "System.Text.Json": "10.0.4" - } - }, - "MimeTypes": { - "type": "CentralTransitive", - "requested": "[2.5.2, )", - "resolved": "2.5.2", - "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" - }, - "System.Text.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", - "dependencies": { - "System.IO.Pipelines": "10.0.4", - "System.Text.Encodings.Web": "10.0.4" - } - } - } - } +{ + "version": 2, + "dependencies": { + "net8.0": { + "Google.Apis": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", + "dependencies": { + "Google.Apis.Core": "1.69.0" + } + }, + "Google.Apis.Core": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", + "dependencies": { + "System.CodeDom": "7.0.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" + }, + "google.genai": { + "type": "Project", + "dependencies": { + "Google.Apis.Auth": "[1.69.0, )", + "Microsoft.Extensions.AI.Abstractions": "[10.4.1, )", + "MimeTypes": "[2.5.2, )", + "System.Text.Json": "[10.0.4, )" + } + }, + "Google.Apis.Auth": { + "type": "CentralTransitive", + "requested": "[1.69.0, )", + "resolved": "1.69.0", + "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", + "dependencies": { + "Google.Apis": "1.69.0", + "Google.Apis.Core": "1.69.0", + "System.Management": "7.0.2" + } + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.4.1, )", + "resolved": "10.4.1", + "contentHash": "ZxhU/wg9BOc3ohibhLl18toPLWm96ysQoE+3OhCgrZ0TUPZd7bsUmGteeatz08yweyuPIEhtyUzEZTF+3bMWEQ==", + "dependencies": { + "System.Text.Json": "10.0.4" + } + }, + "MimeTypes": { + "type": "CentralTransitive", + "requested": "[2.5.2, )", + "resolved": "2.5.2", + "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" + }, + "System.Text.Json": { + "type": "CentralTransitive", + "requested": "[10.0.4, )", + "resolved": "10.0.4", + "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", + "dependencies": { + "System.IO.Pipelines": "10.0.4", + "System.Text.Encodings.Web": "10.0.4" + } + } + } + } } \ No newline at end of file diff --git a/DemoApp/RegisterFiles/packages.lock.json b/DemoApp/RegisterFiles/packages.lock.json new file mode 100644 index 00000000..e0388869 --- /dev/null +++ b/DemoApp/RegisterFiles/packages.lock.json @@ -0,0 +1,95 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "Google.Apis": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", + "dependencies": { + "Google.Apis.Core": "1.69.0" + } + }, + "Google.Apis.Core": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", + "dependencies": { + "System.CodeDom": "7.0.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" + }, + "google.genai": { + "type": "Project", + "dependencies": { + "Google.Apis.Auth": "[1.69.0, )", + "Microsoft.Extensions.AI.Abstractions": "[10.4.1, )", + "MimeTypes": "[2.5.2, )" + } + }, + "Google.Apis.Auth": { + "type": "CentralTransitive", + "requested": "[1.69.0, )", + "resolved": "1.69.0", + "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", + "dependencies": { + "Google.Apis": "1.69.0", + "Google.Apis.Core": "1.69.0", + "System.Management": "7.0.2" + } + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.4.1, )", + "resolved": "10.4.1", + "contentHash": "ZxhU/wg9BOc3ohibhLl18toPLWm96ysQoE+3OhCgrZ0TUPZd7bsUmGteeatz08yweyuPIEhtyUzEZTF+3bMWEQ==", + "dependencies": { + "System.Text.Json": "10.0.4" + } + }, + "MimeTypes": { + "type": "CentralTransitive", + "requested": "[2.5.2, )", + "resolved": "2.5.2", + "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" + }, + "System.Text.Json": { + "type": "CentralTransitive", + "requested": "[10.0.4, )", + "resolved": "10.0.4", + "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", + "dependencies": { + "System.IO.Pipelines": "10.0.4", + "System.Text.Encodings.Web": "10.0.4" + } + } + } + } +} \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index c8b8d735..7b6ba1e2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,7 +13,7 @@ - + diff --git a/Google.GenAI.E2E.Tests/Netstandard2_0Tests/packages.lock.json b/Google.GenAI.E2E.Tests/Netstandard2_0Tests/packages.lock.json index 4bfece9f..a3505287 100644 --- a/Google.GenAI.E2E.Tests/Netstandard2_0Tests/packages.lock.json +++ b/Google.GenAI.E2E.Tests/Netstandard2_0Tests/packages.lock.json @@ -283,7 +283,7 @@ "type": "Project", "dependencies": { "Google.Apis.Auth": "[1.69.0, )", - "Microsoft.Extensions.AI.Abstractions": "[10.4.0, )", + "Microsoft.Extensions.AI.Abstractions": "[10.4.1, )", "MimeTypes": "[2.5.2, )" } }, @@ -300,9 +300,9 @@ }, "Microsoft.Extensions.AI.Abstractions": { "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "t3S2H4do4YeNheIfE3GEl3MnKIrnxpbLu7a88spfApYR3in9ddhIq/GMtxgMaFjn/PUMTCFv5YH7Y6Q91dsDXQ==", + "requested": "[10.4.1, )", + "resolved": "10.4.1", + "contentHash": "ZxhU/wg9BOc3ohibhLl18toPLWm96ysQoE+3OhCgrZ0TUPZd7bsUmGteeatz08yweyuPIEhtyUzEZTF+3bMWEQ==", "dependencies": { "System.Text.Json": "10.0.4" } diff --git a/Google.GenAI.E2E.Tests/packages.lock.json b/Google.GenAI.E2E.Tests/packages.lock.json index 79753e82..66676549 100644 --- a/Google.GenAI.E2E.Tests/packages.lock.json +++ b/Google.GenAI.E2E.Tests/packages.lock.json @@ -1,318 +1,313 @@ -{ - "version": 2, - "dependencies": { - "net8.0": { - "coverlet.collector": { - "type": "Direct", - "requested": "[6.0.4, )", - "resolved": "6.0.4", - "contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg==" - }, - "Microsoft.NET.Test.Sdk": { - "type": "Direct", - "requested": "[17.10.0, )", - "resolved": "17.10.0", - "contentHash": "0/2HeACkaHEYU3wc83YlcD2Fi4LMtECJjqrtvw0lPi9DCEa35zSPt1j4fuvM8NagjDqJuh1Ja35WcRtn1Um6/A==", - "dependencies": { - "Microsoft.CodeCoverage": "17.10.0", - "Microsoft.TestPlatform.TestHost": "17.10.0" - } - }, - "Microsoft.Testing.Extensions.CodeCoverage": { - "type": "Direct", - "requested": "[17.10.1, )", - "resolved": "17.10.1", - "contentHash": "EdPqUfB4GShYeXiivrjWrTAjZz93tmrF313QlyK/CI4afdBAcNCrJ2IqEWAQ1n+05sc+tEwZnyaZAdStnwQqcw==", - "dependencies": { - "Microsoft.DiaSymReader": "2.0.0", - "Microsoft.Extensions.DependencyModel": "6.0.0", - "Microsoft.Testing.Platform": "1.0.0", - "Mono.Cecil": "0.11.5", - "System.Reflection.Metadata": "6.0.1" - } - }, - "Moq": { - "type": "Direct", - "requested": "[4.20.70, )", - "resolved": "4.20.70", - "contentHash": "4rNnAwdpXJBuxqrOCzCyICXHSImOTRktCgCWXWykuF1qwoIsVvEnR7PjbMk/eLOxWvhmj5Kwt+kDV3RGUYcNwg==", - "dependencies": { - "Castle.Core": "5.1.1" - } - }, - "MSTest.TestAdapter": { - "type": "Direct", - "requested": "[3.4.3, )", - "resolved": "3.4.3", - "contentHash": "5ul31wYr17590gDumPxWMiBLPREfPF/ggtdPGfaKoYSsO0EW6H1GWY+7xnVCKa2SB4I/dSEZLDYSwRLDjA0LEQ==", - "dependencies": { - "Microsoft.Testing.Extensions.VSTestBridge": "1.2.1", - "Microsoft.Testing.Platform.MSBuild": "1.2.1" - } - }, - "MSTest.TestFramework": { - "type": "Direct", - "requested": "[3.4.3, )", - "resolved": "3.4.3", - "contentHash": "hu7F0PyRe47LScY2SCjRFIzP2QYxq1oeHMAIdao9onUm5WhobO9tfZrFAAkJ4v+66EQjilloEbA4kspVHCZpTg==" - }, - "System.Text.Json": { - "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", - "dependencies": { - "System.IO.Pipelines": "10.0.4", - "System.Text.Encodings.Web": "10.0.4" - } - }, - "TestServerSdk": { - "type": "Direct", - "requested": "[0.1.5, )", - "resolved": "0.1.5", - "contentHash": "cQZJyJvSn12KSe5yzWDuq+YK1rKLT+ndNdUgmiCN+SmQtQIMy0uGbqhen6i7mKvgdF9K+J62QhvM6bT9TSqlyg==", - "dependencies": { - "SharpCompress": "0.29.0", - "YamlDotNet": "12.0.2" - } - }, - "Castle.Core": { - "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", - "dependencies": { - "System.Diagnostics.EventLog": "6.0.0" - } - }, - "Google.Apis": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", - "dependencies": { - "Google.Apis.Core": "1.69.0" - } - }, - "Google.Apis.Core": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", - "dependencies": { - "Newtonsoft.Json": "13.0.3" - } - }, - "Microsoft.ApplicationInsights": { - "type": "Transitive", - "resolved": "2.22.0", - "contentHash": "3AOM9bZtku7RQwHyMEY3tQMrHIgjcfRDa6YQpd/QG2LDGvMydSlL9Di+8LLMt7J2RDdfJ7/2jdYv6yHcMJAnNw==", - "dependencies": { - "System.Diagnostics.DiagnosticSource": "5.0.0" - } - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "yC7oSlnR54XO5kOuHlVOKtxomNNN1BWXX8lK1G2jaPXT9sUok7kCOoA4Pgs0qyFaCtMrNsprztYMeoEGqCm4uA==" - }, - "Microsoft.DiaSymReader": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "TD5QHg98m3+QhgEV1YVoNMl5KtBw/4rjfxLHO0e/YV9bPUBDKntApP4xdrVtGgCeQZHVfC2EXIGsdpRNrr87Pg==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Memory": "4.5.4", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encodings.Web": "6.0.0", - "System.Text.Json": "6.0.0" - } - }, - "Microsoft.Testing.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "MKGxwQhDDEoTS/ntFb21Z6Bxh9VvknmSLgEWH+NFD86fbcIqE2Al8lrXkQPeH+AqCvlhx2WnPLKd81T2PXc2dw==", - "dependencies": { - "Microsoft.ApplicationInsights": "2.22.0", - "Microsoft.Testing.Platform": "1.2.1" - } - }, - "Microsoft.Testing.Extensions.TrxReport.Abstractions": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "46SnzaLR+SDaTtBWy49xdFm/rI40I8nZtziqnt2d4lgILKovWPnkM8Pehnga/uwl+OznVIh0XuRsN3NokkX1TQ==", - "dependencies": { - "Microsoft.Testing.Platform": "1.2.1" - } - }, - "Microsoft.Testing.Extensions.VSTestBridge": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "Tu8CWHEwV/92WM2DRr/qeIdH243diV5s43ODPLl13XeRqGbZlu9lk7X0a7kcxhp0BLRlA3fqMW3F6RynrnDrPw==", - "dependencies": { - "Microsoft.ApplicationInsights": "2.22.0", - "Microsoft.TestPlatform.ObjectModel": "17.5.0", - "Microsoft.Testing.Extensions.Telemetry": "1.2.1", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.2.1", - "Microsoft.Testing.Platform": "1.2.1" - } - }, - "Microsoft.Testing.Platform": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "mb7irPwqjgusJ05BxuQ5KP6uofWaoDr/dfjFNItX1Q1Ntv3EDMr3CeLInrlU2PNcPwwObw4X6bZG7wJvvFjKZQ==" - }, - "Microsoft.Testing.Platform.MSBuild": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "leUhW4iQNy7vmPk5uRHd4OROqfRtugWDQkWL/4AD17gxZwAAwGCaTcrqG0YVPi7uuZ+lj2Loa6kU7hBLA/v5+w==", - "dependencies": { - "Microsoft.Testing.Platform": "1.2.1" - } - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "KkwhjQevuDj0aBRoPLY6OLAhGqbPUEBuKLbaCs0kUVw29qiOYncdORd4mLVJbn9vGZ7/iFGQ/+AoJl0Tu5Umdg==", - "dependencies": { - "System.Reflection.Metadata": "1.6.0" - } - }, - "Microsoft.TestPlatform.TestHost": { - "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "LWpMdfqhHvcUkeMCvNYJO8QlPLlYz9XPPb+ZbaXIKhdmjAV0wqTSrTiW5FLaf7RRZT50AQADDOYMOe0HxDxNgA==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "17.10.0", - "Newtonsoft.Json": "13.0.1" - } - }, - "Mono.Cecil": { - "type": "Transitive", - "resolved": "0.11.5", - "contentHash": "fxfX+0JGTZ8YQeu1MYjbBiK2CYTSzDyEeIixt+yqKKTn7FW8rv7JMY70qevup4ZJfD7Kk/VG/jDzQQTpfch87g==" - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "SharpCompress": { - "type": "Transitive", - "resolved": "0.29.0", - "contentHash": "k7iUB0mZWS6eEj6L0PuuxASzjpQvlYKfHJ0JtnHBJFbU3VeQJ4SNQC4eloiYsPbmFrJCJYiqFhV4AZuBXpIXiQ==" - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "tCQTzPsGZh/A9LhhA6zrqCRV4hOHsK90/G7q3Khxmn6tnB1PuNU0cRaKANP2AWcF9bn0zsuOoZOSrHuJk6oNBA==" - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" - }, - "System.Management": { - "type": "Transitive", - "resolved": "7.0.2", - "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", - "dependencies": { - "System.CodeDom": "7.0.0" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "III/lNMSn0ZRBuM9m5Cgbiho5j81u0FAEagFX5ta2DKbljZ3T0IpD8j+BIiHQPeKqJppWS9bGEp6JnKnWKze0g==", - "dependencies": { - "System.Collections.Immutable": "6.0.0" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" - }, - "YamlDotNet": { - "type": "Transitive", - "resolved": "12.0.2", - "contentHash": "IFj2oDZYJIv5sM8mqaj5edVrpgUyoXk/wCGqZQJrgys/0tBajajpjRSgFM+iA/9ILOfTsPYKtcDcwvqBnBcNIg==" - }, - "google.genai": { - "type": "Project", - "dependencies": { - "Google.Apis.Auth": "[1.69.0, )", - "Microsoft.Extensions.AI.Abstractions": "[10.4.0, )", - "MimeTypes": "[2.5.2, )", - "System.Collections.Immutable": "[9.0.0, )", - "System.Net.ServerSentEvents": "[9.0.0, )" - } - }, - "Google.Apis.Auth": { - "type": "CentralTransitive", - "requested": "[1.69.0, )", - "resolved": "1.69.0", - "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", - "dependencies": { - "Google.Apis": "1.69.0", - "Google.Apis.Core": "1.69.0", - "System.Management": "7.0.2" - } - }, - "Microsoft.Extensions.AI.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "t3S2H4do4YeNheIfE3GEl3MnKIrnxpbLu7a88spfApYR3in9ddhIq/GMtxgMaFjn/PUMTCFv5YH7Y6Q91dsDXQ==", - "dependencies": { - "System.Text.Json": "10.0.4" - } - }, - "MimeTypes": { - "type": "CentralTransitive", - "requested": "[2.5.2, )", - "resolved": "2.5.2", - "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" - }, - "System.Collections.Immutable": { - "type": "CentralTransitive", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==" - }, - "System.Net.ServerSentEvents": { - "type": "CentralTransitive", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "VTWjeyx9nPb4+hkjGcAaDw1nOckypMtvABmxSWm6PPYwrXoIiVG3jwtNlAGhaGVjDkBrERABox67wYTAcHxg7Q==" - } - } - } +{ + "version": 2, + "dependencies": { + "net8.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[6.0.4, )", + "resolved": "6.0.4", + "contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.10.0, )", + "resolved": "17.10.0", + "contentHash": "0/2HeACkaHEYU3wc83YlcD2Fi4LMtECJjqrtvw0lPi9DCEa35zSPt1j4fuvM8NagjDqJuh1Ja35WcRtn1Um6/A==", + "dependencies": { + "Microsoft.CodeCoverage": "17.10.0", + "Microsoft.TestPlatform.TestHost": "17.10.0" + } + }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Direct", + "requested": "[17.10.1, )", + "resolved": "17.10.1", + "contentHash": "EdPqUfB4GShYeXiivrjWrTAjZz93tmrF313QlyK/CI4afdBAcNCrJ2IqEWAQ1n+05sc+tEwZnyaZAdStnwQqcw==", + "dependencies": { + "Microsoft.DiaSymReader": "2.0.0", + "Microsoft.Extensions.DependencyModel": "6.0.0", + "Microsoft.Testing.Platform": "1.0.0", + "Mono.Cecil": "0.11.5", + "System.Reflection.Metadata": "6.0.1" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.20.70, )", + "resolved": "4.20.70", + "contentHash": "4rNnAwdpXJBuxqrOCzCyICXHSImOTRktCgCWXWykuF1qwoIsVvEnR7PjbMk/eLOxWvhmj5Kwt+kDV3RGUYcNwg==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "MSTest.TestAdapter": { + "type": "Direct", + "requested": "[3.4.3, )", + "resolved": "3.4.3", + "contentHash": "5ul31wYr17590gDumPxWMiBLPREfPF/ggtdPGfaKoYSsO0EW6H1GWY+7xnVCKa2SB4I/dSEZLDYSwRLDjA0LEQ==", + "dependencies": { + "Microsoft.Testing.Extensions.VSTestBridge": "1.2.1", + "Microsoft.Testing.Platform.MSBuild": "1.2.1" + } + }, + "MSTest.TestFramework": { + "type": "Direct", + "requested": "[3.4.3, )", + "resolved": "3.4.3", + "contentHash": "hu7F0PyRe47LScY2SCjRFIzP2QYxq1oeHMAIdao9onUm5WhobO9tfZrFAAkJ4v+66EQjilloEbA4kspVHCZpTg==" + }, + "System.Text.Json": { + "type": "Direct", + "requested": "[10.0.4, )", + "resolved": "10.0.4", + "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", + "dependencies": { + "System.IO.Pipelines": "10.0.4", + "System.Text.Encodings.Web": "10.0.4" + } + }, + "TestServerSdk": { + "type": "Direct", + "requested": "[0.1.5, )", + "resolved": "0.1.5", + "contentHash": "cQZJyJvSn12KSe5yzWDuq+YK1rKLT+ndNdUgmiCN+SmQtQIMy0uGbqhen6i7mKvgdF9K+J62QhvM6bT9TSqlyg==", + "dependencies": { + "SharpCompress": "0.29.0", + "YamlDotNet": "12.0.2" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "Google.Apis": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", + "dependencies": { + "Google.Apis.Core": "1.69.0" + } + }, + "Google.Apis.Core": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.22.0", + "contentHash": "3AOM9bZtku7RQwHyMEY3tQMrHIgjcfRDa6YQpd/QG2LDGvMydSlL9Di+8LLMt7J2RDdfJ7/2jdYv6yHcMJAnNw==", + "dependencies": { + "System.Diagnostics.DiagnosticSource": "5.0.0" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.10.0", + "contentHash": "yC7oSlnR54XO5kOuHlVOKtxomNNN1BWXX8lK1G2jaPXT9sUok7kCOoA4Pgs0qyFaCtMrNsprztYMeoEGqCm4uA==" + }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "TD5QHg98m3+QhgEV1YVoNMl5KtBw/4rjfxLHO0e/YV9bPUBDKntApP4xdrVtGgCeQZHVfC2EXIGsdpRNrr87Pg==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.4", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "6.0.0", + "System.Text.Json": "6.0.0" + } + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "MKGxwQhDDEoTS/ntFb21Z6Bxh9VvknmSLgEWH+NFD86fbcIqE2Al8lrXkQPeH+AqCvlhx2WnPLKd81T2PXc2dw==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.22.0", + "Microsoft.Testing.Platform": "1.2.1" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "46SnzaLR+SDaTtBWy49xdFm/rI40I8nZtziqnt2d4lgILKovWPnkM8Pehnga/uwl+OznVIh0XuRsN3NokkX1TQ==", + "dependencies": { + "Microsoft.Testing.Platform": "1.2.1" + } + }, + "Microsoft.Testing.Extensions.VSTestBridge": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "Tu8CWHEwV/92WM2DRr/qeIdH243diV5s43ODPLl13XeRqGbZlu9lk7X0a7kcxhp0BLRlA3fqMW3F6RynrnDrPw==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.22.0", + "Microsoft.TestPlatform.ObjectModel": "17.5.0", + "Microsoft.Testing.Extensions.Telemetry": "1.2.1", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.2.1", + "Microsoft.Testing.Platform": "1.2.1" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "mb7irPwqjgusJ05BxuQ5KP6uofWaoDr/dfjFNItX1Q1Ntv3EDMr3CeLInrlU2PNcPwwObw4X6bZG7wJvvFjKZQ==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "leUhW4iQNy7vmPk5uRHd4OROqfRtugWDQkWL/4AD17gxZwAAwGCaTcrqG0YVPi7uuZ+lj2Loa6kU7hBLA/v5+w==", + "dependencies": { + "Microsoft.Testing.Platform": "1.2.1" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.10.0", + "contentHash": "KkwhjQevuDj0aBRoPLY6OLAhGqbPUEBuKLbaCs0kUVw29qiOYncdORd4mLVJbn9vGZ7/iFGQ/+AoJl0Tu5Umdg==", + "dependencies": { + "System.Reflection.Metadata": "1.6.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.10.0", + "contentHash": "LWpMdfqhHvcUkeMCvNYJO8QlPLlYz9XPPb+ZbaXIKhdmjAV0wqTSrTiW5FLaf7RRZT50AQADDOYMOe0HxDxNgA==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.10.0", + "Newtonsoft.Json": "13.0.1" + } + }, + "Mono.Cecil": { + "type": "Transitive", + "resolved": "0.11.5", + "contentHash": "fxfX+0JGTZ8YQeu1MYjbBiK2CYTSzDyEeIixt+yqKKTn7FW8rv7JMY70qevup4ZJfD7Kk/VG/jDzQQTpfch87g==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "SharpCompress": { + "type": "Transitive", + "resolved": "0.29.0", + "contentHash": "k7iUB0mZWS6eEj6L0PuuxASzjpQvlYKfHJ0JtnHBJFbU3VeQJ4SNQC4eloiYsPbmFrJCJYiqFhV4AZuBXpIXiQ==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "tCQTzPsGZh/A9LhhA6zrqCRV4hOHsK90/G7q3Khxmn6tnB1PuNU0cRaKANP2AWcF9bn0zsuOoZOSrHuJk6oNBA==" + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", + "dependencies": { + "System.CodeDom": "7.0.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "III/lNMSn0ZRBuM9m5Cgbiho5j81u0FAEagFX5ta2DKbljZ3T0IpD8j+BIiHQPeKqJppWS9bGEp6JnKnWKze0g==", + "dependencies": { + "System.Collections.Immutable": "6.0.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" + }, + "YamlDotNet": { + "type": "Transitive", + "resolved": "12.0.2", + "contentHash": "IFj2oDZYJIv5sM8mqaj5edVrpgUyoXk/wCGqZQJrgys/0tBajajpjRSgFM+iA/9ILOfTsPYKtcDcwvqBnBcNIg==" + }, + "google.genai": { + "type": "Project", + "dependencies": { + "Google.Apis.Auth": "[1.69.0, )", + "Microsoft.Extensions.AI.Abstractions": "[10.4.1, )", + "MimeTypes": "[2.5.2, )", + "System.Text.Json": "[10.0.4, )" + } + }, + "Google.Apis.Auth": { + "type": "CentralTransitive", + "requested": "[1.69.0, )", + "resolved": "1.69.0", + "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", + "dependencies": { + "Google.Apis": "1.69.0", + "Google.Apis.Core": "1.69.0", + "System.Management": "7.0.2" + } + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.4.1, )", + "resolved": "10.4.1", + "contentHash": "ZxhU/wg9BOc3ohibhLl18toPLWm96ysQoE+3OhCgrZ0TUPZd7bsUmGteeatz08yweyuPIEhtyUzEZTF+3bMWEQ==", + "dependencies": { + "System.Text.Json": "10.0.4" + } + }, + "MimeTypes": { + "type": "CentralTransitive", + "requested": "[2.5.2, )", + "resolved": "2.5.2", + "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" + } + } + } } \ No newline at end of file diff --git a/Google.GenAI.Tests/AotJsonContextTest.cs b/Google.GenAI.Tests/AotJsonContextTest.cs new file mode 100644 index 00000000..9cd93760 --- /dev/null +++ b/Google.GenAI.Tests/AotJsonContextTest.cs @@ -0,0 +1,266 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using Google.GenAI; +using Google.GenAI.Types; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Type = System.Type; + +namespace Google.GenAI.Tests +{ + /// + /// Tests that the source-generated JSON context covers all types used in serialization. + /// This ensures AOT compatibility by verifying type metadata is available without reflection. + /// + [TestClass] + public class AotJsonContextTest + { + /// + /// Verifies that all types directly used in JsonSerializer.Serialize/Deserialize calls + /// are resolvable via the GenAIJsonContext source-generated context (without reflection fallback). + /// + [TestMethod] + public void GenAIJsonContext_CoversAllDirectlySerializedTypes() + { + var requiredTypes = new[] + { + // Service parameter/response types + typeof(BatchJob), + typeof(CreateBatchJobParameters), + typeof(CreateEmbeddingsBatchJobParameters), + typeof(GetBatchJobParameters), + typeof(CancelBatchJobParameters), + typeof(ListBatchJobsResponse), + typeof(DeleteBatchJobParameters), + typeof(DeleteResourceJob), + typeof(CachedContent), + typeof(CreateCachedContentParameters), + typeof(GetCachedContentParameters), + typeof(DeleteCachedContentParameters), + typeof(DeleteCachedContentResponse), + typeof(UpdateCachedContentParameters), + typeof(ListCachedContentsParameters), + typeof(ListCachedContentsResponse), + typeof(Google.GenAI.Types.File), + typeof(ListFilesParameters), + typeof(ListFilesResponse), + typeof(CreateFileParameters), + typeof(CreateFileResponse), + typeof(GetFileParameters), + typeof(DeleteFileParameters), + typeof(DeleteFileResponse), + typeof(InternalRegisterFilesParameters), + typeof(RegisterFilesResponse), + typeof(GenerateContentParameters), + typeof(GenerateContentResponse), + typeof(EmbedContentParametersPrivate), + typeof(EmbedContentResponse), + typeof(GenerateImagesParameters), + typeof(GenerateImagesResponse), + typeof(EditImageParameters), + typeof(EditImageResponse), + typeof(UpscaleImageAPIParameters), + typeof(UpscaleImageResponse), + typeof(RecontextImageParameters), + typeof(RecontextImageResponse), + typeof(SegmentImageParameters), + typeof(SegmentImageResponse), + typeof(Model), + typeof(GetModelParameters), + typeof(ListModelsParameters), + typeof(ListModelsResponse), + typeof(UpdateModelParameters), + typeof(DeleteModelParameters), + typeof(DeleteModelResponse), + typeof(CountTokensParameters), + typeof(CountTokensResponse), + typeof(ComputeTokensParameters), + typeof(ComputeTokensResponse), + typeof(GenerateVideosParameters), + typeof(GenerateVideosOperation), + typeof(GenerateVideosResponse), + typeof(GetOperationParameters), + typeof(FetchPredictOperationParameters), + typeof(TuningJob), + typeof(GetTuningJobParameters), + typeof(ListTuningJobsParameters), + typeof(ListTuningJobsResponse), + typeof(CancelTuningJobParameters), + typeof(CancelTuningJobResponse), + typeof(CreateTuningJobParametersPrivate), + typeof(TuningOperation), + // Live/Realtime types + typeof(LiveConnectParameters), + typeof(LiveServerMessage), + typeof(LiveClientMessage), + typeof(LiveClientRealtimeInput), + typeof(LiveClientSetup), + typeof(LiveClientContent), + typeof(LiveClientToolResponse), + typeof(LiveConnectConfig), + typeof(LiveSendClientContentParameters), + typeof(LiveSendRealtimeInputParameters), + typeof(LiveSendToolResponseParameters), + typeof(LiveServerContent), + typeof(LiveServerGoAway), + typeof(LiveServerSetupComplete), + typeof(LiveServerToolCall), + typeof(LiveServerToolCallCancellation), + typeof(LiveServerSessionResumptionUpdate), + typeof(RealtimeInputConfig), + // Transformer types + typeof(Content), + typeof(Schema), + typeof(SpeechConfig), + typeof(Tool), + typeof(Blob), + // Collection types + typeof(List), + typeof(List), + }; + + var missingTypes = new List(); + + foreach (var type in requiredTypes) + { + var typeInfo = GenAIJsonContext.Default.GetTypeInfo(type); + if (typeInfo == null) + { + missingTypes.Add(type); + } + } + + Assert.AreEqual(0, missingTypes.Count, + $"The following types are missing from GenAIJsonContext and will fail in AOT: " + + $"{string.Join(", ", missingTypes.Select(t => t.Name))}"); + } + + /// + /// Verifies that key nested types (transitively reachable from root types) + /// are auto-discovered by the source generator. + /// + [TestMethod] + public void GenAIJsonContext_AutoDiscoversNestedTypes() + { + // These types are NOT explicitly registered but should be auto-discovered + // as properties of registered root types + var nestedTypes = new[] + { + typeof(Part), + typeof(Candidate), + typeof(SafetyRating), + typeof(FunctionDeclaration), + typeof(GenerationConfig), + typeof(ToolConfig), + typeof(FunctionCall), + typeof(FunctionResponse), + typeof(CitationMetadata), + typeof(GenerateContentResponseUsageMetadata), + }; + + var missingTypes = new List(); + + foreach (var type in nestedTypes) + { + var typeInfo = GenAIJsonContext.Default.GetTypeInfo(type); + if (typeInfo == null) + { + missingTypes.Add(type); + } + } + + Assert.AreEqual(0, missingTypes.Count, + $"The following nested types are NOT auto-discovered by the source generator. " + + $"Add [JsonSerializable(typeof(X))] to GenAIJsonContext: " + + $"{string.Join(", ", missingTypes.Select(t => t.Name))}"); + } + + /// + /// Verifies that serializing and deserializing a realtime message round-trips correctly + /// using only the source-generated context (no reflection fallback). + /// + [TestMethod] + public void GenAIJsonContext_RoundTripsLiveServerMessage() + { + var message = new LiveServerMessage + { + ServerContent = new LiveServerContent + { + ModelTurn = new Content + { + Role = "model", + Parts = new List + { + new Part { Text = "Hello from the model" } + } + } + } + }; + + // Serialize using source-gen context only (no reflection) + var json = JsonSerializer.Serialize(message, GenAIJsonContext.Default.LiveServerMessage); + Assert.IsFalse(string.IsNullOrEmpty(json)); + + // Deserialize using source-gen context + var deserialized = JsonSerializer.Deserialize(json, GenAIJsonContext.Default.LiveServerMessage); + Assert.IsNotNull(deserialized); + Assert.IsNotNull(deserialized.ServerContent); + Assert.AreEqual("model", deserialized.ServerContent.ModelTurn?.Role); + Assert.AreEqual("Hello from the model", deserialized.ServerContent.ModelTurn?.Parts?[0]?.Text); + } + + /// + /// Verifies that serializing a GenerateContentParameters type round-trips correctly, + /// exercising a deep type graph (Content, Part, Tool, FunctionDeclaration, Schema, etc). + /// + [TestMethod] + public void GenAIJsonContext_RoundTripsGenerateContentParameters() + { + var parameters = new GenerateContentParameters + { + Model = "gemini-2.0-flash", + Contents = new List + { + new Content + { + Role = "user", + Parts = new List { new Part { Text = "Hello" } } + } + }, + Config = new GenerateContentConfig + { + MaxOutputTokens = 100, + Temperature = 0.7f, + } + }; + + var json = JsonSerializer.Serialize(parameters, GenAIJsonContext.Default.GenerateContentParameters); + Assert.IsFalse(string.IsNullOrEmpty(json)); + Assert.IsTrue(json.Contains("gemini-2.0-flash")); + + var deserialized = JsonSerializer.Deserialize(json, GenAIJsonContext.Default.GenerateContentParameters); + Assert.IsNotNull(deserialized); + Assert.AreEqual("gemini-2.0-flash", deserialized.Model); + Assert.AreEqual(1, deserialized.Contents?.Count); + } + } +} diff --git a/Google.GenAI.Tests/GoogleGenAIRealtimeTest.cs b/Google.GenAI.Tests/GoogleGenAIRealtimeTest.cs new file mode 100644 index 00000000..ac827b85 --- /dev/null +++ b/Google.GenAI.Tests/GoogleGenAIRealtimeTest.cs @@ -0,0 +1,2852 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma warning disable MEAI001 // Experimental AI API + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Google.GenAI.Types; +using Microsoft.Extensions.AI; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +#pragma warning disable MEAI001 + +namespace Google.GenAI.Tests; + +[TestClass] +public class GoogleGenAIRealtimeClientTest +{ + #region Extension Method Tests + + [TestMethod] + public void AsIRealtimeClient_NullClient_ThrowsArgumentNullException() + { + Client? client = null; + Assert.ThrowsException(() => client!.AsIRealtimeClient("model")); + } + + [TestMethod] + public void AsIRealtimeClient_ValidClient_ReturnsNonNull() + { + var client = new Client(apiKey: "fake-api-key"); + var realtimeClient = client.AsIRealtimeClient("model"); + Assert.IsNotNull(realtimeClient); + } + + [TestMethod] + public void AsIRealtimeClient_NullModelId_ReturnsNonNull() + { + var client = new Client(apiKey: "fake-api-key"); + var realtimeClient = client.AsIRealtimeClient(); + Assert.IsNotNull(realtimeClient); + } + + #endregion + + #region Client Constructor Tests + + [TestMethod] + public void RealtimeClient_NullClient_ThrowsArgumentNullException() + { + Assert.ThrowsException( + () => new GoogleGenAIRealtimeClient((Client)null!, "model")); + } + + [TestMethod] + public void RealtimeClient_NullModelId_Succeeds() + { + var client = new Client(apiKey: "fake-api-key"); + var realtimeClient = new GoogleGenAIRealtimeClient(client); + Assert.IsNotNull(realtimeClient); + } + + #endregion + + #region Client GetService Tests + + [TestMethod] + public void RealtimeClient_GetService_ReturnsMetadata() + { + var client = new Client(apiKey: "fake-api-key"); + var realtimeClient = new GoogleGenAIRealtimeClient(client, "my-model"); + + var metadata = realtimeClient.GetService(typeof(ChatClientMetadata)) as ChatClientMetadata; + Assert.IsNotNull(metadata); + Assert.AreEqual("google-genai", metadata.ProviderName); + Assert.AreEqual("my-model", metadata.DefaultModelId); + } + + [TestMethod] + public void RealtimeClient_GetService_ReturnsSelf() + { + var client = new Client(apiKey: "fake-api-key"); + var realtimeClient = new GoogleGenAIRealtimeClient(client, "model"); + + var self = realtimeClient.GetService(typeof(GoogleGenAIRealtimeClient)); + Assert.AreSame(realtimeClient, self); + } + + [TestMethod] + public void RealtimeClient_GetService_ReturnsInnerClient() + { + var client = new Client(apiKey: "fake-api-key"); + var realtimeClient = new GoogleGenAIRealtimeClient(client, "model"); + + var inner = realtimeClient.GetService(typeof(Client)); + Assert.AreSame(client, inner); + } + + [TestMethod] + public void RealtimeClient_GetService_NullServiceType_ThrowsArgumentNullException() + { + var client = new Client(apiKey: "fake-api-key"); + var realtimeClient = new GoogleGenAIRealtimeClient(client, "model"); + + Assert.ThrowsException( + () => realtimeClient.GetService(null!)); + } + + [TestMethod] + public void RealtimeClient_GetService_WithKey_ReturnsNull() + { + var client = new Client(apiKey: "fake-api-key"); + var realtimeClient = new GoogleGenAIRealtimeClient(client, "model"); + + var result = realtimeClient.GetService(typeof(ChatClientMetadata), serviceKey: "some-key"); + Assert.IsNull(result); + } + + [TestMethod] + public void RealtimeClient_GetService_UnknownType_ReturnsNull() + { + var client = new Client(apiKey: "fake-api-key"); + var realtimeClient = new GoogleGenAIRealtimeClient(client, "model"); + + var result = realtimeClient.GetService(typeof(string)); + Assert.IsNull(result); + } + + #endregion + + #region Client Dispose Tests + + [TestMethod] + public void RealtimeClient_Dispose_IsIdempotent() + { + var client = new Client(apiKey: "fake-api-key"); + var realtimeClient = new GoogleGenAIRealtimeClient(client, "model"); + + // Should not throw + realtimeClient.Dispose(); + realtimeClient.Dispose(); + } + + #endregion + + #region Client CreateSession Tests + + [TestMethod] + public async Task RealtimeClient_CreateSession_NoModel_ThrowsInvalidOperationException() + { + var client = new Client(apiKey: "fake-api-key"); + var realtimeClient = new GoogleGenAIRealtimeClient(client); + + await Assert.ThrowsExceptionAsync( + () => realtimeClient.CreateSessionAsync(new RealtimeSessionOptions())); + } + + [TestMethod] + public async Task RealtimeClient_CreateSession_Cancelled_ThrowsOperationCanceledException() + { + var client = new Client(apiKey: "fake-api-key"); + var realtimeClient = new GoogleGenAIRealtimeClient(client, "model"); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsExceptionAsync( + () => realtimeClient.CreateSessionAsync(new RealtimeSessionOptions(), cts.Token)); + } + + #endregion + + #region ToGoogleFunctionDeclaration Tests + + [TestMethod] + public void ToGoogleFunctionDeclaration_MapsNameAndDescription() + { + var function = AIFunctionFactory.Create(() => "result", "myFunc", "A test function"); + + var declaration = GoogleGenAIRealtimeSession.ToGoogleFunctionDeclaration(function); + + Assert.AreEqual("myFunc", declaration.Name); + Assert.AreEqual("A test function", declaration.Description); + } + + [TestMethod] + public void ToGoogleFunctionDeclaration_WithJsonSchema_MapsParameters() + { + var function = AIFunctionFactory.Create((string name, int age) => $"{name} is {age}", + "greet", "Greets a person"); + + var declaration = GoogleGenAIRealtimeSession.ToGoogleFunctionDeclaration(function); + + Assert.AreEqual("greet", declaration.Name); + Assert.AreEqual("Greets a person", declaration.Description); + + // The Parameters (Google Schema) should be set since the function has parameters + Assert.IsNotNull(declaration.Parameters); + } + + [TestMethod] + public void ToGoogleFunctionDeclaration_NoParameters_HasNoParameterSchema() + { + var function = AIFunctionFactory.Create(() => "hello", "noParams", "No parameters"); + + var declaration = GoogleGenAIRealtimeSession.ToGoogleFunctionDeclaration(function); + + Assert.AreEqual("noParams", declaration.Name); + } + + #endregion +} + +[TestClass] +public class GoogleGenAIRealtimeSessionTest +{ + private Mock _mockWebSocket = null!; + private AsyncSession _asyncSession = null!; + private GoogleGenAIRealtimeSession _session = null!; + + [TestInitialize] + public void TestInitialize() + { + _mockWebSocket = new Mock(); + _mockWebSocket.Setup(ws => ws.State).Returns(WebSocketState.Open); + _asyncSession = new AsyncSession(_mockWebSocket.Object, new TestApiClient(false)); + _session = new GoogleGenAIRealtimeSession(_asyncSession, "test-model", null); + } + + [TestCleanup] + public async Task TestCleanup() + { + await _session.DisposeAsync(); + } + + #region Constructor Tests + + [TestMethod] + public void Session_NullAsyncSession_ThrowsArgumentNullException() + { + Assert.ThrowsException( + () => new GoogleGenAIRealtimeSession(null!, "model", null)); + } + + [TestMethod] + public void Session_ValidConstruction_SetsOptions() + { + var options = new RealtimeSessionOptions { Model = "my-model" }; + var session = new GoogleGenAIRealtimeSession( + _asyncSession, "my-model", options); + + Assert.AreSame(options, session.Options); + } + + [TestMethod] + public void Session_NullOptions_OptionsIsNull() + { + Assert.IsNull(_session.Options); + } + + #endregion + + #region Session GetService Tests + + [TestMethod] + public void Session_GetService_ReturnsMetadata() + { + var metadata = _session.GetService(typeof(ChatClientMetadata)) as ChatClientMetadata; + Assert.IsNotNull(metadata); + Assert.AreEqual("google-genai", metadata.ProviderName); + Assert.AreEqual("test-model", metadata.DefaultModelId); + } + + [TestMethod] + public void Session_GetService_ReturnsSelf() + { + var self = _session.GetService(typeof(GoogleGenAIRealtimeSession)); + Assert.AreSame(_session, self); + } + + [TestMethod] + public void Session_GetService_ReturnsInnerAsyncSession() + { + var inner = _session.GetService(typeof(AsyncSession)); + Assert.AreSame(_asyncSession, inner); + } + + [TestMethod] + public void Session_GetService_NullServiceType_ThrowsArgumentNullException() + { + Assert.ThrowsException( + () => _session.GetService(null!)); + } + + [TestMethod] + public void Session_GetService_WithKey_ReturnsNull() + { + var result = _session.GetService(typeof(ChatClientMetadata), serviceKey: "key"); + Assert.IsNull(result); + } + + [TestMethod] + public void Session_GetService_UnknownType_ReturnsNull() + { + var result = _session.GetService(typeof(string)); + Assert.IsNull(result); + } + + #endregion + + #region Session DisposeAsync Tests + + [TestMethod] + public async Task Session_DisposeAsync_IsIdempotent() + { + var session = new GoogleGenAIRealtimeSession(_asyncSession, "model", null); + + // Should not throw on double dispose + await session.DisposeAsync(); + await session.DisposeAsync(); + } + + #endregion + + #region SendAsync Guard Tests + + [TestMethod] + public async Task SendAsync_NullMessage_ThrowsArgumentNullException() + { + await Assert.ThrowsExceptionAsync( + () => _session.SendAsync(null!)); + } + + [TestMethod] + public async Task SendAsync_AfterDispose_ThrowsObjectDisposedException() + { + await _session.DisposeAsync(); + + await Assert.ThrowsExceptionAsync( + () => _session.SendAsync(new CreateResponseRealtimeClientMessage())); + } + + [TestMethod] + public async Task SendAsync_CancelledToken_ThrowsOperationCanceledException() + { + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsExceptionAsync( + () => _session.SendAsync(new CreateResponseRealtimeClientMessage(), cts.Token)); + } + + [TestMethod] + public async Task SendAsync_SessionUpdate_DoesNotUpdateOptions() + { + var initialOptions = new RealtimeSessionOptions { Model = "initial-model" }; + var session = new GoogleGenAIRealtimeSession(_asyncSession, "initial-model", initialOptions); + + var newOptions = new RealtimeSessionOptions { Model = "updated-model" }; + var msg = new SessionUpdateRealtimeClientMessage(newOptions); + + await session.SendAsync(msg); + + // Gemini does not support mid-session updates; options should remain unchanged. + Assert.AreSame(initialOptions, session.Options); + + await session.DisposeAsync(); + } + + [TestMethod] + public void SendAsync_SessionUpdate_NullOptions_ThrowsArgumentNullException() + { + Assert.ThrowsException( + () => new SessionUpdateRealtimeClientMessage(null!)); + } + + [TestMethod] + public async Task SendAsync_UnknownMessageType_DoesNotThrow() + { + // A generic RealtimeClientMessage should just be ignored (default case) + var msg = new RealtimeClientMessage(); + await _session.SendAsync(msg); + } + + #endregion + + #region Audio Buffering Tests + + [TestMethod] + public void SendAsync_AudioAppend_NoContent_ThrowsArgumentNullException() + { + Assert.ThrowsException( + () => new InputAudioBufferAppendRealtimeClientMessage(null!)); + } + + [TestMethod] + public async Task SendAsync_AudioAppend_NonAudioContent_DoesNotThrow() + { + var msg = new InputAudioBufferAppendRealtimeClientMessage(new DataContent("data:text/plain;base64,SGVsbG8=", "text/plain")); + await _session.SendAsync(msg); + } + + [TestMethod] + public async Task SendAsync_AudioAppendThenCommit_SendsFrames() + { + // Prepare audio content + byte[] audioData = new byte[100]; + new Random(42).NextBytes(audioData); + string base64 = Convert.ToBase64String(audioData); + var dataUri = $"data:audio/pcm;base64,{base64}"; + + var appendMsg = new InputAudioBufferAppendRealtimeClientMessage(new DataContent(dataUri, "audio/pcm")); + + // Track SendAsync calls on the WebSocket + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + var copy = new byte[buffer.Count]; + Buffer.BlockCopy(buffer.Array!, buffer.Offset, copy, 0, buffer.Count); + sentMessages.Add(copy); + }) + .Returns(Task.CompletedTask); + + await _session.SendAsync(appendMsg); + + // Nothing sent yet (just buffered) + Assert.AreEqual(0, sentMessages.Count); + + // Commit triggers actual send + await _session.SendAsync(new InputAudioBufferCommitRealtimeClientMessage()); + + // Should have sent: ActivityStart + audio frame(s) + ActivityEnd + Assert.IsTrue(sentMessages.Count >= 3, + $"Expected at least 3 WebSocket messages (ActivityStart + audio + ActivityEnd), got {sentMessages.Count}"); + } + + [TestMethod] + public async Task SendAsync_AudioCommit_EmptyBuffer_DoesNothing() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + var copy = new byte[buffer.Count]; + Buffer.BlockCopy(buffer.Array!, buffer.Offset, copy, 0, buffer.Count); + sentMessages.Add(copy); + }) + .Returns(Task.CompletedTask); + + await _session.SendAsync(new InputAudioBufferCommitRealtimeClientMessage()); + + Assert.AreEqual(0, sentMessages.Count); + } + + [TestMethod] + public async Task SendAsync_AudioAppend_ExceedsBufferLimit_ThrowsInvalidOperationException() + { + // Create audio data close to the 10MB limit + byte[] largeAudio = new byte[10 * 1024 * 1024 + 1]; + string base64 = Convert.ToBase64String(largeAudio); + var dataUri = $"data:audio/pcm;base64,{base64}"; + + var appendMsg = new InputAudioBufferAppendRealtimeClientMessage(new DataContent(dataUri, "audio/pcm")); + + await Assert.ThrowsExceptionAsync( + () => _session.SendAsync(appendMsg)); + } + + [TestMethod] + public async Task SendAsync_AudioAppend_LargeFrame_SplitsIntoMultipleFrames() + { + // Create audio data larger than 32KB frame limit (e.g. 70KB) + byte[] audioData = new byte[70_000]; + new Random(42).NextBytes(audioData); + string base64 = Convert.ToBase64String(audioData); + var dataUri = $"data:audio/pcm;base64,{base64}"; + + var appendMsg = new InputAudioBufferAppendRealtimeClientMessage(new DataContent(dataUri, "audio/pcm")); + + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + var copy = new byte[buffer.Count]; + Buffer.BlockCopy(buffer.Array!, buffer.Offset, copy, 0, buffer.Count); + sentMessages.Add(copy); + }) + .Returns(Task.CompletedTask); + + await _session.SendAsync(appendMsg); + await _session.SendAsync(new InputAudioBufferCommitRealtimeClientMessage()); + + // 70KB audio = 3 frames (32K + 32K + 6K) + ActivityStart + ActivityEnd = 5 messages + Assert.AreEqual(5, sentMessages.Count, + $"Expected 5 WebSocket messages (ActivityStart + 3 audio frames + ActivityEnd), got {sentMessages.Count}"); + } + + #endregion + + #region GetStreamingResponseAsync Tests + + [TestMethod] + public async Task GetStreamingResponseAsync_AfterDispose_ThrowsObjectDisposedException() + { + await _session.DisposeAsync(); + + await Assert.ThrowsExceptionAsync(async () => + { + await foreach (var _ in _session.GetStreamingResponseAsync()) + { + } + }); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_NullMessage_YieldsBreak() + { + // AsyncSession.ReceiveAsync returns null on close + var closeResult = new WebSocketReceiveResult( + 0, WebSocketMessageType.Close, true, + WebSocketCloseStatus.NormalClosure, "done"); + + _mockWebSocket + .Setup(ws => ws.ReceiveAsync( + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(closeResult); + + var messages = new List(); + await foreach (var msg in _session.GetStreamingResponseAsync()) + { + messages.Add(msg); + } + + Assert.AreEqual(0, messages.Count); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_CallerCancelled_YieldsNoResults() + { + // A pre-cancelled token causes the while-loop condition to be false immediately, + // so the iterator completes without throwing. + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var messages = new List(); + await foreach (var msg in _session.GetStreamingResponseAsync(cts.Token)) + { + messages.Add(msg); + } + + Assert.AreEqual(0, messages.Count); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_WebSocketException_YieldsBreak() + { + _mockWebSocket + .Setup(ws => ws.ReceiveAsync( + It.IsAny>(), + It.IsAny())) + .ThrowsAsync(new WebSocketException("connection closed")); + + var messages = new List(); + await foreach (var msg in _session.GetStreamingResponseAsync()) + { + messages.Add(msg); + } + + Assert.AreEqual(0, messages.Count); + } + + #endregion + + #region MapServerMessage Tests + + [TestMethod] + public async Task GetStreamingResponseAsync_SetupComplete_IsSkipped() + { + SetupReceiveOnce(new LiveServerMessage { SetupComplete = new LiveServerSetupComplete() }); + + var messages = await CollectMessages(); + + Assert.AreEqual(0, messages.Count); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_TextResponse_EmitsResponseCreatedAndTextDelta() + { + SetupReceiveOnce(new LiveServerMessage + { + ServerContent = new LiveServerContent + { + ModelTurn = new Content + { + Parts = new List { new Part { Text = "Hello world" } } + } + } + }); + + var messages = await CollectMessages(); + + // Expect: ResponseCreated + OutputTextDelta + Assert.AreEqual(2, messages.Count); + Assert.AreEqual(RealtimeServerMessageType.ResponseCreated, messages[0].Type); + Assert.AreEqual(RealtimeServerMessageType.OutputTextDelta, messages[1].Type); + Assert.AreEqual("Hello world", ((OutputTextAudioRealtimeServerMessage)messages[1]).Text); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_AudioResponse_EmitsAudioDelta() + { + byte[] audioBytes = new byte[] { 1, 2, 3, 4 }; + SetupReceiveOnce(new LiveServerMessage + { + ServerContent = new LiveServerContent + { + ModelTurn = new Content + { + Parts = new List + { + new Part + { + InlineData = new Blob + { + Data = audioBytes, + MimeType = "audio/pcm", + } + } + } + } + } + }); + + var messages = await CollectMessages(); + + // Expect: ResponseCreated + OutputAudioDelta + Assert.AreEqual(2, messages.Count); + Assert.AreEqual(RealtimeServerMessageType.ResponseCreated, messages[0].Type); + Assert.AreEqual(RealtimeServerMessageType.OutputAudioDelta, messages[1].Type); + var audioMsg = (OutputTextAudioRealtimeServerMessage)messages[1]; + Assert.AreEqual(Convert.ToBase64String(audioBytes), audioMsg.Audio); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_TurnComplete_EmitsResponseDone() + { + SetupReceiveSequence(new[] + { + new LiveServerMessage + { + ServerContent = new LiveServerContent + { + ModelTurn = new Content + { + Parts = new List { new Part { Text = "Hi" } } + } + } + }, + new LiveServerMessage + { + ServerContent = new LiveServerContent { TurnComplete = true } + } + }); + + var messages = await CollectMessages(); + + // Expect: ResponseCreated + OutputTextDelta + ResponseDone + Assert.AreEqual(3, messages.Count); + Assert.AreEqual(RealtimeServerMessageType.ResponseCreated, messages[0].Type); + Assert.AreEqual(RealtimeServerMessageType.OutputTextDelta, messages[1].Type); + Assert.AreEqual(RealtimeServerMessageType.ResponseDone, messages[2].Type); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_InputTranscription_EmitsTranscriptionCompleted() + { + SetupReceiveOnce(new LiveServerMessage + { + ServerContent = new LiveServerContent + { + InputTranscription = new Transcription { Text = "what is the weather?" } + } + }); + + var messages = await CollectMessages(); + + Assert.AreEqual(1, messages.Count); + Assert.AreEqual(RealtimeServerMessageType.InputAudioTranscriptionCompleted, messages[0].Type); + var transcription = (InputAudioTranscriptionRealtimeServerMessage)messages[0]; + Assert.AreEqual("what is the weather?", transcription.Transcription); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_OutputTranscription_EmitsTranscriptionDelta() + { + SetupReceiveOnce(new LiveServerMessage + { + ServerContent = new LiveServerContent + { + OutputTranscription = new Transcription { Text = "The weather is sunny." } + } + }); + + var messages = await CollectMessages(); + + Assert.AreEqual(1, messages.Count); + Assert.AreEqual(RealtimeServerMessageType.OutputAudioTranscriptionDelta, messages[0].Type); + var outputMsg = (OutputTextAudioRealtimeServerMessage)messages[0]; + Assert.AreEqual("The weather is sunny.", outputMsg.Text); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_ToolCall_EmitsResponseCreatedAndFunctionCall() + { + SetupReceiveOnce(new LiveServerMessage + { + ToolCall = new LiveServerToolCall + { + FunctionCalls = new List + { + new FunctionCall + { + Id = "call-1", + Name = "get_weather", + Args = new Dictionary { ["city"] = "Seattle" } + } + } + } + }); + + var messages = await CollectMessages(); + + // Expect: ResponseCreated + ResponseOutputItemAdded + ResponseOutputItemDone + Assert.AreEqual(3, messages.Count); + Assert.AreEqual(RealtimeServerMessageType.ResponseCreated, messages[0].Type); + Assert.AreEqual(RealtimeServerMessageType.ResponseOutputItemAdded, messages[1].Type); + Assert.AreEqual(RealtimeServerMessageType.ResponseOutputItemDone, messages[2].Type); + + var itemMsg = (ResponseOutputItemRealtimeServerMessage)messages[1]; + var functionCall = itemMsg.Item?.Contents?.OfType().First(); + Assert.IsNotNull(functionCall); + Assert.AreEqual("call-1", functionCall.CallId); + Assert.AreEqual("get_weather", functionCall.Name); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_GoAway_EmitsError() + { + SetupReceiveOnce(new LiveServerMessage + { + GoAway = new LiveServerGoAway() + }); + + var messages = await CollectMessages(); + + Assert.AreEqual(1, messages.Count); + var errorMsg = messages[0] as ErrorRealtimeServerMessage; + Assert.IsNotNull(errorMsg); + Assert.IsNotNull(errorMsg.Error); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_UsageMetadata_EmitsResponseDone_WhenResponseInProgress() + { + SetupReceiveSequence(new[] + { + // Start a response so _responseInProgress is true + new LiveServerMessage + { + ServerContent = new LiveServerContent + { + ModelTurn = new Content + { + Parts = new List { new Part { Text = "response" } } + } + } + }, + // Usage metadata should emit ResponseDone + new LiveServerMessage + { + UsageMetadata = new UsageMetadata + { + PromptTokenCount = 10, + ResponseTokenCount = 20, + TotalTokenCount = 30, + } + } + }); + + var messages = await CollectMessages(); + + // ResponseCreated + OutputTextDelta + ResponseDone(usage) + Assert.AreEqual(3, messages.Count); + Assert.AreEqual(RealtimeServerMessageType.ResponseDone, messages[2].Type); + var responseDone = (ResponseCreatedRealtimeServerMessage)messages[2]; + Assert.IsNotNull(responseDone.Usage); + Assert.AreEqual(10, responseDone.Usage.InputTokenCount); + Assert.AreEqual(20, responseDone.Usage.OutputTokenCount); + Assert.AreEqual(30, responseDone.Usage.TotalTokenCount); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_UsageMetadata_NoResponseInProgress_IsSkipped() + { + // No prior model response — _responseInProgress is false + SetupReceiveOnce(new LiveServerMessage + { + UsageMetadata = new UsageMetadata + { + PromptTokenCount = 5, + ResponseTokenCount = 10, + TotalTokenCount = 15, + } + }); + + var messages = await CollectMessages(); + + // Usage metadata without a response in progress should be skipped + Assert.AreEqual(0, messages.Count); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_TurnComplete_PreventsDoubleResponseDone_FromUsage() + { + SetupReceiveSequence(new[] + { + // Model response + new LiveServerMessage + { + ServerContent = new LiveServerContent + { + ModelTurn = new Content + { + Parts = new List { new Part { Text = "Hi" } } + } + } + }, + // TurnComplete — emits ResponseDone and resets _responseInProgress + new LiveServerMessage + { + ServerContent = new LiveServerContent { TurnComplete = true } + }, + // Usage after TurnComplete — should NOT emit another ResponseDone + new LiveServerMessage + { + UsageMetadata = new UsageMetadata + { + PromptTokenCount = 10, + ResponseTokenCount = 20, + TotalTokenCount = 30, + } + } + }); + + var messages = await CollectMessages(); + + // ResponseCreated + OutputTextDelta + ResponseDone(TurnComplete) — no second ResponseDone + Assert.AreEqual(3, messages.Count); + var responseDoneMessages = messages.Where( + m => m.Type == RealtimeServerMessageType.ResponseDone).ToList(); + Assert.AreEqual(1, responseDoneMessages.Count, "Should only have one ResponseDone"); + } + + #endregion + + #region ConversationItemCreate Tests + + [TestMethod] + public async Task SendAsync_ConversationItemCreate_TextContent_SendsClientContent() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new TextContent("Hello from user") }, + role: ChatRole.User)); + + await _session.SendAsync(msg); + + Assert.AreEqual(1, sentMessages.Count); + // Verify the sent JSON contains our text + Assert.IsTrue(sentMessages[0].Contains("Hello from user"), + "Sent message should contain the text content"); + } + + [TestMethod] + public async Task SendAsync_ConversationItemCreate_FunctionResult_SendsToolResponse() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new FunctionResultContent("call-123", "sunny") }, + role: ChatRole.Tool)); + + await _session.SendAsync(msg); + + // Function results are buffered until CreateResponse + Assert.AreEqual(0, sentMessages.Count, + "Function results should be buffered, not sent immediately"); + + // Flush via CreateResponse + await _session.SendAsync(new CreateResponseRealtimeClientMessage()); + + Assert.AreEqual(1, sentMessages.Count, + "Flushed tool response should be sent as a single WebSocket message"); + Assert.IsTrue(sentMessages[0].Contains("call-123"), + "Tool response should contain the call ID"); + } + + [TestMethod] + public async Task SendAsync_ConversationItemCreate_FunctionResult_IncludesFunctionName() + { + // First, simulate receiving a tool call so the session caches the CallId → Name mapping. + SetupReceiveOnce(new LiveServerMessage + { + ToolCall = new LiveServerToolCall + { + FunctionCalls = new List + { + new FunctionCall { Id = "call-42", Name = "get_weather" } + } + } + }); + + await CollectMessages(); + + // Now send the function result back + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new FunctionResultContent("call-42", "sunny") }, + role: ChatRole.Tool)); + + await _session.SendAsync(msg); + + // Function results are buffered until CreateResponse + Assert.AreEqual(0, sentMessages.Count); + + // Flush via CreateResponse + await _session.SendAsync(new CreateResponseRealtimeClientMessage()); + + Assert.AreEqual(1, sentMessages.Count); + Assert.IsTrue(sentMessages[0].Contains("get_weather"), + "Tool response should include the function name from the original tool call"); + } + + [TestMethod] + public async Task SendAsync_ConversationItemCreate_EmptyContents_DoesNotSend() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List(), + role: ChatRole.User)); + + await _session.SendAsync(msg); + + Assert.AreEqual(0, sentMessages.Count); + } + + [TestMethod] + public async Task SendAsync_ConversationItemCreate_AssistantRole_MappedToModel() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new TextContent("I am the model") }, + role: ChatRole.Assistant)); + + await _session.SendAsync(msg); + + Assert.AreEqual(1, sentMessages.Count); + // Gemini uses "model" role, not "assistant" + Assert.IsTrue(sentMessages[0].Contains("model"), + "Assistant role should be mapped to 'model' for Gemini"); + } + + #endregion + + #region BuildLiveConnectConfig Tests + + [TestMethod] + public void BuildLiveConnectConfig_NullOptions_DefaultsToAudioModality() + { + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(null); + + Assert.IsNotNull(config.ResponseModalities); + Assert.AreEqual(1, config.ResponseModalities.Count); + Assert.AreEqual(Modality.Audio, config.ResponseModalities[0]); + Assert.IsTrue(config.RealtimeInputConfig.AutomaticActivityDetection.Disabled); + } + + [TestMethod] + public void BuildLiveConnectConfig_SystemInstructions_MappedCorrectly() + { + var options = new RealtimeSessionOptions + { + Instructions = "You are a helpful assistant.", + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNotNull(config.SystemInstruction); + Assert.AreEqual(1, config.SystemInstruction.Parts.Count); + Assert.AreEqual("You are a helpful assistant.", config.SystemInstruction.Parts[0].Text); + Assert.AreEqual("user", config.SystemInstruction.Role); + } + + [TestMethod] + public void BuildLiveConnectConfig_EmptyInstructions_NoSystemInstruction() + { + var options = new RealtimeSessionOptions + { + Instructions = "", + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNull(config.SystemInstruction); + } + + [TestMethod] + public void BuildLiveConnectConfig_OutputModalities_AudioAndText() + { + var options = new RealtimeSessionOptions + { + OutputModalities = new List { "audio", "text" }, + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.AreEqual(2, config.ResponseModalities.Count); + Assert.AreEqual(Modality.Audio, config.ResponseModalities[0]); + Assert.AreEqual(Modality.Text, config.ResponseModalities[1]); + } + + [TestMethod] + public void BuildLiveConnectConfig_NoOutputModalities_DefaultsToAudio() + { + var options = new RealtimeSessionOptions(); + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.AreEqual(1, config.ResponseModalities.Count); + Assert.AreEqual(Modality.Audio, config.ResponseModalities[0]); + } + + [TestMethod] + public void BuildLiveConnectConfig_UnknownModality_DefaultsToText() + { + var options = new RealtimeSessionOptions + { + OutputModalities = new List { "video" }, + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.AreEqual(1, config.ResponseModalities.Count); + Assert.AreEqual(Modality.Text, config.ResponseModalities[0]); + } + + [TestMethod] + public void BuildLiveConnectConfig_Voice_MappedToSpeechConfig() + { + var options = new RealtimeSessionOptions + { + Voice = "Puck", + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNotNull(config.SpeechConfig); + Assert.IsNotNull(config.SpeechConfig.VoiceConfig); + Assert.AreEqual("Puck", config.SpeechConfig.VoiceConfig.PrebuiltVoiceConfig.VoiceName); + } + + [TestMethod] + public void BuildLiveConnectConfig_NullVoice_NoSpeechConfig() + { + var options = new RealtimeSessionOptions(); + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNull(config.SpeechConfig); + } + + [TestMethod] + public void BuildLiveConnectConfig_MaxOutputTokens_MappedToGenerationConfig() + { + var options = new RealtimeSessionOptions + { + MaxOutputTokens = 500, + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNotNull(config.GenerationConfig); + Assert.AreEqual(500, config.GenerationConfig.MaxOutputTokens); + } + + [TestMethod] + public void BuildLiveConnectConfig_NoMaxOutputTokens_NoGenerationConfig() + { + var options = new RealtimeSessionOptions(); + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNull(config.GenerationConfig); + } + + [TestMethod] + public void BuildLiveConnectConfig_TranscriptionOptions_EnablesBothDirections() + { + var options = new RealtimeSessionOptions + { + TranscriptionOptions = new TranscriptionOptions(), + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNotNull(config.InputAudioTranscription); + Assert.IsNotNull(config.OutputAudioTranscription); + } + + [TestMethod] + public void BuildLiveConnectConfig_NoTranscriptionOptions_NoTranscription() + { + var options = new RealtimeSessionOptions(); + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNull(config.InputAudioTranscription); + Assert.IsNull(config.OutputAudioTranscription); + } + + [TestMethod] + public void BuildLiveConnectConfig_NoVadOptions_DisablesVadByDefault() + { + var options = new RealtimeSessionOptions(); + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNotNull(config.RealtimeInputConfig); + Assert.IsNotNull(config.RealtimeInputConfig.AutomaticActivityDetection); + Assert.IsTrue(config.RealtimeInputConfig.AutomaticActivityDetection.Disabled); + } + + [TestMethod] + public void BuildLiveConnectConfig_VadEnabled_EnablesAutomaticActivityDetection() + { + var options = new RealtimeSessionOptions + { + VoiceActivityDetection = new VoiceActivityDetectionOptions + { + Enabled = true, + AllowInterruption = true, + }, + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNotNull(config.RealtimeInputConfig); + Assert.IsNotNull(config.RealtimeInputConfig.AutomaticActivityDetection); + Assert.IsFalse(config.RealtimeInputConfig.AutomaticActivityDetection.Disabled); + Assert.AreEqual( + ActivityHandling.StartOfActivityInterrupts, + config.RealtimeInputConfig.ActivityHandling); + } + + [TestMethod] + public void BuildLiveConnectConfig_VadEnabled_NoInterruption() + { + var options = new RealtimeSessionOptions + { + VoiceActivityDetection = new VoiceActivityDetectionOptions + { + Enabled = true, + AllowInterruption = false, + }, + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNotNull(config.RealtimeInputConfig); + Assert.IsFalse(config.RealtimeInputConfig.AutomaticActivityDetection.Disabled); + Assert.AreEqual( + ActivityHandling.NoInterruption, + config.RealtimeInputConfig.ActivityHandling); + } + + [TestMethod] + public void BuildLiveConnectConfig_VadDisabled_DisablesAutomaticActivityDetection() + { + var options = new RealtimeSessionOptions + { + VoiceActivityDetection = new VoiceActivityDetectionOptions + { + Enabled = false, + }, + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNotNull(config.RealtimeInputConfig); + Assert.IsTrue(config.RealtimeInputConfig.AutomaticActivityDetection.Disabled); + } + + [TestMethod] + public void BuildLiveConnectConfig_Tools_MappedToFunctionDeclarations() + { + var fn = AIFunctionFactory.Create( + (string city) => $"Weather in {city}: sunny", + "get_weather", + "Gets the weather"); + + var options = new RealtimeSessionOptions + { + Tools = new List { fn }, + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNotNull(config.Tools); + Assert.AreEqual(1, config.Tools.Count); + Assert.AreEqual(1, config.Tools[0].FunctionDeclarations.Count); + Assert.AreEqual("get_weather", config.Tools[0].FunctionDeclarations[0].Name); + Assert.AreEqual("Gets the weather", config.Tools[0].FunctionDeclarations[0].Description); + } + + [TestMethod] + public void BuildLiveConnectConfig_EmptyTools_NoToolsConfig() + { + var options = new RealtimeSessionOptions + { + Tools = new List(), + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNull(config.Tools); + } + + [TestMethod] + public void BuildLiveConnectConfig_AllOptionsCombined_MapsEverything() + { + var fn = AIFunctionFactory.Create( + (string q) => "result", + "search", + "Searches things"); + + var options = new RealtimeSessionOptions + { + Instructions = "Be concise.", + OutputModalities = new List { "audio", "text" }, + Voice = "Aoede", + MaxOutputTokens = 1000, + Tools = new List { fn }, + TranscriptionOptions = new TranscriptionOptions(), + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.AreEqual("Be concise.", config.SystemInstruction.Parts[0].Text); + Assert.AreEqual(2, config.ResponseModalities.Count); + Assert.AreEqual("Aoede", config.SpeechConfig.VoiceConfig.PrebuiltVoiceConfig.VoiceName); + Assert.AreEqual(1000, config.GenerationConfig.MaxOutputTokens); + Assert.AreEqual(1, config.Tools[0].FunctionDeclarations.Count); + Assert.IsNotNull(config.InputAudioTranscription); + Assert.IsNotNull(config.OutputAudioTranscription); + Assert.IsTrue(config.RealtimeInputConfig.AutomaticActivityDetection.Disabled); + } + + [TestMethod] + public void BuildLiveConnectConfig_TranscriptionMode_OnlyInputTranscription() + { + var options = new RealtimeSessionOptions + { + SessionKind = RealtimeSessionKind.Transcription, + TranscriptionOptions = new TranscriptionOptions(), + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNotNull(config.InputAudioTranscription); + Assert.IsNull(config.OutputAudioTranscription); + } + + [TestMethod] + public void BuildLiveConnectConfig_TranscriptionMode_TextModalityOnly() + { + var options = new RealtimeSessionOptions + { + SessionKind = RealtimeSessionKind.Transcription, + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.AreEqual(1, config.ResponseModalities.Count); + Assert.AreEqual(Modality.Text, config.ResponseModalities[0]); + } + + [TestMethod] + public void BuildLiveConnectConfig_TranscriptionMode_NoVoiceOrToolsOrInstructions() + { + var fn = AIFunctionFactory.Create( + (string q) => "result", + "search", + "Searches things"); + + var options = new RealtimeSessionOptions + { + SessionKind = RealtimeSessionKind.Transcription, + Instructions = "Be concise.", + Voice = "Aoede", + MaxOutputTokens = 500, + Tools = new List { fn }, + TranscriptionOptions = new TranscriptionOptions(), + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + // Transcription-only: conversation-oriented options are ignored + Assert.IsNull(config.SystemInstruction); + Assert.IsNull(config.SpeechConfig); + Assert.IsNull(config.GenerationConfig); + Assert.IsNull(config.Tools); + Assert.IsNotNull(config.InputAudioTranscription); + Assert.IsNull(config.OutputAudioTranscription); + } + + [TestMethod] + public void BuildLiveConnectConfig_TranscriptionMode_LanguageCodeMapped() + { + var options = new RealtimeSessionOptions + { + SessionKind = RealtimeSessionKind.Transcription, + TranscriptionOptions = new TranscriptionOptions { SpeechLanguage = "en-US" }, + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNotNull(config.InputAudioTranscription); + Assert.AreEqual(1, config.InputAudioTranscription.LanguageCodes.Count); + Assert.AreEqual("en-US", config.InputAudioTranscription.LanguageCodes[0]); + } + + [TestMethod] + public void BuildLiveConnectConfig_TranscriptionMode_NoLanguage_NoLanguageCodes() + { + var options = new RealtimeSessionOptions + { + SessionKind = RealtimeSessionKind.Transcription, + TranscriptionOptions = new TranscriptionOptions(), + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNotNull(config.InputAudioTranscription); + Assert.IsNull(config.InputAudioTranscription.LanguageCodes); + } + + [TestMethod] + public void BuildLiveConnectConfig_TranscriptionMode_VadEnabled() + { + var options = new RealtimeSessionOptions + { + SessionKind = RealtimeSessionKind.Transcription, + VoiceActivityDetection = new VoiceActivityDetectionOptions { Enabled = true, AllowInterruption = false }, + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsFalse(config.RealtimeInputConfig.AutomaticActivityDetection.Disabled); + Assert.AreEqual(ActivityHandling.NoInterruption, config.RealtimeInputConfig.ActivityHandling); + } + + [TestMethod] + public void BuildLiveConnectConfig_TranscriptionMode_DefaultVadDisabled() + { + var options = new RealtimeSessionOptions + { + SessionKind = RealtimeSessionKind.Transcription, + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsTrue(config.RealtimeInputConfig.AutomaticActivityDetection.Disabled); + } + + [TestMethod] + public void BuildLiveConnectConfig_ConversationMode_LanguageCodeMapped() + { + var options = new RealtimeSessionOptions + { + TranscriptionOptions = new TranscriptionOptions { SpeechLanguage = "ja-JP" }, + }; + + var config = GoogleGenAIRealtimeClient.BuildLiveConnectConfig(options); + + Assert.IsNotNull(config.InputAudioTranscription); + Assert.AreEqual(1, config.InputAudioTranscription.LanguageCodes.Count); + Assert.AreEqual("ja-JP", config.InputAudioTranscription.LanguageCodes[0]); + // Output transcription is also enabled in conversation mode (no language codes) + Assert.IsNotNull(config.OutputAudioTranscription); + } + + #endregion + + #region ExtractDataBytes Tests + + [TestMethod] + public void ExtractDataBytes_ValidBase64DataUri_ExtractsBytes() + { + byte[] expected = new byte[] { 1, 2, 3, 4, 5 }; + string base64 = Convert.ToBase64String(expected); + var content = new DataContent($"data:audio/pcm;base64,{base64}", "audio/pcm"); + + byte[] result = GoogleGenAIRealtimeSession.ExtractDataBytes(content); + + CollectionAssert.AreEqual(expected, result); + } + + [TestMethod] + public void ExtractDataBytes_InvalidBase64_FallsBackToData() + { + byte[] expected = new byte[] { 10, 20, 30 }; + var content = new DataContent(expected, "audio/pcm"); + + byte[] result = GoogleGenAIRealtimeSession.ExtractDataBytes(content); + + CollectionAssert.AreEqual(expected, result); + } + + [TestMethod] + public void ExtractDataBytes_NullUri_UsesDirectData() + { + byte[] expected = new byte[] { 7, 8, 9 }; + var content = new DataContent(expected, "audio/pcm"); + + byte[] result = GoogleGenAIRealtimeSession.ExtractDataBytes(content); + + CollectionAssert.AreEqual(expected, result); + } + + [TestMethod] + public void ExtractDataBytes_DataUriNoComma_FallsBackToData() + { + byte[] expected = new byte[] { 42, 43, 44 }; + // DataContent with raw byte data but no URI + var content = new DataContent(expected, "audio/pcm"); + + byte[] result = GoogleGenAIRealtimeSession.ExtractDataBytes(content); + + CollectionAssert.AreEqual(expected, result); + } + + #endregion + + #region CreateResponseRealtimeClientMessage Tests + + [TestMethod] + public async Task SendAsync_CreateResponse_AfterConversationItem_SendsTurnComplete() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + // Send a conversation item with text + var itemMsg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new TextContent("Hello") }, + role: ChatRole.User)); + await _session.SendAsync(itemMsg); + + // Text input auto-triggers a response in Gemini's Live API, so the text + // send happened during CreateConversationItem, not CreateResponse. + Assert.AreEqual(1, sentMessages.Count, "Text should be sent via SendRealtimeInputAsync"); + Assert.IsTrue(sentMessages[0].Contains("Hello"), + "The sent message should contain the text content"); + + sentMessages.Clear(); + + // CreateResponse after text input does nothing — text auto-triggers. + var createResp = new CreateResponseRealtimeClientMessage(); + await _session.SendAsync(createResp); + + Assert.AreEqual(0, sentMessages.Count, + "CreateResponse should not send additional messages after text input (auto-triggers)"); + } + + [TestMethod] + public async Task SendAsync_CreateResponse_AfterAudioCommit_DoesNotSendTurnComplete() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + // Send audio append + commit (sets _lastInputWasRealtime = true) + byte[] audioData = new byte[100]; + new Random(42).NextBytes(audioData); + string base64 = Convert.ToBase64String(audioData); + + await _session.SendAsync(new InputAudioBufferAppendRealtimeClientMessage( + new DataContent($"data:audio/pcm;base64,{base64}", "audio/pcm"))); + await _session.SendAsync(new InputAudioBufferCommitRealtimeClientMessage()); + + int countBeforeCreateResponse = sentMessages.Count; + + // CreateResponse should NOT send anything (audio commit already sent ActivityEnd) + await _session.SendAsync(new CreateResponseRealtimeClientMessage()); + + Assert.AreEqual(countBeforeCreateResponse, sentMessages.Count, + "CreateResponse should not send TurnComplete after realtime audio input"); + } + + [TestMethod] + public async Task SendAsync_CreateResponse_AfterToolResponse_DoesNotSendTurnComplete() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + // Send a function result (simulates middleware returning tool response) + var toolResultMsg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new FunctionResultContent("call-1", "sunny") }, + role: ChatRole.Tool)); + await _session.SendAsync(toolResultMsg); + + // CreateResponse should flush the tool response but NOT send TurnComplete + await _session.SendAsync(new CreateResponseRealtimeClientMessage()); + + // Should have exactly 1 message: the batched tool response. No TurnComplete. + Assert.AreEqual(1, sentMessages.Count, + "Should send tool response but NOT TurnComplete after function result"); + Assert.IsTrue(sentMessages[0].Contains("call-1"), + "Tool response should contain the call ID"); + Assert.IsFalse(sentMessages[0].Contains("turnComplete"), + "Should not contain turnComplete after tool response"); + } + + [TestMethod] + public async Task SendAsync_CreateResponse_BatchesMultipleToolResponses() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + // Simulate middleware sending separate CreateConversationItem per function result + var toolResult1 = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new FunctionResultContent("call-1", "sunny") }, + role: ChatRole.Tool)); + var toolResult2 = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new FunctionResultContent("call-2", "72F") }, + role: ChatRole.Tool)); + + await _session.SendAsync(toolResult1); + await _session.SendAsync(toolResult2); + + // No WebSocket sends yet — results are buffered + Assert.AreEqual(0, sentMessages.Count, + "Function results should be buffered until CreateResponse"); + + // CreateResponse flushes all results in a single batched SendToolResponseAsync + await _session.SendAsync(new CreateResponseRealtimeClientMessage()); + + Assert.AreEqual(1, sentMessages.Count, + "All function results should be sent in a single WebSocket message"); + Assert.IsTrue(sentMessages[0].Contains("call-1"), + "Batched tool response should contain first call ID"); + Assert.IsTrue(sentMessages[0].Contains("call-2"), + "Batched tool response should contain second call ID"); + Assert.IsFalse(sentMessages[0].Contains("turnComplete"), + "Should not contain turnComplete after batched tool responses"); + } + + [TestMethod] + public async Task SendAsync_CreateResponse_AfterToolResponse_NextTextSendResetsFlagCorrectly() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + // Complete a full tool call cycle: tool response → CreateResponse + var toolResultMsg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new FunctionResultContent("call-1", "sunny") }, + role: ChatRole.Tool)); + await _session.SendAsync(toolResultMsg); + await _session.SendAsync(new CreateResponseRealtimeClientMessage()); + + sentMessages.Clear(); + + // Now send normal text → CreateResponse. Text auto-triggers; CreateResponse is a no-op. + var textMsg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new TextContent("Hello again") }, + role: ChatRole.User)); + await _session.SendAsync(textMsg); + await _session.SendAsync(new CreateResponseRealtimeClientMessage()); + + // Should have 1 message: the text content sent via SendRealtimeInputAsync. + // CreateResponse does not send a turnComplete — text auto-triggers in Gemini. + Assert.AreEqual(1, sentMessages.Count, + "Normal text followed by CreateResponse should send only the text content"); + Assert.IsTrue(sentMessages[0].Contains("Hello again"), + "The sent message should contain the text content"); + } + + #endregion + + #region Additional MapServerMessage Tests + + [TestMethod] + public async Task GetStreamingResponseAsync_GenerationComplete_EmitsResponseDone() + { + SetupReceiveSequence(new[] + { + new LiveServerMessage + { + ServerContent = new LiveServerContent + { + ModelTurn = new Content + { + Parts = new List { new Part { Text = "Response" } } + } + } + }, + new LiveServerMessage + { + ServerContent = new LiveServerContent { GenerationComplete = true } + } + }); + + var messages = await CollectMessages(); + + Assert.AreEqual(3, messages.Count); + Assert.AreEqual(RealtimeServerMessageType.ResponseCreated, messages[0].Type); + Assert.AreEqual(RealtimeServerMessageType.OutputTextDelta, messages[1].Type); + Assert.AreEqual(RealtimeServerMessageType.ResponseDone, messages[2].Type); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_ToolCallCancellation_EmitsRawContentOnly() + { + SetupReceiveOnce(new LiveServerMessage + { + ToolCallCancellation = new LiveServerToolCallCancellation + { + Ids = new List { "call-1", "call-2" }, + } + }); + + var messages = await CollectMessages(); + + Assert.AreEqual(1, messages.Count); + Assert.AreEqual(RealtimeServerMessageType.RawContentOnly, messages[0].Type); + Assert.IsNotNull(messages[0].RawRepresentation); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_MultipleTextParts_EmitsMultipleDeltas() + { + SetupReceiveOnce(new LiveServerMessage + { + ServerContent = new LiveServerContent + { + ModelTurn = new Content + { + Parts = new List + { + new Part { Text = "Part one" }, + new Part { Text = "Part two" }, + } + } + } + }); + + var messages = await CollectMessages(); + + // ResponseCreated + 2 OutputTextDelta + Assert.AreEqual(3, messages.Count); + Assert.AreEqual(RealtimeServerMessageType.ResponseCreated, messages[0].Type); + Assert.AreEqual(RealtimeServerMessageType.OutputTextDelta, messages[1].Type); + Assert.AreEqual(RealtimeServerMessageType.OutputTextDelta, messages[2].Type); + Assert.AreEqual("Part one", ((OutputTextAudioRealtimeServerMessage)messages[1]).Text); + Assert.AreEqual("Part two", ((OutputTextAudioRealtimeServerMessage)messages[2]).Text); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_MixedAudioAndText_EmitsBothDeltas() + { + byte[] audioBytes = new byte[] { 10, 20, 30 }; + SetupReceiveOnce(new LiveServerMessage + { + ServerContent = new LiveServerContent + { + ModelTurn = new Content + { + Parts = new List + { + new Part + { + InlineData = new Blob + { + Data = audioBytes, + MimeType = "audio/pcm", + } + }, + new Part { Text = "Hello there" }, + } + } + } + }); + + var messages = await CollectMessages(); + + // ResponseCreated + OutputAudioDelta + OutputTextDelta + Assert.AreEqual(3, messages.Count); + Assert.AreEqual(RealtimeServerMessageType.ResponseCreated, messages[0].Type); + Assert.AreEqual(RealtimeServerMessageType.OutputAudioDelta, messages[1].Type); + Assert.AreEqual(RealtimeServerMessageType.OutputTextDelta, messages[2].Type); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_MultipleModelTurns_OnlyOneResponseCreated() + { + SetupReceiveSequence(new[] + { + new LiveServerMessage + { + ServerContent = new LiveServerContent + { + ModelTurn = new Content + { + Parts = new List { new Part { Text = "First chunk" } } + } + } + }, + new LiveServerMessage + { + ServerContent = new LiveServerContent + { + ModelTurn = new Content + { + Parts = new List { new Part { Text = "Second chunk" } } + } + } + }, + }); + + var messages = await CollectMessages(); + + // Only one ResponseCreated for the entire response cycle + var responseCreatedCount = messages.Count(m => m.Type == RealtimeServerMessageType.ResponseCreated); + Assert.AreEqual(1, responseCreatedCount, + "Should only emit one ResponseCreated across multiple ModelTurn messages in the same response"); + + // But both text deltas should appear + var textDeltas = messages.Where(m => m.Type == RealtimeServerMessageType.OutputTextDelta).ToList(); + Assert.AreEqual(2, textDeltas.Count); + Assert.AreEqual("First chunk", ((OutputTextAudioRealtimeServerMessage)textDeltas[0]).Text); + Assert.AreEqual("Second chunk", ((OutputTextAudioRealtimeServerMessage)textDeltas[1]).Text); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_MultipleToolCalls_EmitsAllItems() + { + SetupReceiveOnce(new LiveServerMessage + { + ToolCall = new LiveServerToolCall + { + FunctionCalls = new List + { + new FunctionCall { Id = "c1", Name = "fn1", Args = new Dictionary { ["a"] = "1" } }, + new FunctionCall { Id = "c2", Name = "fn2", Args = new Dictionary { ["b"] = "2" } }, + } + } + }); + + var messages = await CollectMessages(); + + // ResponseCreated + (ResponseOutputItemAdded + ResponseOutputItemDone) × 2 + Assert.AreEqual(5, messages.Count); + Assert.AreEqual(RealtimeServerMessageType.ResponseCreated, messages[0].Type); + Assert.AreEqual(RealtimeServerMessageType.ResponseOutputItemAdded, messages[1].Type); + Assert.AreEqual(RealtimeServerMessageType.ResponseOutputItemDone, messages[2].Type); + Assert.AreEqual(RealtimeServerMessageType.ResponseOutputItemAdded, messages[3].Type); + Assert.AreEqual(RealtimeServerMessageType.ResponseOutputItemDone, messages[4].Type); + + var fn1 = ((ResponseOutputItemRealtimeServerMessage)messages[1]).Item?.Contents?.OfType().First(); + var fn2 = ((ResponseOutputItemRealtimeServerMessage)messages[3]).Item?.Contents?.OfType().First(); + Assert.AreEqual("fn1", fn1?.Name); + Assert.AreEqual("fn2", fn2?.Name); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_ToolCallThenTurnComplete_EmitsResponseDone() + { + SetupReceiveSequence(new[] + { + new LiveServerMessage + { + ToolCall = new LiveServerToolCall + { + FunctionCalls = new List + { + new FunctionCall { Id = "c1", Name = "fn1" } + } + } + }, + new LiveServerMessage + { + ServerContent = new LiveServerContent { TurnComplete = true } + } + }); + + var messages = await CollectMessages(); + + // ResponseCreated + ItemAdded + ItemDone + ResponseDone + Assert.AreEqual(4, messages.Count); + Assert.AreEqual(RealtimeServerMessageType.ResponseDone, messages[3].Type); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_ToolCallWithNullArgs_EmitsEmptyArguments() + { + SetupReceiveOnce(new LiveServerMessage + { + ToolCall = new LiveServerToolCall + { + FunctionCalls = new List + { + new FunctionCall { Id = "c1", Name = "no_args_fn", Args = null } + } + } + }); + + var messages = await CollectMessages(); + + Assert.AreEqual(3, messages.Count); + var fc = ((ResponseOutputItemRealtimeServerMessage)messages[1]).Item?.Contents?.OfType().First(); + Assert.IsNotNull(fc); + Assert.AreEqual("no_args_fn", fc.Name); + Assert.IsNull(fc.Arguments); + } + + #endregion + + #region ConversationItemCreate Additional Tests + + [TestMethod] + public async Task SendAsync_ConversationItemCreate_UserRole_MapsToUser() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new TextContent("User text") }, + role: ChatRole.User)); + + await _session.SendAsync(msg); + + // Gemini's SendRealtimeInputAsync sends text only (no role field). + // Verify the text content was sent. + Assert.AreEqual(1, sentMessages.Count); + Assert.IsTrue(sentMessages[0].Contains("User text"), + "User text should be sent via SendRealtimeInputAsync"); + } + + [TestMethod] + public async Task SendAsync_ConversationItemCreate_NullRole_DefaultsToUser() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new TextContent("No role text") })); + + await _session.SendAsync(msg); + + // Gemini's SendRealtimeInputAsync sends text only (no role field). + // Verify the text content was sent regardless of the absence of a role. + Assert.AreEqual(1, sentMessages.Count); + Assert.IsTrue(sentMessages[0].Contains("No role text"), + "Text should be sent via SendRealtimeInputAsync even without a role"); + } + + [TestMethod] + public async Task SendAsync_ConversationItemCreate_MultipleFunctionResults_SendsAll() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + // Multiple function results should all be sent in a single SendToolResponseAsync call + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List + { + new FunctionResultContent("call-1", "result-one"), + new FunctionResultContent("call-2", "result-two"), + }, + role: ChatRole.Tool)); + + await _session.SendAsync(msg); + + // Function results are buffered until CreateResponse + Assert.AreEqual(0, sentMessages.Count, + "Function results should be buffered, not sent immediately"); + + // Flush via CreateResponse + await _session.SendAsync(new CreateResponseRealtimeClientMessage()); + + // All function results are batched into one WebSocket message + Assert.AreEqual(1, sentMessages.Count); + Assert.IsTrue(sentMessages[0].Contains("call-1")); + Assert.IsTrue(sentMessages[0].Contains("result-one")); + Assert.IsTrue(sentMessages[0].Contains("call-2")); + Assert.IsTrue(sentMessages[0].Contains("result-two")); + } + + [TestMethod] + public async Task SendAsync_ConversationItemCreate_NullContents_DoesNotSend() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List())); + + await _session.SendAsync(msg); + + Assert.AreEqual(0, sentMessages.Count); + } + + [TestMethod] + public async Task SendAsync_ConversationItemCreate_AudioContent_SendsInlineData() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + byte[] audioData = new byte[] { 1, 2, 3 }; + string base64 = Convert.ToBase64String(audioData); + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new DataContent($"data:audio/pcm;base64,{base64}", "audio/pcm") }, + role: ChatRole.User)); + + await _session.SendAsync(msg); + + Assert.AreEqual(1, sentMessages.Count); + Assert.IsTrue(sentMessages[0].Contains("audio/pcm")); + } + + [TestMethod] + public async Task SendAsync_ConversationItemCreate_ImageContent_SendsInlineData() + { + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + byte[] imageData = new byte[] { 0x89, 0x50, 0x4E, 0x47 }; + string base64 = Convert.ToBase64String(imageData); + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new DataContent($"data:image/png;base64,{base64}", "image/png") }, + role: ChatRole.User)); + + await _session.SendAsync(msg); + + Assert.AreEqual(1, sentMessages.Count); + Assert.IsTrue(sentMessages[0].Contains("image/png")); + } + + #endregion + + #region Audio Frame Content Verification Tests + + [TestMethod] + public async Task SendAsync_AudioCommit_FrameContentVerification_PreservesData() + { + // Use a known pattern so we can verify exact bytes + byte[] audioData = new byte[64_000]; // Exactly 2 frames + for (int i = 0; i < audioData.Length; i++) + { + audioData[i] = (byte)(i % 256); + } + + string base64 = Convert.ToBase64String(audioData); + var appendMsg = new InputAudioBufferAppendRealtimeClientMessage( + new DataContent($"data:audio/pcm;base64,{base64}", "audio/pcm")); + + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + await _session.SendAsync(appendMsg); + await _session.SendAsync(new InputAudioBufferCommitRealtimeClientMessage()); + + // ActivityStart + 2 audio frames + ActivityEnd = 4 messages + Assert.AreEqual(4, sentMessages.Count); + + // Verify the first message is ActivityStart + Assert.IsTrue(sentMessages[0].Contains("activityStart"), + "First message should be ActivityStart"); + + // Verify audio frames contain base64-encoded data + Assert.IsTrue(sentMessages[1].Contains("audio/pcm"), + "Audio frame should contain mime type"); + Assert.IsTrue(sentMessages[2].Contains("audio/pcm"), + "Audio frame should contain mime type"); + + // Verify the last message is ActivityEnd + Assert.IsTrue(sentMessages[3].Contains("activityEnd"), + "Last message should be ActivityEnd"); + } + + [TestMethod] + public async Task SendAsync_AudioCommit_ExactFrameSize_SingleFrame() + { + // Exactly 32000 bytes = exactly 1 frame (no splitting needed) + byte[] audioData = new byte[32_000]; + new Random(42).NextBytes(audioData); + + string base64 = Convert.ToBase64String(audioData); + var appendMsg = new InputAudioBufferAppendRealtimeClientMessage( + new DataContent($"data:audio/pcm;base64,{base64}", "audio/pcm")); + + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + await _session.SendAsync(appendMsg); + await _session.SendAsync(new InputAudioBufferCommitRealtimeClientMessage()); + + // ActivityStart + 1 frame + ActivityEnd = 3 + Assert.AreEqual(3, sentMessages.Count); + } + + [TestMethod] + public async Task SendAsync_AudioCommit_FrameBoundary_SplitsCorrectly() + { + // 32001 bytes = 32000 + 1 → 2 frames + byte[] audioData = new byte[32_001]; + new Random(42).NextBytes(audioData); + + string base64 = Convert.ToBase64String(audioData); + var appendMsg = new InputAudioBufferAppendRealtimeClientMessage( + new DataContent($"data:audio/pcm;base64,{base64}", "audio/pcm")); + + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + await _session.SendAsync(appendMsg); + await _session.SendAsync(new InputAudioBufferCommitRealtimeClientMessage()); + + // ActivityStart + 2 frames (32000 + 1) + ActivityEnd = 4 + Assert.AreEqual(4, sentMessages.Count); + } + + [TestMethod] + public async Task SendAsync_AudioAppend_MultipleAppends_PreservesOrder() + { + byte[] chunk1 = new byte[100]; + byte[] chunk2 = new byte[200]; + new Random(42).NextBytes(chunk1); + new Random(43).NextBytes(chunk2); + + string base64_1 = Convert.ToBase64String(chunk1); + string base64_2 = Convert.ToBase64String(chunk2); + + await _session.SendAsync(new InputAudioBufferAppendRealtimeClientMessage( + new DataContent($"data:audio/pcm;base64,{base64_1}", "audio/pcm"))); + await _session.SendAsync(new InputAudioBufferAppendRealtimeClientMessage( + new DataContent($"data:audio/pcm;base64,{base64_2}", "audio/pcm"))); + + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + await _session.SendAsync(new InputAudioBufferCommitRealtimeClientMessage()); + + // ActivityStart + 2 audio frames (both under 32KB) + ActivityEnd = 4 + Assert.AreEqual(4, sentMessages.Count); + } + + #endregion + + #region Audio MIME Type Tests + + [TestMethod] + public async Task SendAsync_AudioCommit_UsesDefaultMimeType_WhenNoInputAudioFormat() + { + // Default session (null options) should use audio/pcm + byte[] audioData = new byte[] { 1, 2, 3 }; + string base64 = Convert.ToBase64String(audioData); + + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + await _session.SendAsync(new InputAudioBufferAppendRealtimeClientMessage( + new DataContent($"data:audio/pcm;base64,{base64}", "audio/pcm"))); + await _session.SendAsync(new InputAudioBufferCommitRealtimeClientMessage()); + + // Verify audio frame uses default audio/pcm mime type + Assert.IsTrue(sentMessages[1].Contains("audio/pcm"), "Audio frame should use default audio/pcm"); + } + + [TestMethod] + public async Task SendAsync_AudioCommit_UsesCustomMimeType_WhenInputAudioFormatSet() + { + // Create session with custom audio format + var customOptions = new RealtimeSessionOptions + { + InputAudioFormat = new RealtimeAudioFormat("audio/opus", 48000), + }; + var customSession = new GoogleGenAIRealtimeSession(_asyncSession, "test-model", customOptions); + + byte[] audioData = new byte[] { 1, 2, 3 }; + string base64 = Convert.ToBase64String(audioData); + + var sentMessages = new List(); + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>( + (buffer, _, _, _) => + { + sentMessages.Add(Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count)); + }) + .Returns(Task.CompletedTask); + + await customSession.SendAsync(new InputAudioBufferAppendRealtimeClientMessage( + new DataContent($"data:audio/opus;base64,{base64}", "audio/opus"))); + await customSession.SendAsync(new InputAudioBufferCommitRealtimeClientMessage()); + + // Verify audio frame uses the custom mime type + Assert.IsTrue(sentMessages[1].Contains("audio/opus"), + "Audio frame should use the configured audio/opus mime type"); + Assert.IsFalse(sentMessages[1].Contains("audio/pcm"), + "Audio frame should NOT contain the default audio/pcm"); + } + + #endregion + + #region Dispose Race Safety Tests + + [TestMethod] + public async Task SendAsync_ConcurrentDispose_DoesNotThrow() + { + // Simulate a race: SendAsync acquires the lock, then DisposeAsync runs. + // The Release() in finally should not throw even if the semaphore is disposed. + byte[] audioData = new byte[] { 1, 2, 3 }; + string base64 = Convert.ToBase64String(audioData); + + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Returns(Task.CompletedTask); + + _mockWebSocket + .Setup(ws => ws.CloseAsync( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Task.CompletedTask); + + // Append audio, then commit and dispose concurrently + await _session.SendAsync(new InputAudioBufferAppendRealtimeClientMessage( + new DataContent($"data:audio/pcm;base64,{base64}", "audio/pcm"))); + await _session.SendAsync(new InputAudioBufferCommitRealtimeClientMessage()); + + // Dispose should not throw even after sends completed + await _session.DisposeAsync(); + + // Double dispose should also be safe + await _session.DisposeAsync(); + } + + #endregion + + #region DisposeAsync Additional Tests + + [TestMethod] + public async Task DisposeAsync_ClosesUnderlyingWebSocket() + { + _mockWebSocket + .Setup(ws => ws.CloseAsync( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Task.CompletedTask); + + await _session.DisposeAsync(); + + _mockWebSocket.Verify(ws => ws.CloseAsync( + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Once); + } + + #endregion + + #region Exception Handling Tests + + [TestMethod] + public async Task SendAsync_ObjectDisposedException_FromAsyncSession_Throws() + { + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .ThrowsAsync(new ObjectDisposedException("WebSocket")); + + // Send a conversation item that triggers a real send to the WebSocket + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new TextContent("Hello") }, + role: ChatRole.User)); + + // ObjectDisposedException is re-surfaced so callers know the session is gone + await Assert.ThrowsExceptionAsync( + () => _session.SendAsync(msg)); + } + + [TestMethod] + public async Task SendAsync_WebSocketException_FromAsyncSession_WhenNotDisposed_Throws() + { + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .ThrowsAsync(new WebSocketException("connection lost")); + + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new TextContent("Hello") }, + role: ChatRole.User)); + + // WebSocketException when session is NOT disposed indicates a real error + await Assert.ThrowsExceptionAsync( + () => _session.SendAsync(msg)); + } + + [TestMethod] + public async Task SendAsync_InternalOperationCancelled_Propagated() + { + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .ThrowsAsync(new OperationCanceledException("internal disposal")); + + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new TextContent("Hello") }, + role: ChatRole.User)); + + // Internal cancellation (not the caller's token) is now propagated + // so callers can observe unexpected teardown + await Assert.ThrowsExceptionAsync( + () => _session.SendAsync(msg)); + } + + [TestMethod] + public async Task SendAsync_CallerCancellation_Propagated() + { + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var msg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new TextContent("Hello") }, + role: ChatRole.User)); + + // Caller cancellation should propagate (ThrowIfCancellationRequested at top) + await Assert.ThrowsExceptionAsync( + () => _session.SendAsync(msg, cts.Token)); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_ObjectDisposedException_Swallowed() + { + _mockWebSocket + .Setup(ws => ws.ReceiveAsync( + It.IsAny>(), + It.IsAny())) + .ThrowsAsync(new ObjectDisposedException("WebSocket")); + + var messages = new List(); + await foreach (var msg in _session.GetStreamingResponseAsync()) + { + messages.Add(msg); + } + + Assert.AreEqual(0, messages.Count); + } + + [TestMethod] + public async Task GetStreamingResponseAsync_InternalOperationCancelled_Swallowed() + { + _mockWebSocket + .Setup(ws => ws.ReceiveAsync( + It.IsAny>(), + It.IsAny())) + .ThrowsAsync(new OperationCanceledException("internal cancellation")); + + var messages = new List(); + await foreach (var msg in _session.GetStreamingResponseAsync()) + { + messages.Add(msg); + } + + Assert.AreEqual(0, messages.Count); + } + + #endregion + + #region Helper Methods + + private void SetupReceiveOnce(LiveServerMessage message) + { + string json = JsonSerializer.Serialize(message, JsonConfig.JsonSerializerOptions); + byte[] bytes = Encoding.UTF8.GetBytes(json); + var callCount = 0; + + _mockWebSocket + .Setup(ws => ws.ReceiveAsync( + It.IsAny>(), + It.IsAny())) + .Callback, CancellationToken>((buffer, _) => + { + if (callCount == 0) + { + Buffer.BlockCopy(bytes, 0, buffer.Array!, buffer.Offset, bytes.Length); + } + }) + .Returns, CancellationToken>((_, _) => + { + callCount++; + if (callCount == 1) + { + return Task.FromResult( + new WebSocketReceiveResult(bytes.Length, WebSocketMessageType.Text, true)); + } + return Task.FromResult( + new WebSocketReceiveResult(0, WebSocketMessageType.Close, true, + WebSocketCloseStatus.NormalClosure, "done")); + }); + } + + private void SetupReceiveSequence(LiveServerMessage[] messages) + { + var serialized = messages.Select(m => + { + string json = JsonSerializer.Serialize(m, JsonConfig.JsonSerializerOptions); + return Encoding.UTF8.GetBytes(json); + }).ToList(); + + var callCount = 0; + + _mockWebSocket + .Setup(ws => ws.ReceiveAsync( + It.IsAny>(), + It.IsAny())) + .Callback, CancellationToken>((buffer, _) => + { + if (callCount < serialized.Count) + { + Buffer.BlockCopy(serialized[callCount], 0, buffer.Array!, buffer.Offset, + serialized[callCount].Length); + } + }) + .Returns, CancellationToken>((_, _) => + { + var idx = callCount; + callCount++; + if (idx < serialized.Count) + { + return Task.FromResult( + new WebSocketReceiveResult(serialized[idx].Length, WebSocketMessageType.Text, true)); + } + return Task.FromResult( + new WebSocketReceiveResult(0, WebSocketMessageType.Close, true, + WebSocketCloseStatus.NormalClosure, "done")); + }); + } + + private async Task> CollectMessages() + { + var messages = new List(); + await foreach (var msg in _session.GetStreamingResponseAsync()) + { + messages.Add(msg); + } + return messages; + } + + #endregion + + #region Concurrency Tests + + [TestMethod] + public async Task SendAsync_ConcurrentCalls_AreSerialized() + { + // Verify that concurrent SendAsync calls don't interleave WebSocket sends. + // We track the order of send completions to ensure no overlap. + var sendOrder = new List(); + var gate = new SemaphoreSlim(0); + + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Returns, WebSocketMessageType, bool, CancellationToken>( + async (buffer, _, _, ct) => + { + string json = Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count); + // The first call blocks briefly; if sends aren't serialized, the second + // would start before the first completes. + if (json.Contains("turn-1")) + { + lock (sendOrder) { sendOrder.Add(1); } + gate.Release(); + await Task.Delay(50, ct); + lock (sendOrder) { sendOrder.Add(-1); } + } + else if (json.Contains("turn-2")) + { + await gate.WaitAsync(ct); + lock (sendOrder) { sendOrder.Add(2); } + } + }); + + var msg1 = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new TextContent("turn-1") }, role: ChatRole.User)); + var msg2 = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new TextContent("turn-2") }, role: ChatRole.User)); + + // Launch both sends concurrently + var t1 = _session.SendAsync(msg1); + var t2 = _session.SendAsync(msg2); + await Task.WhenAll(t1, t2); + + // Because sends are serialized, turn-2 can only start after turn-1 completes. + // Expected order: [1, -1, 2] (start-1, end-1, start-2) + Assert.AreEqual(3, sendOrder.Count); + Assert.AreEqual(1, sendOrder[0], "turn-1 should start first"); + Assert.AreEqual(-1, sendOrder[1], "turn-1 should complete before turn-2 starts"); + Assert.AreEqual(2, sendOrder[2], "turn-2 should start after turn-1 completes"); + } + + [TestMethod] + public async Task SendAsync_AudioAppend_DoesNotBlockConcurrentSends() + { + // AudioAppend only buffers — it should NOT acquire the send lock, + // so it completes even while another send holds the lock. + var sendStarted = new TaskCompletionSource(); + var sendCanProceed = new TaskCompletionSource(); + + _mockWebSocket + .Setup(ws => ws.SendAsync( + It.IsAny>(), + WebSocketMessageType.Text, + true, + It.IsAny())) + .Returns, WebSocketMessageType, bool, CancellationToken>( + async (_, _, _, ct) => + { + sendStarted.SetResult(true); + await sendCanProceed.Task; + }); + + // Start a text send that will hold the lock + var textMsg = new CreateConversationItemRealtimeClientMessage(new RealtimeConversationItem( + new List { new TextContent("hello") }, role: ChatRole.User)); + var textSend = _session.SendAsync(textMsg); + + // Wait until the text send has acquired the lock and is in-flight + await sendStarted.Task; + + // AudioAppend should complete immediately (no lock contention) + var audioContent = new DataContent("data:audio/pcm;base64,AQID", "audio/pcm"); + var audioAppend = new InputAudioBufferAppendRealtimeClientMessage(audioContent); + var appendTask = _session.SendAsync(audioAppend); + + // Append should complete quickly even though the text send holds the lock + var completed = await Task.WhenAny(appendTask, Task.Delay(1000)); + Assert.AreSame(appendTask, completed, "AudioAppend should not block on the send lock"); + + // Release the text send + sendCanProceed.SetResult(true); + await textSend; + } + + #endregion + + #region Tool Payload Normalization Tests + + [TestMethod] + public void NormalizeToolPayload_ByteArray_EncodesAsBase64() + { + byte[] payload = [1, 2, 3, 4]; + var result = GoogleGenAIRealtimeSession.NormalizeToolPayload(payload); + Assert.AreEqual(Convert.ToBase64String(payload), result); + } + + [TestMethod] + public void NormalizeToolPayload_JsonElement_DecomposesCorrectly() + { + var json = JsonSerializer.SerializeToElement(new { name = "test", count = 42 }); + var result = GoogleGenAIRealtimeSession.NormalizeToolPayload(json) as Dictionary; + Assert.IsNotNull(result); + Assert.AreEqual("test", result["name"]); + // JSON numbers may deserialize as int64 or double depending on the runtime + Assert.IsTrue( + result["count"] is 42L or 42.0, + $"Expected 42 as long or double, got {result["count"]} ({result["count"]?.GetType()})"); + } + + [TestMethod] + public void NormalizeToolPayload_TooDeep_Throws() + { + var payload = new Dictionary(); + IDictionary current = payload; + for (int i = 0; i < 80; i++) + { + var next = new Dictionary(); + current[$"level{i}"] = next; + current = next; + } + + Assert.ThrowsException( + () => GoogleGenAIRealtimeSession.NormalizeToolPayload(payload)); + } + + [TestMethod] + public void NormalizeToolArguments_NormalizesNestedJsonElements() + { + var jsonElement = JsonSerializer.SerializeToElement("hello"); + var args = new Dictionary { ["key"] = jsonElement }; + var result = GoogleGenAIRealtimeSession.NormalizeToolArguments(args); + Assert.AreEqual("hello", result["key"]); + } + + #endregion + + #region Concurrent Enumeration Guard Tests + + [TestMethod] + public async Task GetStreamingResponseAsync_ConcurrentEnumeration_Throws() + { + // Set up WebSocket to block on receive so the first enumeration stays active + var receiveGate = new TaskCompletionSource(); + _mockWebSocket + .Setup(ws => ws.ReceiveAsync( + It.IsAny>(), + It.IsAny())) + .Returns(receiveGate.Task); + + // Start first enumeration + using var cts = new CancellationTokenSource(); + var enumerator = _session.GetStreamingResponseAsync(cts.Token).GetAsyncEnumerator(cts.Token); + var firstMoveNext = enumerator.MoveNextAsync(); + + // Attempt second concurrent enumeration — should throw + await Assert.ThrowsExceptionAsync(async () => + { + await foreach (var _ in _session.GetStreamingResponseAsync()) + { + } + }); + + // Clean up + cts.Cancel(); + receiveGate.SetCanceled(); + try { await firstMoveNext; } catch { } + await enumerator.DisposeAsync(); + } + + #endregion +} + +#pragma warning restore MEAI001 diff --git a/Google.GenAI.Tests/Netstandard2_0Tests/packages.lock.json b/Google.GenAI.Tests/Netstandard2_0Tests/packages.lock.json index a23b4a43..5e2a83f9 100644 --- a/Google.GenAI.Tests/Netstandard2_0Tests/packages.lock.json +++ b/Google.GenAI.Tests/Netstandard2_0Tests/packages.lock.json @@ -1,298 +1,299 @@ -{ - "version": 2, - "dependencies": { - "net8.0": { - "coverlet.collector": { - "type": "Direct", - "requested": "[6.0.4, )", - "resolved": "6.0.4", - "contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg==" - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Direct", - "requested": "[10.0.3, )", - "resolved": "10.0.3", - "contentHash": "TV62UsrJZPX6gbt3c4WrtXh7bmaDIcMqf9uft1cc4L6gJXOU07hDGEh+bFQh/L2Az0R1WVOkiT66lFqS6G2NmA==" - }, - "Microsoft.NET.Test.Sdk": { - "type": "Direct", - "requested": "[17.10.0, )", - "resolved": "17.10.0", - "contentHash": "0/2HeACkaHEYU3wc83YlcD2Fi4LMtECJjqrtvw0lPi9DCEa35zSPt1j4fuvM8NagjDqJuh1Ja35WcRtn1Um6/A==", - "dependencies": { - "Microsoft.CodeCoverage": "17.10.0", - "Microsoft.TestPlatform.TestHost": "17.10.0" - } - }, - "Microsoft.Testing.Extensions.CodeCoverage": { - "type": "Direct", - "requested": "[17.10.1, )", - "resolved": "17.10.1", - "contentHash": "EdPqUfB4GShYeXiivrjWrTAjZz93tmrF313QlyK/CI4afdBAcNCrJ2IqEWAQ1n+05sc+tEwZnyaZAdStnwQqcw==", - "dependencies": { - "Microsoft.DiaSymReader": "2.0.0", - "Microsoft.Extensions.DependencyModel": "6.0.0", - "Microsoft.Testing.Platform": "1.0.0", - "Mono.Cecil": "0.11.5", - "System.Reflection.Metadata": "6.0.1" - } - }, - "Moq": { - "type": "Direct", - "requested": "[4.20.70, )", - "resolved": "4.20.70", - "contentHash": "4rNnAwdpXJBuxqrOCzCyICXHSImOTRktCgCWXWykuF1qwoIsVvEnR7PjbMk/eLOxWvhmj5Kwt+kDV3RGUYcNwg==", - "dependencies": { - "Castle.Core": "5.1.1" - } - }, - "MSTest.TestAdapter": { - "type": "Direct", - "requested": "[3.4.3, )", - "resolved": "3.4.3", - "contentHash": "5ul31wYr17590gDumPxWMiBLPREfPF/ggtdPGfaKoYSsO0EW6H1GWY+7xnVCKa2SB4I/dSEZLDYSwRLDjA0LEQ==", - "dependencies": { - "Microsoft.Testing.Extensions.VSTestBridge": "1.2.1", - "Microsoft.Testing.Platform.MSBuild": "1.2.1" - } - }, - "MSTest.TestFramework": { - "type": "Direct", - "requested": "[3.4.3, )", - "resolved": "3.4.3", - "contentHash": "hu7F0PyRe47LScY2SCjRFIzP2QYxq1oeHMAIdao9onUm5WhobO9tfZrFAAkJ4v+66EQjilloEbA4kspVHCZpTg==" - }, - "System.Text.Json": { - "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", - "dependencies": { - "System.IO.Pipelines": "10.0.4", - "System.Text.Encodings.Web": "10.0.4" - } - }, - "Castle.Core": { - "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", - "dependencies": { - "System.Diagnostics.EventLog": "6.0.0" - } - }, - "Google.Apis": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", - "dependencies": { - "Google.Apis.Core": "1.69.0" - } - }, - "Google.Apis.Core": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", - "dependencies": { - "Newtonsoft.Json": "13.0.3" - } - }, - "Microsoft.ApplicationInsights": { - "type": "Transitive", - "resolved": "2.22.0", - "contentHash": "3AOM9bZtku7RQwHyMEY3tQMrHIgjcfRDa6YQpd/QG2LDGvMydSlL9Di+8LLMt7J2RDdfJ7/2jdYv6yHcMJAnNw==", - "dependencies": { - "System.Diagnostics.DiagnosticSource": "5.0.0" - } - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "yC7oSlnR54XO5kOuHlVOKtxomNNN1BWXX8lK1G2jaPXT9sUok7kCOoA4Pgs0qyFaCtMrNsprztYMeoEGqCm4uA==" - }, - "Microsoft.DiaSymReader": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "TD5QHg98m3+QhgEV1YVoNMl5KtBw/4rjfxLHO0e/YV9bPUBDKntApP4xdrVtGgCeQZHVfC2EXIGsdpRNrr87Pg==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Memory": "4.5.4", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encodings.Web": "6.0.0", - "System.Text.Json": "6.0.0" - } - }, - "Microsoft.Testing.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "MKGxwQhDDEoTS/ntFb21Z6Bxh9VvknmSLgEWH+NFD86fbcIqE2Al8lrXkQPeH+AqCvlhx2WnPLKd81T2PXc2dw==", - "dependencies": { - "Microsoft.ApplicationInsights": "2.22.0", - "Microsoft.Testing.Platform": "1.2.1" - } - }, - "Microsoft.Testing.Extensions.TrxReport.Abstractions": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "46SnzaLR+SDaTtBWy49xdFm/rI40I8nZtziqnt2d4lgILKovWPnkM8Pehnga/uwl+OznVIh0XuRsN3NokkX1TQ==", - "dependencies": { - "Microsoft.Testing.Platform": "1.2.1" - } - }, - "Microsoft.Testing.Extensions.VSTestBridge": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "Tu8CWHEwV/92WM2DRr/qeIdH243diV5s43ODPLl13XeRqGbZlu9lk7X0a7kcxhp0BLRlA3fqMW3F6RynrnDrPw==", - "dependencies": { - "Microsoft.ApplicationInsights": "2.22.0", - "Microsoft.TestPlatform.ObjectModel": "17.5.0", - "Microsoft.Testing.Extensions.Telemetry": "1.2.1", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.2.1", - "Microsoft.Testing.Platform": "1.2.1" - } - }, - "Microsoft.Testing.Platform": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "mb7irPwqjgusJ05BxuQ5KP6uofWaoDr/dfjFNItX1Q1Ntv3EDMr3CeLInrlU2PNcPwwObw4X6bZG7wJvvFjKZQ==" - }, - "Microsoft.Testing.Platform.MSBuild": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "leUhW4iQNy7vmPk5uRHd4OROqfRtugWDQkWL/4AD17gxZwAAwGCaTcrqG0YVPi7uuZ+lj2Loa6kU7hBLA/v5+w==", - "dependencies": { - "Microsoft.Testing.Platform": "1.2.1" - } - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "KkwhjQevuDj0aBRoPLY6OLAhGqbPUEBuKLbaCs0kUVw29qiOYncdORd4mLVJbn9vGZ7/iFGQ/+AoJl0Tu5Umdg==", - "dependencies": { - "System.Reflection.Metadata": "1.6.0" - } - }, - "Microsoft.TestPlatform.TestHost": { - "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "LWpMdfqhHvcUkeMCvNYJO8QlPLlYz9XPPb+ZbaXIKhdmjAV0wqTSrTiW5FLaf7RRZT50AQADDOYMOe0HxDxNgA==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "17.10.0", - "Newtonsoft.Json": "13.0.1" - } - }, - "Mono.Cecil": { - "type": "Transitive", - "resolved": "0.11.5", - "contentHash": "fxfX+0JGTZ8YQeu1MYjbBiK2CYTSzDyEeIixt+yqKKTn7FW8rv7JMY70qevup4ZJfD7Kk/VG/jDzQQTpfch87g==" - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "tCQTzPsGZh/A9LhhA6zrqCRV4hOHsK90/G7q3Khxmn6tnB1PuNU0cRaKANP2AWcF9bn0zsuOoZOSrHuJk6oNBA==" - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" - }, - "System.Management": { - "type": "Transitive", - "resolved": "7.0.2", - "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", - "dependencies": { - "System.CodeDom": "7.0.0" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "III/lNMSn0ZRBuM9m5Cgbiho5j81u0FAEagFX5ta2DKbljZ3T0IpD8j+BIiHQPeKqJppWS9bGEp6JnKnWKze0g==", - "dependencies": { - "System.Collections.Immutable": "6.0.0" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" - }, - "google.genai": { - "type": "Project", - "dependencies": { - "Google.Apis.Auth": "[1.69.0, )", - "Microsoft.Extensions.AI.Abstractions": "[10.4.0, )", - "MimeTypes": "[2.5.2, )" - } - }, - "Google.Apis.Auth": { - "type": "CentralTransitive", - "requested": "[1.69.0, )", - "resolved": "1.69.0", - "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", - "dependencies": { - "Google.Apis": "1.69.0", - "Google.Apis.Core": "1.69.0", - "System.Management": "7.0.2" - } - }, - "Microsoft.Extensions.AI.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "t3S2H4do4YeNheIfE3GEl3MnKIrnxpbLu7a88spfApYR3in9ddhIq/GMtxgMaFjn/PUMTCFv5YH7Y6Q91dsDXQ==", - "dependencies": { - "System.Text.Json": "10.0.4" - } - }, - "MimeTypes": { - "type": "CentralTransitive", - "requested": "[2.5.2, )", - "resolved": "2.5.2", - "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" - } - } - } +{ + "version": 2, + "dependencies": { + "net8.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[6.0.4, )", + "resolved": "6.0.4", + "contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Direct", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "TV62UsrJZPX6gbt3c4WrtXh7bmaDIcMqf9uft1cc4L6gJXOU07hDGEh+bFQh/L2Az0R1WVOkiT66lFqS6G2NmA==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.10.0, )", + "resolved": "17.10.0", + "contentHash": "0/2HeACkaHEYU3wc83YlcD2Fi4LMtECJjqrtvw0lPi9DCEa35zSPt1j4fuvM8NagjDqJuh1Ja35WcRtn1Um6/A==", + "dependencies": { + "Microsoft.CodeCoverage": "17.10.0", + "Microsoft.TestPlatform.TestHost": "17.10.0" + } + }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Direct", + "requested": "[17.10.1, )", + "resolved": "17.10.1", + "contentHash": "EdPqUfB4GShYeXiivrjWrTAjZz93tmrF313QlyK/CI4afdBAcNCrJ2IqEWAQ1n+05sc+tEwZnyaZAdStnwQqcw==", + "dependencies": { + "Microsoft.DiaSymReader": "2.0.0", + "Microsoft.Extensions.DependencyModel": "6.0.0", + "Microsoft.Testing.Platform": "1.0.0", + "Mono.Cecil": "0.11.5", + "System.Reflection.Metadata": "6.0.1" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.20.70, )", + "resolved": "4.20.70", + "contentHash": "4rNnAwdpXJBuxqrOCzCyICXHSImOTRktCgCWXWykuF1qwoIsVvEnR7PjbMk/eLOxWvhmj5Kwt+kDV3RGUYcNwg==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "MSTest.TestAdapter": { + "type": "Direct", + "requested": "[3.4.3, )", + "resolved": "3.4.3", + "contentHash": "5ul31wYr17590gDumPxWMiBLPREfPF/ggtdPGfaKoYSsO0EW6H1GWY+7xnVCKa2SB4I/dSEZLDYSwRLDjA0LEQ==", + "dependencies": { + "Microsoft.Testing.Extensions.VSTestBridge": "1.2.1", + "Microsoft.Testing.Platform.MSBuild": "1.2.1" + } + }, + "MSTest.TestFramework": { + "type": "Direct", + "requested": "[3.4.3, )", + "resolved": "3.4.3", + "contentHash": "hu7F0PyRe47LScY2SCjRFIzP2QYxq1oeHMAIdao9onUm5WhobO9tfZrFAAkJ4v+66EQjilloEbA4kspVHCZpTg==" + }, + "System.Text.Json": { + "type": "Direct", + "requested": "[10.0.4, )", + "resolved": "10.0.4", + "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", + "dependencies": { + "System.IO.Pipelines": "10.0.4", + "System.Text.Encodings.Web": "10.0.4" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "Google.Apis": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", + "dependencies": { + "Google.Apis.Core": "1.69.0" + } + }, + "Google.Apis.Core": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.22.0", + "contentHash": "3AOM9bZtku7RQwHyMEY3tQMrHIgjcfRDa6YQpd/QG2LDGvMydSlL9Di+8LLMt7J2RDdfJ7/2jdYv6yHcMJAnNw==", + "dependencies": { + "System.Diagnostics.DiagnosticSource": "5.0.0" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.10.0", + "contentHash": "yC7oSlnR54XO5kOuHlVOKtxomNNN1BWXX8lK1G2jaPXT9sUok7kCOoA4Pgs0qyFaCtMrNsprztYMeoEGqCm4uA==" + }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "TD5QHg98m3+QhgEV1YVoNMl5KtBw/4rjfxLHO0e/YV9bPUBDKntApP4xdrVtGgCeQZHVfC2EXIGsdpRNrr87Pg==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.4", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "6.0.0", + "System.Text.Json": "6.0.0" + } + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "MKGxwQhDDEoTS/ntFb21Z6Bxh9VvknmSLgEWH+NFD86fbcIqE2Al8lrXkQPeH+AqCvlhx2WnPLKd81T2PXc2dw==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.22.0", + "Microsoft.Testing.Platform": "1.2.1" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "46SnzaLR+SDaTtBWy49xdFm/rI40I8nZtziqnt2d4lgILKovWPnkM8Pehnga/uwl+OznVIh0XuRsN3NokkX1TQ==", + "dependencies": { + "Microsoft.Testing.Platform": "1.2.1" + } + }, + "Microsoft.Testing.Extensions.VSTestBridge": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "Tu8CWHEwV/92WM2DRr/qeIdH243diV5s43ODPLl13XeRqGbZlu9lk7X0a7kcxhp0BLRlA3fqMW3F6RynrnDrPw==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.22.0", + "Microsoft.TestPlatform.ObjectModel": "17.5.0", + "Microsoft.Testing.Extensions.Telemetry": "1.2.1", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.2.1", + "Microsoft.Testing.Platform": "1.2.1" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "mb7irPwqjgusJ05BxuQ5KP6uofWaoDr/dfjFNItX1Q1Ntv3EDMr3CeLInrlU2PNcPwwObw4X6bZG7wJvvFjKZQ==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "leUhW4iQNy7vmPk5uRHd4OROqfRtugWDQkWL/4AD17gxZwAAwGCaTcrqG0YVPi7uuZ+lj2Loa6kU7hBLA/v5+w==", + "dependencies": { + "Microsoft.Testing.Platform": "1.2.1" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.10.0", + "contentHash": "KkwhjQevuDj0aBRoPLY6OLAhGqbPUEBuKLbaCs0kUVw29qiOYncdORd4mLVJbn9vGZ7/iFGQ/+AoJl0Tu5Umdg==", + "dependencies": { + "System.Reflection.Metadata": "1.6.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.10.0", + "contentHash": "LWpMdfqhHvcUkeMCvNYJO8QlPLlYz9XPPb+ZbaXIKhdmjAV0wqTSrTiW5FLaf7RRZT50AQADDOYMOe0HxDxNgA==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.10.0", + "Newtonsoft.Json": "13.0.1" + } + }, + "Mono.Cecil": { + "type": "Transitive", + "resolved": "0.11.5", + "contentHash": "fxfX+0JGTZ8YQeu1MYjbBiK2CYTSzDyEeIixt+yqKKTn7FW8rv7JMY70qevup4ZJfD7Kk/VG/jDzQQTpfch87g==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "tCQTzPsGZh/A9LhhA6zrqCRV4hOHsK90/G7q3Khxmn6tnB1PuNU0cRaKANP2AWcF9bn0zsuOoZOSrHuJk6oNBA==" + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", + "dependencies": { + "System.CodeDom": "7.0.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "III/lNMSn0ZRBuM9m5Cgbiho5j81u0FAEagFX5ta2DKbljZ3T0IpD8j+BIiHQPeKqJppWS9bGEp6JnKnWKze0g==", + "dependencies": { + "System.Collections.Immutable": "6.0.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" + }, + "google.genai": { + "type": "Project", + "dependencies": { + "Google.Apis.Auth": "[1.69.0, )", + "Microsoft.Extensions.AI.Abstractions": "[10.4.1, )", + "MimeTypes": "[2.5.2, )", + "System.Text.Json": "[10.0.4, )" + } + }, + "Google.Apis.Auth": { + "type": "CentralTransitive", + "requested": "[1.69.0, )", + "resolved": "1.69.0", + "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", + "dependencies": { + "Google.Apis": "1.69.0", + "Google.Apis.Core": "1.69.0", + "System.Management": "7.0.2" + } + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.4.1, )", + "resolved": "10.4.1", + "contentHash": "ZxhU/wg9BOc3ohibhLl18toPLWm96ysQoE+3OhCgrZ0TUPZd7bsUmGteeatz08yweyuPIEhtyUzEZTF+3bMWEQ==", + "dependencies": { + "System.Text.Json": "10.0.4" + } + }, + "MimeTypes": { + "type": "CentralTransitive", + "requested": "[2.5.2, )", + "resolved": "2.5.2", + "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" + } + } + } } \ No newline at end of file diff --git a/Google.GenAI.Tests/packages.lock.json b/Google.GenAI.Tests/packages.lock.json index 36019bfe..a81a9052 100644 --- a/Google.GenAI.Tests/packages.lock.json +++ b/Google.GenAI.Tests/packages.lock.json @@ -1,292 +1,293 @@ -{ - "version": 2, - "dependencies": { - "net8.0": { - "coverlet.collector": { - "type": "Direct", - "requested": "[6.0.4, )", - "resolved": "6.0.4", - "contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg==" - }, - "Microsoft.NET.Test.Sdk": { - "type": "Direct", - "requested": "[17.10.0, )", - "resolved": "17.10.0", - "contentHash": "0/2HeACkaHEYU3wc83YlcD2Fi4LMtECJjqrtvw0lPi9DCEa35zSPt1j4fuvM8NagjDqJuh1Ja35WcRtn1Um6/A==", - "dependencies": { - "Microsoft.CodeCoverage": "17.10.0", - "Microsoft.TestPlatform.TestHost": "17.10.0" - } - }, - "Microsoft.Testing.Extensions.CodeCoverage": { - "type": "Direct", - "requested": "[17.10.1, )", - "resolved": "17.10.1", - "contentHash": "EdPqUfB4GShYeXiivrjWrTAjZz93tmrF313QlyK/CI4afdBAcNCrJ2IqEWAQ1n+05sc+tEwZnyaZAdStnwQqcw==", - "dependencies": { - "Microsoft.DiaSymReader": "2.0.0", - "Microsoft.Extensions.DependencyModel": "6.0.0", - "Microsoft.Testing.Platform": "1.0.0", - "Mono.Cecil": "0.11.5", - "System.Reflection.Metadata": "6.0.1" - } - }, - "Moq": { - "type": "Direct", - "requested": "[4.20.70, )", - "resolved": "4.20.70", - "contentHash": "4rNnAwdpXJBuxqrOCzCyICXHSImOTRktCgCWXWykuF1qwoIsVvEnR7PjbMk/eLOxWvhmj5Kwt+kDV3RGUYcNwg==", - "dependencies": { - "Castle.Core": "5.1.1" - } - }, - "MSTest.TestAdapter": { - "type": "Direct", - "requested": "[3.4.3, )", - "resolved": "3.4.3", - "contentHash": "5ul31wYr17590gDumPxWMiBLPREfPF/ggtdPGfaKoYSsO0EW6H1GWY+7xnVCKa2SB4I/dSEZLDYSwRLDjA0LEQ==", - "dependencies": { - "Microsoft.Testing.Extensions.VSTestBridge": "1.2.1", - "Microsoft.Testing.Platform.MSBuild": "1.2.1" - } - }, - "MSTest.TestFramework": { - "type": "Direct", - "requested": "[3.4.3, )", - "resolved": "3.4.3", - "contentHash": "hu7F0PyRe47LScY2SCjRFIzP2QYxq1oeHMAIdao9onUm5WhobO9tfZrFAAkJ4v+66EQjilloEbA4kspVHCZpTg==" - }, - "Castle.Core": { - "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", - "dependencies": { - "System.Diagnostics.EventLog": "6.0.0" - } - }, - "Google.Apis": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", - "dependencies": { - "Google.Apis.Core": "1.69.0" - } - }, - "Google.Apis.Core": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", - "dependencies": { - "Newtonsoft.Json": "13.0.3" - } - }, - "Microsoft.ApplicationInsights": { - "type": "Transitive", - "resolved": "2.22.0", - "contentHash": "3AOM9bZtku7RQwHyMEY3tQMrHIgjcfRDa6YQpd/QG2LDGvMydSlL9Di+8LLMt7J2RDdfJ7/2jdYv6yHcMJAnNw==", - "dependencies": { - "System.Diagnostics.DiagnosticSource": "5.0.0" - } - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "yC7oSlnR54XO5kOuHlVOKtxomNNN1BWXX8lK1G2jaPXT9sUok7kCOoA4Pgs0qyFaCtMrNsprztYMeoEGqCm4uA==" - }, - "Microsoft.DiaSymReader": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "TD5QHg98m3+QhgEV1YVoNMl5KtBw/4rjfxLHO0e/YV9bPUBDKntApP4xdrVtGgCeQZHVfC2EXIGsdpRNrr87Pg==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Memory": "4.5.4", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encodings.Web": "6.0.0", - "System.Text.Json": "6.0.0" - } - }, - "Microsoft.Testing.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "MKGxwQhDDEoTS/ntFb21Z6Bxh9VvknmSLgEWH+NFD86fbcIqE2Al8lrXkQPeH+AqCvlhx2WnPLKd81T2PXc2dw==", - "dependencies": { - "Microsoft.ApplicationInsights": "2.22.0", - "Microsoft.Testing.Platform": "1.2.1" - } - }, - "Microsoft.Testing.Extensions.TrxReport.Abstractions": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "46SnzaLR+SDaTtBWy49xdFm/rI40I8nZtziqnt2d4lgILKovWPnkM8Pehnga/uwl+OznVIh0XuRsN3NokkX1TQ==", - "dependencies": { - "Microsoft.Testing.Platform": "1.2.1" - } - }, - "Microsoft.Testing.Extensions.VSTestBridge": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "Tu8CWHEwV/92WM2DRr/qeIdH243diV5s43ODPLl13XeRqGbZlu9lk7X0a7kcxhp0BLRlA3fqMW3F6RynrnDrPw==", - "dependencies": { - "Microsoft.ApplicationInsights": "2.22.0", - "Microsoft.TestPlatform.ObjectModel": "17.5.0", - "Microsoft.Testing.Extensions.Telemetry": "1.2.1", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.2.1", - "Microsoft.Testing.Platform": "1.2.1" - } - }, - "Microsoft.Testing.Platform": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "mb7irPwqjgusJ05BxuQ5KP6uofWaoDr/dfjFNItX1Q1Ntv3EDMr3CeLInrlU2PNcPwwObw4X6bZG7wJvvFjKZQ==" - }, - "Microsoft.Testing.Platform.MSBuild": { - "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "leUhW4iQNy7vmPk5uRHd4OROqfRtugWDQkWL/4AD17gxZwAAwGCaTcrqG0YVPi7uuZ+lj2Loa6kU7hBLA/v5+w==", - "dependencies": { - "Microsoft.Testing.Platform": "1.2.1" - } - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "KkwhjQevuDj0aBRoPLY6OLAhGqbPUEBuKLbaCs0kUVw29qiOYncdORd4mLVJbn9vGZ7/iFGQ/+AoJl0Tu5Umdg==", - "dependencies": { - "System.Reflection.Metadata": "1.6.0" - } - }, - "Microsoft.TestPlatform.TestHost": { - "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "LWpMdfqhHvcUkeMCvNYJO8QlPLlYz9XPPb+ZbaXIKhdmjAV0wqTSrTiW5FLaf7RRZT50AQADDOYMOe0HxDxNgA==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "17.10.0", - "Newtonsoft.Json": "13.0.1" - } - }, - "Mono.Cecil": { - "type": "Transitive", - "resolved": "0.11.5", - "contentHash": "fxfX+0JGTZ8YQeu1MYjbBiK2CYTSzDyEeIixt+yqKKTn7FW8rv7JMY70qevup4ZJfD7Kk/VG/jDzQQTpfch87g==" - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "tCQTzPsGZh/A9LhhA6zrqCRV4hOHsK90/G7q3Khxmn6tnB1PuNU0cRaKANP2AWcF9bn0zsuOoZOSrHuJk6oNBA==" - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" - }, - "System.Management": { - "type": "Transitive", - "resolved": "7.0.2", - "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", - "dependencies": { - "System.CodeDom": "7.0.0" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "III/lNMSn0ZRBuM9m5Cgbiho5j81u0FAEagFX5ta2DKbljZ3T0IpD8j+BIiHQPeKqJppWS9bGEp6JnKnWKze0g==", - "dependencies": { - "System.Collections.Immutable": "6.0.0" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" - }, - "google.genai": { - "type": "Project", - "dependencies": { - "Google.Apis.Auth": "[1.69.0, )", - "Microsoft.Extensions.AI.Abstractions": "[10.4.0, )", - "MimeTypes": "[2.5.2, )" - } - }, - "Google.Apis.Auth": { - "type": "CentralTransitive", - "requested": "[1.69.0, )", - "resolved": "1.69.0", - "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", - "dependencies": { - "Google.Apis": "1.69.0", - "Google.Apis.Core": "1.69.0", - "System.Management": "7.0.2" - } - }, - "Microsoft.Extensions.AI.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "t3S2H4do4YeNheIfE3GEl3MnKIrnxpbLu7a88spfApYR3in9ddhIq/GMtxgMaFjn/PUMTCFv5YH7Y6Q91dsDXQ==", - "dependencies": { - "System.Text.Json": "10.0.4" - } - }, - "MimeTypes": { - "type": "CentralTransitive", - "requested": "[2.5.2, )", - "resolved": "2.5.2", - "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" - }, - "System.Text.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", - "dependencies": { - "System.IO.Pipelines": "10.0.4", - "System.Text.Encodings.Web": "10.0.4" - } - } - } - } +{ + "version": 2, + "dependencies": { + "net8.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[6.0.4, )", + "resolved": "6.0.4", + "contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.10.0, )", + "resolved": "17.10.0", + "contentHash": "0/2HeACkaHEYU3wc83YlcD2Fi4LMtECJjqrtvw0lPi9DCEa35zSPt1j4fuvM8NagjDqJuh1Ja35WcRtn1Um6/A==", + "dependencies": { + "Microsoft.CodeCoverage": "17.10.0", + "Microsoft.TestPlatform.TestHost": "17.10.0" + } + }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Direct", + "requested": "[17.10.1, )", + "resolved": "17.10.1", + "contentHash": "EdPqUfB4GShYeXiivrjWrTAjZz93tmrF313QlyK/CI4afdBAcNCrJ2IqEWAQ1n+05sc+tEwZnyaZAdStnwQqcw==", + "dependencies": { + "Microsoft.DiaSymReader": "2.0.0", + "Microsoft.Extensions.DependencyModel": "6.0.0", + "Microsoft.Testing.Platform": "1.0.0", + "Mono.Cecil": "0.11.5", + "System.Reflection.Metadata": "6.0.1" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.20.70, )", + "resolved": "4.20.70", + "contentHash": "4rNnAwdpXJBuxqrOCzCyICXHSImOTRktCgCWXWykuF1qwoIsVvEnR7PjbMk/eLOxWvhmj5Kwt+kDV3RGUYcNwg==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "MSTest.TestAdapter": { + "type": "Direct", + "requested": "[3.4.3, )", + "resolved": "3.4.3", + "contentHash": "5ul31wYr17590gDumPxWMiBLPREfPF/ggtdPGfaKoYSsO0EW6H1GWY+7xnVCKa2SB4I/dSEZLDYSwRLDjA0LEQ==", + "dependencies": { + "Microsoft.Testing.Extensions.VSTestBridge": "1.2.1", + "Microsoft.Testing.Platform.MSBuild": "1.2.1" + } + }, + "MSTest.TestFramework": { + "type": "Direct", + "requested": "[3.4.3, )", + "resolved": "3.4.3", + "contentHash": "hu7F0PyRe47LScY2SCjRFIzP2QYxq1oeHMAIdao9onUm5WhobO9tfZrFAAkJ4v+66EQjilloEbA4kspVHCZpTg==" + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "Google.Apis": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", + "dependencies": { + "Google.Apis.Core": "1.69.0" + } + }, + "Google.Apis.Core": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.22.0", + "contentHash": "3AOM9bZtku7RQwHyMEY3tQMrHIgjcfRDa6YQpd/QG2LDGvMydSlL9Di+8LLMt7J2RDdfJ7/2jdYv6yHcMJAnNw==", + "dependencies": { + "System.Diagnostics.DiagnosticSource": "5.0.0" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.10.0", + "contentHash": "yC7oSlnR54XO5kOuHlVOKtxomNNN1BWXX8lK1G2jaPXT9sUok7kCOoA4Pgs0qyFaCtMrNsprztYMeoEGqCm4uA==" + }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "TD5QHg98m3+QhgEV1YVoNMl5KtBw/4rjfxLHO0e/YV9bPUBDKntApP4xdrVtGgCeQZHVfC2EXIGsdpRNrr87Pg==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.4", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "6.0.0", + "System.Text.Json": "6.0.0" + } + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "MKGxwQhDDEoTS/ntFb21Z6Bxh9VvknmSLgEWH+NFD86fbcIqE2Al8lrXkQPeH+AqCvlhx2WnPLKd81T2PXc2dw==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.22.0", + "Microsoft.Testing.Platform": "1.2.1" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "46SnzaLR+SDaTtBWy49xdFm/rI40I8nZtziqnt2d4lgILKovWPnkM8Pehnga/uwl+OznVIh0XuRsN3NokkX1TQ==", + "dependencies": { + "Microsoft.Testing.Platform": "1.2.1" + } + }, + "Microsoft.Testing.Extensions.VSTestBridge": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "Tu8CWHEwV/92WM2DRr/qeIdH243diV5s43ODPLl13XeRqGbZlu9lk7X0a7kcxhp0BLRlA3fqMW3F6RynrnDrPw==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.22.0", + "Microsoft.TestPlatform.ObjectModel": "17.5.0", + "Microsoft.Testing.Extensions.Telemetry": "1.2.1", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.2.1", + "Microsoft.Testing.Platform": "1.2.1" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "mb7irPwqjgusJ05BxuQ5KP6uofWaoDr/dfjFNItX1Q1Ntv3EDMr3CeLInrlU2PNcPwwObw4X6bZG7wJvvFjKZQ==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "1.2.1", + "contentHash": "leUhW4iQNy7vmPk5uRHd4OROqfRtugWDQkWL/4AD17gxZwAAwGCaTcrqG0YVPi7uuZ+lj2Loa6kU7hBLA/v5+w==", + "dependencies": { + "Microsoft.Testing.Platform": "1.2.1" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.10.0", + "contentHash": "KkwhjQevuDj0aBRoPLY6OLAhGqbPUEBuKLbaCs0kUVw29qiOYncdORd4mLVJbn9vGZ7/iFGQ/+AoJl0Tu5Umdg==", + "dependencies": { + "System.Reflection.Metadata": "1.6.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.10.0", + "contentHash": "LWpMdfqhHvcUkeMCvNYJO8QlPLlYz9XPPb+ZbaXIKhdmjAV0wqTSrTiW5FLaf7RRZT50AQADDOYMOe0HxDxNgA==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.10.0", + "Newtonsoft.Json": "13.0.1" + } + }, + "Mono.Cecil": { + "type": "Transitive", + "resolved": "0.11.5", + "contentHash": "fxfX+0JGTZ8YQeu1MYjbBiK2CYTSzDyEeIixt+yqKKTn7FW8rv7JMY70qevup4ZJfD7Kk/VG/jDzQQTpfch87g==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "tCQTzPsGZh/A9LhhA6zrqCRV4hOHsK90/G7q3Khxmn6tnB1PuNU0cRaKANP2AWcF9bn0zsuOoZOSrHuJk6oNBA==" + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", + "dependencies": { + "System.CodeDom": "7.0.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "III/lNMSn0ZRBuM9m5Cgbiho5j81u0FAEagFX5ta2DKbljZ3T0IpD8j+BIiHQPeKqJppWS9bGEp6JnKnWKze0g==", + "dependencies": { + "System.Collections.Immutable": "6.0.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" + }, + "google.genai": { + "type": "Project", + "dependencies": { + "Google.Apis.Auth": "[1.69.0, )", + "Microsoft.Extensions.AI.Abstractions": "[10.4.1, )", + "MimeTypes": "[2.5.2, )", + "System.Text.Json": "[10.0.4, )" + } + }, + "Google.Apis.Auth": { + "type": "CentralTransitive", + "requested": "[1.69.0, )", + "resolved": "1.69.0", + "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", + "dependencies": { + "Google.Apis": "1.69.0", + "Google.Apis.Core": "1.69.0", + "System.Management": "7.0.2" + } + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.4.1, )", + "resolved": "10.4.1", + "contentHash": "ZxhU/wg9BOc3ohibhLl18toPLWm96ysQoE+3OhCgrZ0TUPZd7bsUmGteeatz08yweyuPIEhtyUzEZTF+3bMWEQ==", + "dependencies": { + "System.Text.Json": "10.0.4" + } + }, + "MimeTypes": { + "type": "CentralTransitive", + "requested": "[2.5.2, )", + "resolved": "2.5.2", + "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" + }, + "System.Text.Json": { + "type": "CentralTransitive", + "requested": "[10.0.4, )", + "resolved": "10.0.4", + "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", + "dependencies": { + "System.IO.Pipelines": "10.0.4", + "System.Text.Encodings.Web": "10.0.4" + } + } + } + } } \ No newline at end of file diff --git a/Google.GenAI/Common.cs b/Google.GenAI/Common.cs index 78604dbf..c4ead0ea 100644 --- a/Google.GenAI/Common.cs +++ b/Google.GenAI/Common.cs @@ -1,524 +1,524 @@ -/* - * Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -using System.Text.Json; -using System.Text.Json.Nodes; - -namespace Google.GenAI -{ - - /// - /// Common utility methods for the GenAI SDK to work with JSON. - /// - // TODO(b/413510963): make this method internal to only be used in converters. - public static class Common - { - /// - /// Sets the value of an object by a path. - /// - /// Common.SetValueByPath(containerJsonObject, new string[]{"secondMember", "childMember"}, 42); - /// - // TODO(b/413510963): make this method internal to only be used in converters. - public static void SetValueByPath(JsonObject jsonObject, string[] path, object value) - { - if (path == null || path.Length == 0) - { - throw new ArgumentException("Path cannot be empty."); - } - if (jsonObject == null) - { - throw new ArgumentException("JsonObject cannot be null."); - } - - JsonObject currentObject = jsonObject; - for (int i = 0; i < path.Length - 1; i++) - { - string key = path[i]; - - if (key.EndsWith("[]")) - { - string keyName = key.Substring(0, key.Length - 2); - if (!currentObject.ContainsKey(keyName)) - { - currentObject[keyName] = new JsonArray(); - } - JsonArray arrayNode = (JsonArray)currentObject[keyName]; - if (value is System.Collections.IList listValue) - { - if (arrayNode.Count != listValue.Count) - { - arrayNode.Clear(); - for (int j = 0; j < listValue.Count; j++) - { - arrayNode.Add(new JsonObject()); - } - } - for (int j = 0; j < arrayNode.Count; j++) - { - SetValueByPath( - (JsonObject)arrayNode[j], - path.Skip(i + 1).ToArray(), - listValue[j]); - } - } - else - { - if (arrayNode.Count == 0) - { - arrayNode.Add(new JsonObject()); - } - for (int j = 0; j < arrayNode.Count; j++) - { - SetValueByPath( - (JsonObject)arrayNode[j], path.Skip(i + 1).ToArray(), value); - } - } - return; - } - else if (key.EndsWith("[0]")) - { - string keyName = key.Substring(0, key.Length - 3); - if (!currentObject.ContainsKey(keyName)) - { - currentObject[keyName] = new JsonArray(new[] { (JsonNode)new JsonObject() }); - } - currentObject = (JsonObject)((JsonArray)currentObject[keyName])[0]; - } - else - { - if (!currentObject.ContainsKey(key)) - { - currentObject[key] = new JsonObject(); - } - currentObject = (JsonObject)currentObject[key]; - } - } - - string finalKey = path[path.Length - 1]; - if (finalKey.Equals("_self") && value is JsonObject selfNode) - { - foreach (var property in selfNode.ToList()) - { - currentObject[property.Key] = property.Value == null ? null : property.Value.DeepClone(); - } - return; - } - JsonNode? newNode = ToJsonNode(value); - - if (currentObject.ContainsKey(finalKey) && currentObject[finalKey] is JsonObject existingObject && newNode is JsonObject newObject) - { - foreach (KeyValuePair property in newObject) - { - existingObject[property.Key] = property.Value == null ? null : property.Value.DeepClone(); - } - } - else if(currentObject.ContainsKey(finalKey) && IsZero(value)) - { - return; - } - else - { - currentObject[finalKey] = newNode; - } - } - - /// - /// Gets the value of an object by a path. - /// - /// Common.GetValueByPath(containerJsonNode, new string[]{"secondMember", "childMember"}) - /// - // TODO(b/413510963): make this method internal to only be used in converters. - public static JsonNode? GetValueByPath(JsonNode obj, string[] keys) - { - if (obj == null || keys == null) - { - return null; - } - if (keys.Length == 1 && keys[0].Equals("_self")) - { - return obj; - } - - JsonNode? currentObject = obj; - for (int i = 0; i < keys.Length; i++) - { - string key = keys[i]; - - if (currentObject == null) - { - return null; - } - - if (key.EndsWith("[]")) - { - string keyName = key.Substring(0, key.Length - 2); - if (currentObject is JsonObject objNode - && objNode.ContainsKey(keyName) - && objNode[keyName] is JsonArray arrayNode) - { - if (keys.Length - 1 == i) - { - return arrayNode; - } - JsonArray result = new JsonArray(); - foreach (JsonNode? element in arrayNode) - { - JsonNode? node = - GetValueByPath(element, keys.Skip(i + 1).ToArray()); - if (node != null) - { - result.Add(node.DeepClone()); - } - } - return result; - } - else - { - return null; - } - } - else if (key.EndsWith("[0]")) - { - string keyName = key.Substring(0, key.Length - 3); - if (currentObject is JsonObject objNode - && objNode.ContainsKey(keyName) - && objNode[keyName] is JsonArray arrayNode - && arrayNode.Count > 0) - { - currentObject = arrayNode[0]; - } - else - { - return null; - } - } - else - { - if (currentObject is JsonObject objNode && objNode.ContainsKey(key)) - { - currentObject = objNode[key]; - } - else - { - return null; - } - } - } - - return currentObject; - } - - /// - /// Moves values from source paths to destination paths. - /// Example: MoveValueByPath( {'requests': [{'content': v1}, {'content': v2}]}, {'requests[].*': 'requests[].request.*'} ) -> {'requests': [{'request': {'content': v1}}, {'request': {'content': v2}}]} - /// - public static void MoveValueByPath(JsonNode data, IDictionary paths) - { - if (data == null || paths == null) - { - return; - } - - foreach (KeyValuePair entry in paths) - { - string sourcePath = entry.Key; - string destPath = entry.Value; - - string[] sourceKeys = sourcePath.Split('.'); - string[] destKeys = destPath.Split('.'); - - HashSet excludeKeys = new HashSet(); - int wildcardIdx = -1; - - for (int i = 0; i < sourceKeys.Length; i++) - { - if (sourceKeys[i].Equals("*")) - { - wildcardIdx = i; - break; - } - } - - if (wildcardIdx != -1 && destKeys.Length > wildcardIdx) - { - // Extract the intermediate key between source and dest paths - // Example: source=['requests[]', '*'], dest=['requests[]', 'request', '*'] - // We want to exclude 'request' - for (int i = wildcardIdx; i < destKeys.Length; i++) - { - string key = destKeys[i]; - if (!key.Equals("*") && !key.EndsWith("[]") && !key.EndsWith("[0]")) - { - excludeKeys.Add(key); - } - } - } - - MoveValueRecursive(data, sourceKeys, destKeys, 0, excludeKeys); - } - } - - /// - /// Efficiently converts a value to a JsonNode, using DeepClone() when the value is - /// already a JsonNode to avoid the expensive serialize-to-string-then-parse round-trip - /// that causes OutOfMemoryException with large payloads (e.g. base64 inline image data). - /// - internal static JsonNode? ParseToJsonNode(object? value) - { - if (value == null) - { - return null; - } - if (value is JsonNode node) - { - return node.DeepClone(); - } - return JsonSerializer.SerializeToNode(value); - } - - internal static string FormatQuery(JsonObject queryParams) - { - var queryParts = new List(); - foreach (var param in queryParams) - { - if (param.Value != null) - { - queryParts.Add($"{param.Key}={Uri.EscapeDataString(param.Value.ToString())}"); - } - } - return string.Join("&", queryParts); - } - - internal static string FormatMap(string template, JsonNode? data) - { - if (data is not JsonObject jsonObject) - { - return template; - } - - foreach (var field in jsonObject) - { - string key = field.Key; - string placeholder = "{" + key + "}"; - if (template.Contains(placeholder)) - { - template = template.Replace(placeholder, field.Value?.GetValue() ?? string.Empty); - } - } - return template; - } - - /// - /// Converts a JsonObject into a URL-encoded query string. - /// - /// The JsonObject containing the parameters to encode. - /// A URL-encoded string (e.g., "key1=value1&key2=value2"). - internal static string UrlEncode(JsonObject? paramsNode) - { - if (paramsNode == null || paramsNode.Count == 0) - { - return string.Empty; - } - - var queryParts = new List(); - - foreach (var field in paramsNode) - { - string encodedKey = Uri.EscapeDataString(field.Key); - var valueNode = field.Value; - - if (valueNode == null) - { - queryParts.Add($"{encodedKey}="); - } - else - { - string valueStr = valueNode.GetValueKind() == JsonValueKind.String - ? valueNode.GetValue() - : valueNode.ToJsonString().Trim('"'); - // In Python (and replay files), "*" is encoded as "%2A" although it is not required. - // So we keep the same behavior here. - string encodedValue = Uri.EscapeDataString(valueStr).Replace("*", "%2A"); - queryParts.Add($"{encodedKey}={encodedValue}"); - } - } - - return string.Join("&", queryParts); - } - - internal static bool IsZero(object? obj) - { - if (obj == null) - { - return true; - } - - if (obj is int i) - { - return i == 0; - } - else if (obj is long l) - { - return l == 0L; - } - else if (obj is float f) - { - return f == 0.0f; - } - else if (obj is double d) - { - return d == 0.0; - } - else if (obj is char ch) - { - return ch == '\0'; - } - else if (obj is bool b) - { - return !b; - } - else if (obj is System.Collections.ICollection c) - { - return c.Count == 0; - } - else if (obj is JsonArray a) - { - return a.Count == 0; - } - else if (obj is JsonObject jo) - { - return jo.Count == 0; - } - - return false; - } - - private static JsonNode? ToJsonNode(object value) - { - // TODO: evaluate using System.Text.Json to handle conversion of object to JSON. - switch (value) - { - case null: - return null; - case string s: - return JsonValue.Create(s); - case int i: - return JsonValue.Create(i); - case long l: - return JsonValue.Create(l); - case double d: - return JsonValue.Create(d); - case bool b: - return JsonValue.Create(b); - case JsonNode node: - return node.DeepClone(); - case System.Collections.IEnumerable enumerable: - JsonArray array = new JsonArray(); - foreach (var item in enumerable) - { - array.Add(ToJsonNode(item)); - } - return array; - default: - return JsonNode.Parse(JsonSerializer.Serialize(value)); - } - } - - private static void MoveValueRecursive( - JsonNode data, - string[] sourceKeys, - string[] destKeys, - int keyIdx, - HashSet excludeKeys) - { - if (keyIdx >= sourceKeys.Length || data == null) - { - return; - } - - string key = sourceKeys[keyIdx]; - - if (key.EndsWith("[]")) - { - string keyName = key.Substring(0, key.Length - 2); - if (data is JsonObject dataObj && dataObj.ContainsKey(keyName) && dataObj[keyName] is JsonArray arrayNode) - { - foreach (JsonNode item in arrayNode) - { - MoveValueRecursive(item, sourceKeys, destKeys, keyIdx + 1, excludeKeys); - } - } - } - else if (key.Equals("*")) - { - if (data is JsonObject objectNode) - { - List keysToMove = new List(); - foreach (var property in objectNode) - { - string fieldName = property.Key; - if (!fieldName.StartsWith("_") && !excludeKeys.Contains(fieldName)) - { - keysToMove.Add(fieldName); - } - } - - Dictionary valuesToMove = new Dictionary(); - foreach (string k in keysToMove) - { - valuesToMove.Add(k, objectNode[k]); - } - - foreach (KeyValuePair valueEntry in valuesToMove) - { - string k = valueEntry.Key; - JsonNode v = valueEntry.Value; - - List newDestKeysList = new List(); - for (int i = keyIdx; i < destKeys.Length; i++) - { - string dk = destKeys[i]; - if (dk.Equals("*")) - { - newDestKeysList.Add(k); - } - else - { - newDestKeysList.Add(dk); - } - } - - string[] newDestKeys = newDestKeysList.ToArray(); - SetValueByPath(objectNode, newDestKeys, v); - } - - foreach (string k in keysToMove) - { - objectNode.Remove(k); - } - } - } - else - { - if (data is JsonObject dataObj && dataObj.ContainsKey(key)) - { - JsonNode nextNode = dataObj[key]; - MoveValueRecursive(nextNode, sourceKeys, destKeys, keyIdx + 1, excludeKeys); - } - } - } - } +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Google.GenAI +{ + + /// + /// Common utility methods for the GenAI SDK to work with JSON. + /// + // TODO(b/413510963): make this method internal to only be used in converters. + public static class Common + { + /// + /// Sets the value of an object by a path. + /// + /// Common.SetValueByPath(containerJsonObject, new string[]{"secondMember", "childMember"}, 42); + /// + // TODO(b/413510963): make this method internal to only be used in converters. + public static void SetValueByPath(JsonObject jsonObject, string[] path, object value) + { + if (path == null || path.Length == 0) + { + throw new ArgumentException("Path cannot be empty."); + } + if (jsonObject == null) + { + throw new ArgumentException("JsonObject cannot be null."); + } + + JsonObject currentObject = jsonObject; + for (int i = 0; i < path.Length - 1; i++) + { + string key = path[i]; + + if (key.EndsWith("[]")) + { + string keyName = key.Substring(0, key.Length - 2); + if (!currentObject.ContainsKey(keyName)) + { + currentObject[keyName] = new JsonArray(); + } + JsonArray arrayNode = (JsonArray)currentObject[keyName]; + if (value is System.Collections.IList listValue) + { + if (arrayNode.Count != listValue.Count) + { + arrayNode.Clear(); + for (int j = 0; j < listValue.Count; j++) + { + arrayNode.Add(new JsonObject()); + } + } + for (int j = 0; j < arrayNode.Count; j++) + { + SetValueByPath( + (JsonObject)arrayNode[j], + path.Skip(i + 1).ToArray(), + listValue[j]); + } + } + else + { + if (arrayNode.Count == 0) + { + arrayNode.Add(new JsonObject()); + } + for (int j = 0; j < arrayNode.Count; j++) + { + SetValueByPath( + (JsonObject)arrayNode[j], path.Skip(i + 1).ToArray(), value); + } + } + return; + } + else if (key.EndsWith("[0]")) + { + string keyName = key.Substring(0, key.Length - 3); + if (!currentObject.ContainsKey(keyName)) + { + currentObject[keyName] = new JsonArray(new[] { (JsonNode)new JsonObject() }); + } + currentObject = (JsonObject)((JsonArray)currentObject[keyName])[0]; + } + else + { + if (!currentObject.ContainsKey(key)) + { + currentObject[key] = new JsonObject(); + } + currentObject = (JsonObject)currentObject[key]; + } + } + + string finalKey = path[path.Length - 1]; + if (finalKey.Equals("_self") && value is JsonObject selfNode) + { + foreach (var property in selfNode.ToList()) + { + currentObject[property.Key] = property.Value == null ? null : property.Value.DeepClone(); + } + return; + } + JsonNode? newNode = ToJsonNode(value); + + if (currentObject.ContainsKey(finalKey) && currentObject[finalKey] is JsonObject existingObject && newNode is JsonObject newObject) + { + foreach (KeyValuePair property in newObject) + { + existingObject[property.Key] = property.Value == null ? null : property.Value.DeepClone(); + } + } + else if(currentObject.ContainsKey(finalKey) && IsZero(value)) + { + return; + } + else + { + currentObject[finalKey] = newNode; + } + } + + /// + /// Gets the value of an object by a path. + /// + /// Common.GetValueByPath(containerJsonNode, new string[]{"secondMember", "childMember"}) + /// + // TODO(b/413510963): make this method internal to only be used in converters. + public static JsonNode? GetValueByPath(JsonNode obj, string[] keys) + { + if (obj == null || keys == null) + { + return null; + } + if (keys.Length == 1 && keys[0].Equals("_self")) + { + return obj; + } + + JsonNode? currentObject = obj; + for (int i = 0; i < keys.Length; i++) + { + string key = keys[i]; + + if (currentObject == null) + { + return null; + } + + if (key.EndsWith("[]")) + { + string keyName = key.Substring(0, key.Length - 2); + if (currentObject is JsonObject objNode + && objNode.ContainsKey(keyName) + && objNode[keyName] is JsonArray arrayNode) + { + if (keys.Length - 1 == i) + { + return arrayNode; + } + JsonArray result = new JsonArray(); + foreach (JsonNode? element in arrayNode) + { + JsonNode? node = + GetValueByPath(element, keys.Skip(i + 1).ToArray()); + if (node != null) + { + result.Add(node.DeepClone()); + } + } + return result; + } + else + { + return null; + } + } + else if (key.EndsWith("[0]")) + { + string keyName = key.Substring(0, key.Length - 3); + if (currentObject is JsonObject objNode + && objNode.ContainsKey(keyName) + && objNode[keyName] is JsonArray arrayNode + && arrayNode.Count > 0) + { + currentObject = arrayNode[0]; + } + else + { + return null; + } + } + else + { + if (currentObject is JsonObject objNode && objNode.ContainsKey(key)) + { + currentObject = objNode[key]; + } + else + { + return null; + } + } + } + + return currentObject; + } + + /// + /// Moves values from source paths to destination paths. + /// Example: MoveValueByPath( {'requests': [{'content': v1}, {'content': v2}]}, {'requests[].*': 'requests[].request.*'} ) -> {'requests': [{'request': {'content': v1}}, {'request': {'content': v2}}]} + /// + public static void MoveValueByPath(JsonNode data, IDictionary paths) + { + if (data == null || paths == null) + { + return; + } + + foreach (KeyValuePair entry in paths) + { + string sourcePath = entry.Key; + string destPath = entry.Value; + + string[] sourceKeys = sourcePath.Split('.'); + string[] destKeys = destPath.Split('.'); + + HashSet excludeKeys = new HashSet(); + int wildcardIdx = -1; + + for (int i = 0; i < sourceKeys.Length; i++) + { + if (sourceKeys[i].Equals("*")) + { + wildcardIdx = i; + break; + } + } + + if (wildcardIdx != -1 && destKeys.Length > wildcardIdx) + { + // Extract the intermediate key between source and dest paths + // Example: source=['requests[]', '*'], dest=['requests[]', 'request', '*'] + // We want to exclude 'request' + for (int i = wildcardIdx; i < destKeys.Length; i++) + { + string key = destKeys[i]; + if (!key.Equals("*") && !key.EndsWith("[]") && !key.EndsWith("[0]")) + { + excludeKeys.Add(key); + } + } + } + + MoveValueRecursive(data, sourceKeys, destKeys, 0, excludeKeys); + } + } + + /// + /// Efficiently converts a value to a JsonNode, using DeepClone() when the value is + /// already a JsonNode to avoid the expensive serialize-to-string-then-parse round-trip + /// that causes OutOfMemoryException with large payloads (e.g. base64 inline image data). + /// + internal static JsonNode? ParseToJsonNode(object? value) + { + if (value == null) + { + return null; + } + if (value is JsonNode node) + { + return node.DeepClone(); + } + return JsonSerializer.SerializeToNode(value, JsonConfig.InternalSerializerOptions); + } + + internal static string FormatQuery(JsonObject queryParams) + { + var queryParts = new List(); + foreach (var param in queryParams) + { + if (param.Value != null) + { + queryParts.Add($"{param.Key}={Uri.EscapeDataString(param.Value.ToString())}"); + } + } + return string.Join("&", queryParts); + } + + internal static string FormatMap(string template, JsonNode? data) + { + if (data is not JsonObject jsonObject) + { + return template; + } + + foreach (var field in jsonObject) + { + string key = field.Key; + string placeholder = "{" + key + "}"; + if (template.Contains(placeholder)) + { + template = template.Replace(placeholder, field.Value?.GetValue() ?? string.Empty); + } + } + return template; + } + + /// + /// Converts a JsonObject into a URL-encoded query string. + /// + /// The JsonObject containing the parameters to encode. + /// A URL-encoded string (e.g., "key1=value1&key2=value2"). + internal static string UrlEncode(JsonObject? paramsNode) + { + if (paramsNode == null || paramsNode.Count == 0) + { + return string.Empty; + } + + var queryParts = new List(); + + foreach (var field in paramsNode) + { + string encodedKey = Uri.EscapeDataString(field.Key); + var valueNode = field.Value; + + if (valueNode == null) + { + queryParts.Add($"{encodedKey}="); + } + else + { + string valueStr = valueNode.GetValueKind() == JsonValueKind.String + ? valueNode.GetValue() + : valueNode.ToJsonString().Trim('"'); + // In Python (and replay files), "*" is encoded as "%2A" although it is not required. + // So we keep the same behavior here. + string encodedValue = Uri.EscapeDataString(valueStr).Replace("*", "%2A"); + queryParts.Add($"{encodedKey}={encodedValue}"); + } + } + + return string.Join("&", queryParts); + } + + internal static bool IsZero(object? obj) + { + if (obj == null) + { + return true; + } + + if (obj is int i) + { + return i == 0; + } + else if (obj is long l) + { + return l == 0L; + } + else if (obj is float f) + { + return f == 0.0f; + } + else if (obj is double d) + { + return d == 0.0; + } + else if (obj is char ch) + { + return ch == '\0'; + } + else if (obj is bool b) + { + return !b; + } + else if (obj is System.Collections.ICollection c) + { + return c.Count == 0; + } + else if (obj is JsonArray a) + { + return a.Count == 0; + } + else if (obj is JsonObject jo) + { + return jo.Count == 0; + } + + return false; + } + + private static JsonNode? ToJsonNode(object value) + { + // TODO: evaluate using System.Text.Json to handle conversion of object to JSON. + switch (value) + { + case null: + return null; + case string s: + return JsonValue.Create(s); + case int i: + return JsonValue.Create(i); + case long l: + return JsonValue.Create(l); + case double d: + return JsonValue.Create(d); + case bool b: + return JsonValue.Create(b); + case JsonNode node: + return node.DeepClone(); + case System.Collections.IEnumerable enumerable: + JsonArray array = new JsonArray(); + foreach (var item in enumerable) + { + array.Add(ToJsonNode(item)); + } + return array; + default: + return JsonNode.Parse(JsonSerializer.Serialize(value, JsonConfig.InternalSerializerOptions)); + } + } + + private static void MoveValueRecursive( + JsonNode data, + string[] sourceKeys, + string[] destKeys, + int keyIdx, + HashSet excludeKeys) + { + if (keyIdx >= sourceKeys.Length || data == null) + { + return; + } + + string key = sourceKeys[keyIdx]; + + if (key.EndsWith("[]")) + { + string keyName = key.Substring(0, key.Length - 2); + if (data is JsonObject dataObj && dataObj.ContainsKey(keyName) && dataObj[keyName] is JsonArray arrayNode) + { + foreach (JsonNode item in arrayNode) + { + MoveValueRecursive(item, sourceKeys, destKeys, keyIdx + 1, excludeKeys); + } + } + } + else if (key.Equals("*")) + { + if (data is JsonObject objectNode) + { + List keysToMove = new List(); + foreach (var property in objectNode) + { + string fieldName = property.Key; + if (!fieldName.StartsWith("_") && !excludeKeys.Contains(fieldName)) + { + keysToMove.Add(fieldName); + } + } + + Dictionary valuesToMove = new Dictionary(); + foreach (string k in keysToMove) + { + valuesToMove.Add(k, objectNode[k]); + } + + foreach (KeyValuePair valueEntry in valuesToMove) + { + string k = valueEntry.Key; + JsonNode v = valueEntry.Value; + + List newDestKeysList = new List(); + for (int i = keyIdx; i < destKeys.Length; i++) + { + string dk = destKeys[i]; + if (dk.Equals("*")) + { + newDestKeysList.Add(k); + } + else + { + newDestKeysList.Add(dk); + } + } + + string[] newDestKeys = newDestKeysList.ToArray(); + SetValueByPath(objectNode, newDestKeys, v); + } + + foreach (string k in keysToMove) + { + objectNode.Remove(k); + } + } + } + else + { + if (data is JsonObject dataObj && dataObj.ContainsKey(key)) + { + JsonNode nextNode = dataObj[key]; + MoveValueRecursive(nextNode, sourceKeys, destKeys, keyIdx + 1, excludeKeys); + } + } + } + } } \ No newline at end of file diff --git a/Google.GenAI/GenAIJsonContext.cs b/Google.GenAI/GenAIJsonContext.cs new file mode 100644 index 00000000..c96747b4 --- /dev/null +++ b/Google.GenAI/GenAIJsonContext.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +using Google.GenAI.Types; + +namespace Google.GenAI +{ + [JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true)] + [JsonSerializable(typeof(BatchJob))] + [JsonSerializable(typeof(Blob))] + [JsonSerializable(typeof(CachedContent))] + [JsonSerializable(typeof(CancelBatchJobParameters))] + [JsonSerializable(typeof(CancelTuningJobParameters))] + [JsonSerializable(typeof(CancelTuningJobResponse))] + [JsonSerializable(typeof(ComputeTokensParameters))] + [JsonSerializable(typeof(ComputeTokensResponse))] + [JsonSerializable(typeof(Content))] + [JsonSerializable(typeof(CountTokensParameters))] + [JsonSerializable(typeof(CountTokensResponse))] + [JsonSerializable(typeof(CreateBatchJobParameters))] + [JsonSerializable(typeof(CreateCachedContentParameters))] + [JsonSerializable(typeof(CreateEmbeddingsBatchJobParameters))] + [JsonSerializable(typeof(CreateFileParameters))] + [JsonSerializable(typeof(CreateFileResponse))] + [JsonSerializable(typeof(CreateTuningJobParametersPrivate))] + [JsonSerializable(typeof(DeleteBatchJobParameters))] + [JsonSerializable(typeof(DeleteCachedContentParameters))] + [JsonSerializable(typeof(DeleteCachedContentResponse))] + [JsonSerializable(typeof(DeleteFileParameters))] + [JsonSerializable(typeof(DeleteFileResponse))] + [JsonSerializable(typeof(DeleteModelParameters))] + [JsonSerializable(typeof(DeleteModelResponse))] + [JsonSerializable(typeof(DeleteResourceJob))] + [JsonSerializable(typeof(EditImageParameters))] + [JsonSerializable(typeof(EditImageResponse))] + [JsonSerializable(typeof(EmbedContentParametersPrivate))] + [JsonSerializable(typeof(EmbedContentResponse))] + [JsonSerializable(typeof(FetchPredictOperationParameters))] + [JsonSerializable(typeof(GenerateContentParameters))] + [JsonSerializable(typeof(GenerateContentResponse))] + [JsonSerializable(typeof(GenerateImagesParameters))] + [JsonSerializable(typeof(GenerateImagesResponse))] + [JsonSerializable(typeof(GenerateVideosOperation))] + [JsonSerializable(typeof(GenerateVideosParameters))] + [JsonSerializable(typeof(GenerateVideosResponse))] + [JsonSerializable(typeof(GetBatchJobParameters))] + [JsonSerializable(typeof(GetCachedContentParameters))] + [JsonSerializable(typeof(GetFileParameters))] + [JsonSerializable(typeof(GetModelParameters))] + [JsonSerializable(typeof(GetOperationParameters))] + [JsonSerializable(typeof(GetTuningJobParameters))] + [JsonSerializable(typeof(InternalRegisterFilesParameters))] + [JsonSerializable(typeof(ListBatchJobsResponse))] + [JsonSerializable(typeof(ListCachedContentsParameters))] + [JsonSerializable(typeof(ListCachedContentsResponse))] + [JsonSerializable(typeof(ListFilesParameters))] + [JsonSerializable(typeof(ListFilesResponse))] + [JsonSerializable(typeof(ListModelsParameters))] + [JsonSerializable(typeof(ListModelsResponse))] + [JsonSerializable(typeof(ListTuningJobsParameters))] + [JsonSerializable(typeof(ListTuningJobsResponse))] + [JsonSerializable(typeof(LiveClientContent))] + [JsonSerializable(typeof(LiveClientMessage))] + [JsonSerializable(typeof(LiveClientRealtimeInput))] + [JsonSerializable(typeof(LiveClientSetup))] + [JsonSerializable(typeof(LiveClientToolResponse))] + [JsonSerializable(typeof(LiveConnectConfig))] + [JsonSerializable(typeof(LiveConnectParameters))] + [JsonSerializable(typeof(LiveSendClientContentParameters))] + [JsonSerializable(typeof(LiveSendRealtimeInputParameters))] + [JsonSerializable(typeof(LiveSendToolResponseParameters))] + [JsonSerializable(typeof(LiveServerContent))] + [JsonSerializable(typeof(LiveServerGoAway))] + [JsonSerializable(typeof(LiveServerMessage))] + [JsonSerializable(typeof(LiveServerSessionResumptionUpdate))] + [JsonSerializable(typeof(LiveServerSetupComplete))] + [JsonSerializable(typeof(LiveServerToolCall))] + [JsonSerializable(typeof(LiveServerToolCallCancellation))] + [JsonSerializable(typeof(Model))] + [JsonSerializable(typeof(RealtimeInputConfig))] + [JsonSerializable(typeof(RecontextImageParameters))] + [JsonSerializable(typeof(RecontextImageResponse))] + [JsonSerializable(typeof(RegisterFilesResponse))] + [JsonSerializable(typeof(Schema))] + [JsonSerializable(typeof(SegmentImageParameters))] + [JsonSerializable(typeof(SegmentImageResponse))] + [JsonSerializable(typeof(SpeechConfig))] + [JsonSerializable(typeof(Tool))] + [JsonSerializable(typeof(TuningJob))] + [JsonSerializable(typeof(TuningOperation))] + [JsonSerializable(typeof(UpdateCachedContentParameters))] + [JsonSerializable(typeof(UpdateModelParameters))] + [JsonSerializable(typeof(UpscaleImageAPIParameters))] + [JsonSerializable(typeof(UpscaleImageResponse))] + [JsonSerializable(typeof(Google.GenAI.Types.File))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(System.Text.Json.Nodes.JsonNode))] + [JsonSerializable(typeof(System.Text.Json.Nodes.JsonObject))] + [JsonSerializable(typeof(System.Text.Json.Nodes.JsonArray))] + internal partial class GenAIJsonContext : JsonSerializerContext + { + } +} diff --git a/Google.GenAI/Google.GenAI.csproj b/Google.GenAI/Google.GenAI.csproj index cebfc17f..c5b72144 100644 --- a/Google.GenAI/Google.GenAI.csproj +++ b/Google.GenAI/Google.GenAI.csproj @@ -1,41 +1,42 @@ - - - - - Library - netstandard2.0;net8.0 - 10.0 - enable - enable - true - true - Google.GenAI - Google LLC - Google GenAI SDK for .NET - true - Apache-2.0 - https://github.com/googleapis/dotnet-genai - https://github.com/googleapis/dotnet-genai - git - README.md - - - - $(WarningsAsErrors);CS1570$(NoWarn);CS1591 - IDE0005 - $(NoWarn);MEAI001 - - - - - - - - - - - - - - - + + + + + Library + netstandard2.0;net8.0 + 10.0 + enable + enable + true + true + Google.GenAI + Google LLC + Google GenAI SDK for .NET + true + Apache-2.0 + https://github.com/googleapis/dotnet-genai + https://github.com/googleapis/dotnet-genai + git + README.md + + + + $(WarningsAsErrors);CS1570$(NoWarn);CS1591 + IDE0005 + $(NoWarn);MEAI001 + + + + + + + + + + + + + + + + diff --git a/Google.GenAI/GoogleGenAIExtensions.cs b/Google.GenAI/GoogleGenAIExtensions.cs index 3396872b..f3235e14 100644 --- a/Google.GenAI/GoogleGenAIExtensions.cs +++ b/Google.GenAI/GoogleGenAIExtensions.cs @@ -129,4 +129,21 @@ public static IHostedFileClient AsIHostedFileClient(this Files files) Utilities.ThrowIfNull(files, nameof(files)); return new GoogleGenAIHostedFileClient(files); } + + /// + /// Creates an wrapper around the specified + /// for use with the Google GenAI Live API. + /// + /// The to wrap. + /// The default model ID to use for realtime sessions. + /// An that wraps the specified client. + /// is . +#if NET8_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.Experimental("MEAI001")] +#endif + public static IRealtimeClient AsIRealtimeClient(this Client client, string? defaultModelId = null) + { + Utilities.ThrowIfNull(client, nameof(client)); + return new GoogleGenAIRealtimeClient(client, defaultModelId); + } } diff --git a/Google.GenAI/GoogleGenAIRealtimeClient.cs b/Google.GenAI/GoogleGenAIRealtimeClient.cs new file mode 100644 index 00000000..c830d44c --- /dev/null +++ b/Google.GenAI/GoogleGenAIRealtimeClient.cs @@ -0,0 +1,305 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Google.GenAI; +using Google.GenAI.Types; + +namespace Microsoft.Extensions.AI; + +/// +/// Provides an implementation for Google GenAI's Live API. +/// +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental("MEAI001")] +#endif +public sealed class GoogleGenAIRealtimeClient : IRealtimeClient +{ + private readonly Client _client; + private readonly string? _defaultModelId; + private ChatClientMetadata? _metadata; + + /// Initializes a new instance wrapping an existing . + /// The Google GenAI client. + /// The default model to use for realtime sessions (e.g. "gemini-2.5-flash-native-audio-preview-12-2025"). + /// is . + public GoogleGenAIRealtimeClient(Client client, string? defaultModelId = null) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + _defaultModelId = defaultModelId; + } + + /// Initializes a new instance using an API key. + /// The Google GenAI API key. + /// The default model to use for realtime sessions. + /// is or empty. + public GoogleGenAIRealtimeClient(string apiKey, string? defaultModelId = null) + { + if (string.IsNullOrEmpty(apiKey)) + { + throw new ArgumentNullException(nameof(apiKey)); + } + + _client = new Client(apiKey: apiKey); + _defaultModelId = defaultModelId; + } + + /// + public async Task CreateSessionAsync( + RealtimeSessionOptions? options = null, + CancellationToken cancellationToken = default) + { + string model = options?.Model ?? _defaultModelId + ?? throw new InvalidOperationException( + "No model specified. Provide a model via RealtimeSessionOptions.Model or the defaultModelId constructor parameter."); + + var config = BuildLiveConnectConfig(options); + + var asyncSession = await _client.Live.ConnectAsync(model, config, cancellationToken).ConfigureAwait(false); + + try + { + // The Google SDK's ConnectAsync sends the setup message but does NOT wait + // for the server's SetupComplete acknowledgment. We must drain it here so + // the session is fully ready (tools configured, modalities set) before the + // caller starts sending audio or text. + var setupResponse = await asyncSession.ReceiveAsync(cancellationToken).ConfigureAwait(false); + if (setupResponse?.SetupComplete is null) + { + throw new InvalidOperationException( + "Expected SetupComplete from Gemini server after connection, but received an unexpected message."); + } + + return new GoogleGenAIRealtimeSession(asyncSession, model, options); + } + catch + { + await asyncSession.DisposeAsync().ConfigureAwait(false); + throw; + } + } + + /// + public object? GetService(System.Type serviceType, object? serviceKey = null) + { + if (serviceType is null) + { + throw new ArgumentNullException(nameof(serviceType)); + } + + if (serviceKey is not null) + { + return null; + } + + if (serviceType == typeof(ChatClientMetadata)) + { + return _metadata ??= new ChatClientMetadata("google-genai", defaultModelId: _defaultModelId); + } + + if (serviceType.IsInstanceOfType(this)) + { + return this; + } + + if (serviceType.IsInstanceOfType(_client)) + { + return _client; + } + + return null; + } + + /// + public void Dispose() + { + // Client lifecycle is not owned by this wrapper. + } + + /// Converts MEAI session options to a Google GenAI . + internal static LiveConnectConfig BuildLiveConnectConfig(RealtimeSessionOptions? options) + { + var config = new LiveConnectConfig(); + + if (options is null) + { + config.ResponseModalities = new List { Modality.Audio }; + config.RealtimeInputConfig = new RealtimeInputConfig + { + AutomaticActivityDetection = new AutomaticActivityDetection { Disabled = true } + }; + return config; + } + + // Transcription-only sessions use a minimal configuration with + // only input audio transcription enabled (no audio output, no tools). + if (options.SessionKind == RealtimeSessionKind.Transcription) + { + return BuildTranscriptionConnectConfig(options); + } + + // System instructions + if (!string.IsNullOrEmpty(options.Instructions)) + { + config.SystemInstruction = new Content + { + Parts = new List { new Part { Text = options.Instructions } }, + Role = "user" + }; + } + + // Output modalities + if (options.OutputModalities is { Count: > 0 }) + { + config.ResponseModalities = new List(); + foreach (var modality in options.OutputModalities) + { + config.ResponseModalities.Add(modality.ToLowerInvariant() switch + { + "audio" => Modality.Audio, + "text" => Modality.Text, + _ => Modality.Text, + }); + } + } + else + { + config.ResponseModalities = new List { Modality.Audio }; + } + + // Voice / speech config + if (!string.IsNullOrEmpty(options.Voice)) + { + config.SpeechConfig = new SpeechConfig + { + VoiceConfig = new VoiceConfig + { + PrebuiltVoiceConfig = new PrebuiltVoiceConfig + { + VoiceName = options.Voice, + } + } + }; + } + + // Generation config + if (options.MaxOutputTokens.HasValue) + { + config.GenerationConfig ??= new GenerationConfig(); + config.GenerationConfig.MaxOutputTokens = options.MaxOutputTokens.Value; + } + + // Tools (AIFunction → Google FunctionDeclaration) + if (options.Tools is { Count: > 0 }) + { + var functionDeclarations = new List(); + foreach (var tool in options.Tools) + { + if (tool is AIFunction aiFunction) + { + functionDeclarations.Add(GoogleGenAIRealtimeSession.ToGoogleFunctionDeclaration(aiFunction)); + } + } + + if (functionDeclarations.Count > 0) + { + config.Tools = new List + { + new Tool { FunctionDeclarations = functionDeclarations } + }; + } + } + + // Transcription (both directions for conversation sessions) + if (options.TranscriptionOptions is not null) + { + var inputTranscriptionConfig = new AudioTranscriptionConfig(); + if (!string.IsNullOrEmpty(options.TranscriptionOptions.SpeechLanguage)) + { + inputTranscriptionConfig.LanguageCodes = new List { options.TranscriptionOptions.SpeechLanguage }; + } + + config.InputAudioTranscription = inputTranscriptionConfig; + config.OutputAudioTranscription = new AudioTranscriptionConfig(); + } + + // Configure VAD / activity detection based on MEAI options. + config.RealtimeInputConfig = new RealtimeInputConfig(); + + if (options.VoiceActivityDetection is { Enabled: true } vad) + { + // Automatic VAD enabled — the server detects speech boundaries. + config.RealtimeInputConfig.AutomaticActivityDetection = new AutomaticActivityDetection { Disabled = false }; + config.RealtimeInputConfig.ActivityHandling = vad.AllowInterruption + ? ActivityHandling.StartOfActivityInterrupts + : ActivityHandling.NoInterruption; + } + else if (options.VoiceActivityDetection is { Enabled: false }) + { + // VAD explicitly disabled — the client controls activity boundaries + // via explicit ActivityStart/ActivityEnd signals. + config.RealtimeInputConfig.AutomaticActivityDetection = new AutomaticActivityDetection { Disabled = true }; + } + else + { + // No VAD options specified — disable automatic VAD by default when using + // the MEAI audio buffering pattern (AudioBufferAppend → AudioBufferCommit). + // The client controls activity boundaries via explicit ActivityEnd signals. + config.RealtimeInputConfig.AutomaticActivityDetection = new AutomaticActivityDetection { Disabled = true }; + } + + return config; + } + + /// + /// Builds a minimal for transcription-only sessions. + /// Only input audio transcription is enabled; no audio output, tools, or voice config. + /// + private static LiveConnectConfig BuildTranscriptionConnectConfig(RealtimeSessionOptions options) + { + var config = new LiveConnectConfig + { + // No audio output for transcription-only sessions + ResponseModalities = new List { Modality.Text }, + }; + + // Enable input transcription with optional language hint + var transcriptionConfig = new AudioTranscriptionConfig(); + if (!string.IsNullOrEmpty(options.TranscriptionOptions?.SpeechLanguage)) + { + transcriptionConfig.LanguageCodes = new List { options.TranscriptionOptions.SpeechLanguage }; + } + + config.InputAudioTranscription = transcriptionConfig; + + // VAD configuration still applies for speech boundary detection + config.RealtimeInputConfig = new RealtimeInputConfig(); + + if (options.VoiceActivityDetection is { Enabled: true } vad) + { + config.RealtimeInputConfig.AutomaticActivityDetection = new AutomaticActivityDetection { Disabled = false }; + config.RealtimeInputConfig.ActivityHandling = vad.AllowInterruption + ? ActivityHandling.StartOfActivityInterrupts + : ActivityHandling.NoInterruption; + } + else + { + config.RealtimeInputConfig.AutomaticActivityDetection = new AutomaticActivityDetection { Disabled = true }; + } + + return config; + } +} + diff --git a/Google.GenAI/GoogleGenAIRealtimeSession.cs b/Google.GenAI/GoogleGenAIRealtimeSession.cs new file mode 100644 index 00000000..9d076bf5 --- /dev/null +++ b/Google.GenAI/GoogleGenAIRealtimeSession.cs @@ -0,0 +1,1037 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections; +using System.Collections.Concurrent; +using System.Linq; +using System.Net.WebSockets; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Text.Json; + +using Google.GenAI; +using Google.GenAI.Types; + +namespace Microsoft.Extensions.AI; + +/// +/// Provides an implementation for Google GenAI's Live API, +/// wrapping an WebSocket connection. +/// +#if NET8_0_OR_GREATER +[System.Diagnostics.CodeAnalysis.Experimental("MEAI001")] +#endif +public sealed class GoogleGenAIRealtimeSession : IRealtimeClientSession +{ + private readonly AsyncSession _asyncSession; + private readonly ChatClientMetadata _metadata; + private int _disposed; + + // Buffer for audio chunks between Append and Commit. + // Protected by _audioBufferLock. Capped at MaxAudioBufferBytes to prevent unbounded growth. + private readonly List _audioBuffer = new(); + private readonly object _audioBufferLock = new(); + private int _audioBufferSize; + + /// Maximum buffered audio size (10 MB). Exceeding this throws . + private const int MaxAudioBufferBytes = 10 * 1024 * 1024; + + // Track whether a response is in progress to emit ResponseCreated only once per response. + // Accessed only from GetStreamingResponseAsync's single enumeration; callers must not + // enumerate concurrently. + private bool _responseInProgress; + + // Guards against multiple concurrent GetStreamingResponseAsync enumerations. A single + // WebSocket can't safely serve two concurrent readers. + private int _activeStreamingEnumeration; + + /// Maximum nesting depth for tool payloads to prevent stack overflow from malicious/malformed data. + private const int MaxToolPayloadDepth = 64; + + // Serializes all WebSocket send operations. Required because: + // 1. WebSocket.SendAsync is NOT thread-safe for concurrent calls. + // 2. FunctionInvokingRealtimeSession middleware can call SendAsync (to return + // function results) concurrently with the caller's own SendAsync (e.g., audio). + // 3. HandleAudioCommitAsync sends a multi-message sequence (ActivityStart → + // audio frames → ActivityEnd) that must be atomic. + private readonly SemaphoreSlim _sendLock = new(1, 1); + + // Track whether a tool response was just sent. After SendToolResponseAsync, the server + // automatically continues generating — sending TurnComplete would be unexpected. + private bool _lastSendWasToolResponse; + + // Track whether the last content sent was media (image/video/audio via CreateConversationItem) + // that does not auto-trigger a model response. Unlike text, media input requires an explicit + // ActivityEnd signal in CreateResponse to prompt the model to respond. + private bool _pendingMediaNeedsTrigger; + + // Maps function call IDs to function names. Populated when ToolCall messages arrive, + // consumed when sending FunctionResponse back to the server. + private readonly ConcurrentDictionary _callIdToFunctionName = new(); + + // Accumulates function results across multiple CreateConversationItem sends so they can + // be batched into a single SendToolResponseAsync call. The MEAI middleware sends one + // CreateConversationItem per function result followed by a single CreateResponse. + // Gemini expects all function results in one SendToolResponseAsync call, so we buffer + // them here and flush on CreateResponse. + private readonly List _pendingToolResponses = new(); + + // When true, automatic VAD is enabled and the server handles speech boundary detection. + // ActivityStart/ActivityEnd framing is skipped during audio commit. + private readonly bool _vadEnabled; + + // The MIME type for audio frames sent to the server, derived from InputAudioFormat. + private readonly string _inputAudioMimeType; + + /// + public RealtimeSessionOptions? Options { get; private set; } + + /// Initializes a new instance wrapping a connected . + /// The connected for WebSocket communication. + /// The model name for metadata. + /// Optional initial session options. + public GoogleGenAIRealtimeSession( + AsyncSession asyncSession, + string model, + RealtimeSessionOptions? initialOptions) + { + _asyncSession = asyncSession ?? throw new ArgumentNullException(nameof(asyncSession)); + _metadata = new ChatClientMetadata("google-genai", defaultModelId: model); + Options = initialOptions; + _vadEnabled = initialOptions?.VoiceActivityDetection is { Enabled: true }; + _inputAudioMimeType = initialOptions?.InputAudioFormat?.MediaType ?? "audio/pcm"; + } + + /// + public async Task SendAsync( + RealtimeClientMessage message, + CancellationToken cancellationToken = default) + { + if (message is null) + { + throw new ArgumentNullException(nameof(message)); + } + + if (Volatile.Read(ref _disposed) != 0) + { + throw new ObjectDisposedException(nameof(GoogleGenAIRealtimeSession)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // AudioAppend only buffers data in memory — no WebSocket I/O, no lock needed. + if (message is InputAudioBufferAppendRealtimeClientMessage audioAppend) + { + HandleAudioAppend(audioAppend); + return; + } + + // All other message types perform WebSocket I/O and must be serialized. + // WaitAsync may throw ObjectDisposedException if DisposeAsync races between the + // _disposed check above and this call — treat it the same as a post-dispose send. + try + { + await _sendLock.WaitAsync(cancellationToken).ConfigureAwait(false); + } + catch (ObjectDisposedException) + { + throw new ObjectDisposedException(nameof(GoogleGenAIRealtimeSession)); + } + + try + { + // Recheck after acquiring lock to avoid race with DisposeAsync + if (Volatile.Read(ref _disposed) != 0) + { + throw new ObjectDisposedException(nameof(GoogleGenAIRealtimeSession)); + } + + switch (message) + { + case InputAudioBufferCommitRealtimeClientMessage: + await HandleAudioCommitAsync(cancellationToken).ConfigureAwait(false); + break; + + case CreateConversationItemRealtimeClientMessage itemCreate: + await HandleConversationItemCreateAsync(itemCreate, cancellationToken).ConfigureAwait(false); + break; + + case SessionUpdateRealtimeClientMessage: + // Gemini's Live API does not support mid-session reconfiguration. + break; + + case CreateResponseRealtimeClientMessage: + if (_pendingToolResponses.Count > 0) + { + // Flush all buffered function results in a single SendToolResponseAsync call. + // The MEAI middleware sends one CreateConversationItem per function result, + // but Gemini expects all results in one call. + await _asyncSession.SendToolResponseAsync( + new LiveSendToolResponseParameters + { + FunctionResponses = new List(_pendingToolResponses) + }, + cancellationToken).ConfigureAwait(false); + _pendingToolResponses.Clear(); + _lastSendWasToolResponse = true; + } + + if (_lastSendWasToolResponse) + { + // After a tool response, Gemini automatically continues generating. + // Do not send ActivityEnd — it would be unexpected. + _lastSendWasToolResponse = false; + } + else if (_pendingMediaNeedsTrigger) + { + // Media inputs (image, video) via SendRealtimeInputAsync are added to + // the model's context but don't auto-trigger a response. Gemini's Live API + // has no equivalent to OpenAI's CreateResponse command — the only way to + // trigger a response is via text input. Send a minimal whitespace text to + // prompt the model to respond about the media in context without biasing + // the response content. + _pendingMediaNeedsTrigger = false; + await _asyncSession.SendRealtimeInputAsync( + new LiveSendRealtimeInputParameters + { + Text = " ", + }, + cancellationToken).ConfigureAwait(false); + } + // For text input: auto-triggers, no signal needed. + // For audio commit: ActivityEnd/AudioStreamEnd already sent in HandleAudioCommitAsync. + break; + + default: + break; + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // The caller explicitly cancelled via their token — propagate so they + // can observe the cancellation they requested. + throw; + } + catch (ObjectDisposedException) + { + throw new ObjectDisposedException(nameof(GoogleGenAIRealtimeSession)); + } + catch (WebSocketException) when (Volatile.Read(ref _disposed) != 0) + { + // WebSocketException during/after disposal is expected — swallow. + } + finally + { + try + { + _sendLock.Release(); + } + catch (ObjectDisposedException) + { + // DisposeAsync was called concurrently and disposed the semaphore. + } + } + } + + /// + public async IAsyncEnumerable GetStreamingResponseAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (Volatile.Read(ref _disposed) != 0) + { + throw new ObjectDisposedException(nameof(GoogleGenAIRealtimeSession)); + } + + if (Interlocked.CompareExchange(ref _activeStreamingEnumeration, 1, 0) != 0) + { + throw new InvalidOperationException( + "Only one active streaming enumeration is allowed at a time. " + + "Await or cancel the existing enumeration before starting a new one."); + } + + try + { + while (!cancellationToken.IsCancellationRequested) + { + LiveServerMessage? serverMessage; + try + { + serverMessage = await _asyncSession.ReceiveAsync(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // The caller explicitly cancelled via their token — propagate so they + // can observe the cancellation they requested. + throw; + } + catch (Exception ex) when (ex is OperationCanceledException or ObjectDisposedException or WebSocketException) + { + // These exceptions are expected during session teardown and are swallowed: + // - OperationCanceledException: internal cancellation from disposal (not the caller's token). + // - ObjectDisposedException: DisposeAsync was called on another thread while an + // operation was in-flight on the underlying WebSocket. + // - WebSocketException: the connection was closed (server disconnect or local close). + yield break; + } + + if (serverMessage is null) + { + yield break; + } + + // Map Google Live server messages to MEAI server message types + foreach (var mapped in MapServerMessage(serverMessage)) + { + yield return mapped; + } + } + } + finally + { + Volatile.Write(ref _activeStreamingEnumeration, 0); + } + } + + /// + public object? GetService(System.Type serviceType, object? serviceKey = null) + { + if (serviceType is null) + { + throw new ArgumentNullException(nameof(serviceType)); + } + + if (serviceKey is not null) + { + return null; + } + + if (serviceType == typeof(ChatClientMetadata)) + { + return _metadata; + } + + if (serviceType.IsInstanceOfType(this)) + { + return this; + } + + if (serviceType.IsInstanceOfType(_asyncSession)) + { + return _asyncSession; + } + + return null; + } + + /// + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + { + return; + } + + _responseInProgress = false; + + Exception? firstException = null; + try + { + await _asyncSession.DisposeAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + firstException = ex; + } + + try + { + _sendLock.Dispose(); + } + catch (Exception ex) when (firstException is null) + { + firstException = ex; + } + + if (firstException is not null) + { + ExceptionDispatchInfo.Capture(firstException).Throw(); + } + } + + #region Send Helpers (MEAI → Google GenAI) + + private void HandleAudioAppend( + InputAudioBufferAppendRealtimeClientMessage audioAppend) + { + if (audioAppend.Content is null || !audioAppend.Content.HasTopLevelMediaType("audio")) + { + return; + } + + byte[] audioBytes = ExtractDataBytes(audioAppend.Content); + + // Buffer audio data; it will be sent on commit with proper activity framing. + lock (_audioBufferLock) + { + if (_audioBufferSize + audioBytes.Length > MaxAudioBufferBytes) + { + throw new InvalidOperationException( + $"Audio buffer would exceed {MaxAudioBufferBytes} bytes. " + + "Call AudioBufferCommit before appending more audio."); + } + + _audioBuffer.Add(audioBytes); + _audioBufferSize += audioBytes.Length; + } + } + + private async Task HandleAudioCommitAsync(CancellationToken cancellationToken) + { + List bufferedChunks; + lock (_audioBufferLock) + { + if (_audioBuffer.Count == 0) + { + return; + } + + // Snapshot and clear the buffer. Avoids consolidating all chunks into a + // single array only to re-split — instead we send each buffered chunk directly. + bufferedChunks = new List(_audioBuffer); + _audioBuffer.Clear(); + _audioBufferSize = 0; + } + + _lastSendWasToolResponse = false; + _pendingMediaNeedsTrigger = false; + + // When VAD is disabled, explicit ActivityStart/ActivityEnd framing is required + // to mark speech boundaries and trigger the model to respond. + // When VAD is enabled, the server auto-detects speech boundaries — + // sending explicit framing conflicts with automatic detection. + if (!_vadEnabled) + { + await _asyncSession.SendRealtimeInputAsync( + new LiveSendRealtimeInputParameters + { + ActivityStart = new ActivityStart() + }, + cancellationToken).ConfigureAwait(false); + } + + // Send buffered chunks directly, splitting only those that exceed the frame size limit. + const int maxFrameBytes = 32_000; + foreach (var buffered in bufferedChunks) + { + if (buffered.Length <= maxFrameBytes) + { + // Common case: chunk fits in a single frame — send without copying + await SendAudioFrameAsync(buffered, cancellationToken).ConfigureAwait(false); + } + else + { + // Large chunk: split into frames + for (int i = 0; i < buffered.Length; i += maxFrameBytes) + { + int len = Math.Min(maxFrameBytes, buffered.Length - i); + byte[] frame = new byte[len]; + Buffer.BlockCopy(buffered, i, frame, 0, len); + await SendAudioFrameAsync(frame, cancellationToken).ConfigureAwait(false); + } + } + } + + // When VAD is disabled, signal end of user activity to trigger the model's response. + // When VAD is enabled, send AudioStreamEnd to indicate the mic was turned off and the + // server should process the buffered audio. AudioStreamEnd is specifically designed for + // the push-to-talk pattern with automatic activity detection. + if (!_vadEnabled) + { + await _asyncSession.SendRealtimeInputAsync( + new LiveSendRealtimeInputParameters + { + ActivityEnd = new ActivityEnd() + }, + cancellationToken).ConfigureAwait(false); + } + else + { + await _asyncSession.SendRealtimeInputAsync( + new LiveSendRealtimeInputParameters + { + AudioStreamEnd = true + }, + cancellationToken).ConfigureAwait(false); + } + } + + private Task SendAudioFrameAsync(byte[] data, CancellationToken cancellationToken) + { + return _asyncSession.SendRealtimeInputAsync( + new LiveSendRealtimeInputParameters + { + Audio = new Blob + { + Data = data, + MimeType = _inputAudioMimeType, + } + }, + cancellationToken); + } + + private async Task HandleConversationItemCreateAsync( + CreateConversationItemRealtimeClientMessage itemCreate, + CancellationToken cancellationToken) + { + if (itemCreate.Item?.Contents is null or { Count: 0 }) + { + return; + } + + // Collect all function results (tool responses use a separate API call). + var functionResults = new List(); + foreach (var content in itemCreate.Item.Contents) + { + if (content is FunctionResultContent functionResult) + { + _callIdToFunctionName.TryRemove(functionResult.CallId, out var functionName); + functionResults.Add(new FunctionResponse + { + Id = functionResult.CallId, + Name = functionName ?? string.Empty, + Response = new Dictionary + { + ["result"] = NormalizeToolPayload(functionResult.Result) ?? string.Empty + } + }); + } + } + + if (functionResults.Count > 0) + { + // Buffer function results — they will be flushed as a single batched + // SendToolResponseAsync call when CreateResponse arrives. + _pendingToolResponses.AddRange(functionResults); + _lastSendWasToolResponse = true; + return; + } + + // Send text and media via SendRealtimeInputAsync without activity framing. + // Text auto-triggers a model response. Images/audio are treated as streaming + // context by Gemini's Live API — they do NOT auto-trigger a response. + // When only media is sent (no accompanying text), we append a brief text prompt + // so the model knows to respond about the media content. + bool hasText = false; + bool hasMedia = false; + foreach (var content in itemCreate.Item.Contents) + { + if (content is TextContent textContent && !string.IsNullOrEmpty(textContent.Text)) + { + hasText = true; + _lastSendWasToolResponse = false; + await _asyncSession.SendRealtimeInputAsync( + new LiveSendRealtimeInputParameters + { + Text = textContent.Text, + }, + cancellationToken).ConfigureAwait(false); + } + else if (content is DataContent dataContent) + { + if (dataContent.HasTopLevelMediaType("image")) + { + hasMedia = true; + _lastSendWasToolResponse = false; + await _asyncSession.SendRealtimeInputAsync( + new LiveSendRealtimeInputParameters + { + Video = new Blob + { + Data = ExtractDataBytes(dataContent), + MimeType = dataContent.MediaType ?? "image/jpeg", + } + }, + cancellationToken).ConfigureAwait(false); + } + else if (dataContent.HasTopLevelMediaType("audio")) + { + hasMedia = true; + _lastSendWasToolResponse = false; + await _asyncSession.SendRealtimeInputAsync( + new LiveSendRealtimeInputParameters + { + Audio = new Blob + { + Data = ExtractDataBytes(dataContent), + MimeType = dataContent.MediaType ?? _inputAudioMimeType, + } + }, + cancellationToken).ConfigureAwait(false); + } + } + } + + if (hasMedia && !hasText) + { + // Gemini treats media as streaming context (like a video frame) and won't + // respond until it receives a text/voice prompt. Send a brief text to + // trigger a response about the media content. + _pendingMediaNeedsTrigger = true; + } + } + + internal static byte[] ExtractDataBytes(DataContent content) + { + string? dataUri = content.Uri?.ToString(); + + if (dataUri is not null) + { + int commaIndex = dataUri.LastIndexOf(','); + if (commaIndex >= 0 && commaIndex < dataUri.Length - 1) + { + string base64 = dataUri.Substring(commaIndex + 1); + try + { + return Convert.FromBase64String(base64); + } + catch (FormatException) + { + // Fall through to content.Data.ToArray() below + } + } + } + + return content.Data.ToArray(); + } + + #endregion + + #region Tool Payload Normalization + + internal static Dictionary NormalizeToolArguments(IReadOnlyDictionary arguments, int depth = 0) + { + ValidateToolPayloadDepth(depth); + + var normalized = new Dictionary(arguments.Count); + foreach (var pair in arguments) + { + normalized[pair.Key] = NormalizeToolPayload(pair.Value, depth + 1); + } + return normalized; + } + + internal static object? NormalizeToolPayload(object? value, int depth = 0) + { + ValidateToolPayloadDepth(depth); + + switch (value) + { + case null: + return null; + + case JsonElement element: + return ConvertJsonElementToToolPayload(element, depth + 1); + + case JsonDocument document: + return ConvertJsonElementToToolPayload(document.RootElement, depth + 1); + + case TextContent textContent: + return textContent.Text ?? ""; + + case DataContent dataContent: + return new Dictionary + { + ["data"] = Convert.ToBase64String(ExtractDataBytes(dataContent)), + ["mimeType"] = dataContent.MediaType, + ["name"] = dataContent.Name, + }; + + case UriContent uriContent: + return new Dictionary + { + ["uri"] = uriContent.Uri.AbsoluteUri, + ["mimeType"] = uriContent.MediaType, + }; + + case IEnumerable> pairs: + return NormalizeToolArguments(pairs.ToDictionary(pair => pair.Key, pair => pair.Value), depth + 1); + + case IDictionary dictionary: + var mapped = new Dictionary(); + foreach (DictionaryEntry entry in dictionary) + { + if (entry.Key is string key) + { + mapped[key] = NormalizeToolPayload(entry.Value, depth + 1); + } + } + return mapped; + + case IEnumerable aiContents: + return aiContents.Select(content => NormalizeToolPayload(content, depth + 1)).ToList(); + + case string or bool or byte or sbyte or short or ushort or int or uint or long or ulong or + float or double or decimal: + return value; + + case byte[] bytes: + return Convert.ToBase64String(bytes); + + case ReadOnlyMemory readOnlyMemory: + return Convert.ToBase64String(readOnlyMemory.ToArray()); + + case Memory memory: + return Convert.ToBase64String(memory.ToArray()); + + case Enum enumValue: + return enumValue.ToString(); + + case IEnumerable enumerable when value is not string: + var list = new List(); + foreach (object? item in enumerable) + { + list.Add(NormalizeToolPayload(item, depth + 1)); + } + return list; + + default: + return value.ToString(); + } + } + + private static object? ConvertJsonElementToToolPayload(JsonElement element, int depth) + { + ValidateToolPayloadDepth(depth); + + switch (element.ValueKind) + { + case JsonValueKind.Object: + var dictionary = new Dictionary(); + foreach (var property in element.EnumerateObject()) + { + dictionary[property.Name] = ConvertJsonElementToToolPayload(property.Value, depth + 1); + } + return dictionary; + + case JsonValueKind.Array: + var arrayList = new List(); + foreach (var item in element.EnumerateArray()) + { + arrayList.Add(ConvertJsonElementToToolPayload(item, depth + 1)); + } + return arrayList; + + case JsonValueKind.String: + return element.GetString(); + + case JsonValueKind.Number: + return element.TryGetInt64(out long intValue) ? intValue : element.GetDouble(); + + case JsonValueKind.True: + return true; + + case JsonValueKind.False: + return false; + + case JsonValueKind.Null: + case JsonValueKind.Undefined: + default: + return null; + } + } + + private static void ValidateToolPayloadDepth(int depth) + { + if (depth > MaxToolPayloadDepth) + { + throw new InvalidOperationException( + $"Realtime tool payloads exceed the maximum supported nesting depth of {MaxToolPayloadDepth}."); + } + } + + #endregion + + #region Receive Helpers (Google GenAI → MEAI) + + private IEnumerable MapServerMessage(LiveServerMessage serverMessage) + { + // SetupComplete — skip (internal protocol message, not relevant to MEAI consumers) + if (serverMessage.SetupComplete is not null) + { + yield break; + } + + // Server content (model responses — audio, text, transcription) + if (serverMessage.ServerContent is { } serverContent) + { + foreach (var msg in MapServerContent(serverContent, serverMessage)) + { + yield return msg; + } + } + + // Tool calls — emit ResponseCreated (if not already), then ResponseOutputItemAdded + ResponseOutputItemDone for each + if (serverMessage.ToolCall is { FunctionCalls: { Count: > 0 } functionCalls }) + { + if (!_responseInProgress) + { + _responseInProgress = true; + yield return new ResponseCreatedRealtimeServerMessage(RealtimeServerMessageType.ResponseCreated) + { + RawRepresentation = serverMessage, + }; + } + + foreach (var fc in functionCalls) + { + // Ensure every function call has a usable ID for the round-trip mapping. + var callId = fc.Id ?? Guid.NewGuid().ToString(); + var functionName = fc.Name ?? string.Empty; + + _callIdToFunctionName[callId] = functionName; + + var contents = new List + { + fc.Args is not null + ? FunctionCallContent.CreateFromParsedArguments( + fc.Args, callId, functionName, + static args => args is IReadOnlyDictionary dictionary + ? NormalizeToolArguments(dictionary) : null) + : new FunctionCallContent(callId, functionName) + }; + + var item = new RealtimeConversationItem(contents, id: callId, role: ChatRole.Assistant); + + // Emit ResponseOutputItemAdded (signals start of output item) + yield return new ResponseOutputItemRealtimeServerMessage(RealtimeServerMessageType.ResponseOutputItemAdded) + { + Item = item, + RawRepresentation = serverMessage, + }; + + // Emit ResponseOutputItemDone (required by FunctionInvokingRealtimeSession middleware) + yield return new ResponseOutputItemRealtimeServerMessage(RealtimeServerMessageType.ResponseOutputItemDone) + { + Item = item, + RawRepresentation = serverMessage, + }; + } + } + + // Tool call cancellation + if (serverMessage.ToolCallCancellation is { Ids: { Count: > 0 } }) + { + yield return new RealtimeServerMessage + { + Type = RealtimeServerMessageType.RawContentOnly, + RawRepresentation = serverMessage, + }; + } + + // Usage metadata — emit as ResponseDone only if one wasn't already emitted + // by TurnComplete/GenerationComplete above (which resets _responseInProgress). + if (serverMessage.UsageMetadata is { } usage && _responseInProgress) + { + _responseInProgress = false; + yield return new ResponseCreatedRealtimeServerMessage(RealtimeServerMessageType.ResponseDone) + { + Usage = new UsageDetails + { + InputTokenCount = usage.PromptTokenCount ?? 0, + OutputTokenCount = usage.ResponseTokenCount ?? 0, + TotalTokenCount = usage.TotalTokenCount ?? 0, + }, + RawRepresentation = serverMessage, + }; + } + + // GoAway (server disconnect) + if (serverMessage.GoAway is not null) + { + yield return new ErrorRealtimeServerMessage + { + Error = new ErrorContent("Server is disconnecting (GoAway)"), + RawRepresentation = serverMessage, + }; + } + } + + private IEnumerable MapServerContent( + LiveServerContent serverContent, + LiveServerMessage rawMessage) + { + if (serverContent.ModelTurn?.Parts is { Count: > 0 } parts) + { + // Emit ResponseCreated once when a new response cycle begins + if (!_responseInProgress) + { + _responseInProgress = true; + yield return new ResponseCreatedRealtimeServerMessage(RealtimeServerMessageType.ResponseCreated) + { + RawRepresentation = rawMessage, + }; + } + + foreach (var part in parts) + { + // Audio data + if (part.InlineData is { Data: not null } blob && + blob.MimeType?.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) == true) + { + yield return new OutputTextAudioRealtimeServerMessage(RealtimeServerMessageType.OutputAudioDelta) + { + Audio = Convert.ToBase64String(blob.Data), + RawRepresentation = rawMessage, + }; + } + + // Text response + if (!string.IsNullOrEmpty(part.Text)) + { + yield return new OutputTextAudioRealtimeServerMessage(RealtimeServerMessageType.OutputTextDelta) + { + Text = part.Text, + RawRepresentation = rawMessage, + }; + } + } + } + + // Input transcription + if (serverContent.InputTranscription is { Text: not null } inputTranscription) + { + yield return new InputAudioTranscriptionRealtimeServerMessage(RealtimeServerMessageType.InputAudioTranscriptionCompleted) + { + Transcription = inputTranscription.Text, + RawRepresentation = rawMessage, + }; + } + + // Output transcription + if (serverContent.OutputTranscription is { Text: not null } outputTranscription) + { + yield return new OutputTextAudioRealtimeServerMessage(RealtimeServerMessageType.OutputAudioTranscriptionDelta) + { + Text = outputTranscription.Text, + RawRepresentation = rawMessage, + }; + } + + // Turn complete or generation complete — reset response tracking and emit ResponseDone + if (serverContent.TurnComplete == true || serverContent.GenerationComplete == true) + { + _responseInProgress = false; + yield return new ResponseCreatedRealtimeServerMessage(RealtimeServerMessageType.ResponseDone) + { + RawRepresentation = rawMessage, + }; + } + } + + #endregion + + #region Tool Mapping Helpers + + /// + /// Converts an to a Google GenAI , + /// mapping the function name, description, and JSON schema for parameters. + /// + /// The AI function to convert. + /// A Google GenAI function declaration. + internal static FunctionDeclaration ToGoogleFunctionDeclaration(AIFunction aiFunction) + { + var declaration = new FunctionDeclaration + { + Name = aiFunction.Name, + Description = aiFunction.Description, + }; + + // Convert the MEAI JSON schema to a Google Schema object. + // Google's API expects the Schema type with uppercase type names (STRING, OBJECT, etc.), + // not raw JSON schema with lowercase types. Using Parameters instead of ParametersJsonSchema + // ensures compatibility with the Live API's function calling. + if (aiFunction.JsonSchema is JsonElement schemaElement && + schemaElement.ValueKind == JsonValueKind.Object) + { + declaration.Parameters = ConvertJsonSchemaToGoogleSchema(schemaElement); + } + + return declaration; + } + + /// + /// Recursively converts a standard JSON Schema to a Google GenAI + /// object, mapping lowercase type names to Google's uppercase enum values. + /// + internal static Schema ConvertJsonSchemaToGoogleSchema(JsonElement element) + { + var schema = new Schema(); + + if (element.TryGetProperty("type", out var typeValue)) + { + schema.Type = typeValue.GetString()?.ToLowerInvariant() switch + { + "object" => Google.GenAI.Types.Type.Object, + "string" => Google.GenAI.Types.Type.String, + "integer" => Google.GenAI.Types.Type.Integer, + "number" => Google.GenAI.Types.Type.Number, + "boolean" => Google.GenAI.Types.Type.Boolean, + "array" => Google.GenAI.Types.Type.Array, + _ => null + }; + } + + if (element.TryGetProperty("description", out var desc) && + desc.ValueKind == JsonValueKind.String) + { + schema.Description = desc.GetString(); + } + + if (element.TryGetProperty("properties", out var props) && + props.ValueKind == JsonValueKind.Object) + { + schema.Properties = new Dictionary(); + foreach (var prop in props.EnumerateObject()) + { + schema.Properties[prop.Name] = ConvertJsonSchemaToGoogleSchema(prop.Value); + } + } + + if (element.TryGetProperty("required", out var req) && + req.ValueKind == JsonValueKind.Array) + { + schema.Required = new List(); + foreach (var item in req.EnumerateArray()) + { + if (item.ValueKind == JsonValueKind.String) + { + schema.Required.Add(item.GetString()!); + } + } + } + + if (element.TryGetProperty("items", out var items) && + items.ValueKind == JsonValueKind.Object) + { + schema.Items = ConvertJsonSchemaToGoogleSchema(items); + } + + return schema; + } + + #endregion +} + diff --git a/Google.GenAI/JsonConfig.cs b/Google.GenAI/JsonConfig.cs index 1fe48286..b6170696 100644 --- a/Google.GenAI/JsonConfig.cs +++ b/Google.GenAI/JsonConfig.cs @@ -1,41 +1,59 @@ -/* - * Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -using System.Text.Json; -using System.Text.Json.Serialization; - -using Google.GenAI.Serialization; - -namespace Google.GenAI -{ - /// - /// Configuration for JSON serialization. - /// - internal static class JsonConfig - { - public static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = - { - new StringToLongConverter(), - new StringToNullableLongConverter(), - } - }; - } -} +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +using Google.GenAI.Serialization; + +namespace Google.GenAI +{ + /// + /// Configuration for JSON serialization. + /// + internal static class JsonConfig + { + /// + /// Options for external API output (indented, camelCase, no nulls). + /// + public static readonly JsonSerializerOptions JsonSerializerOptions = CreateOptions(writeIndented: true); + + /// + /// Options for internal serialization (compact, camelCase, no nulls). + /// Used for intermediate serialize-then-parse round-trips where indentation is wasteful. + /// + internal static readonly JsonSerializerOptions InternalSerializerOptions = CreateOptions(writeIndented: false); + + private static JsonSerializerOptions CreateOptions(bool writeIndented) + { + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = writeIndented, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = + { + new StringToLongConverter(), + new StringToNullableLongConverter(), + } + }; + options.TypeInfoResolverChain.Insert(0, GenAIJsonContext.Default); + options.TypeInfoResolverChain.Add(new DefaultJsonTypeInfoResolver()); + return options; + } + } +} diff --git a/Google.GenAI/Live.cs b/Google.GenAI/Live.cs index fa8c58fe..17d68395 100644 --- a/Google.GenAI/Live.cs +++ b/Google.GenAI/Live.cs @@ -1,436 +1,436 @@ -/* - * Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -using System.Net.WebSockets; -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; - -using Google.GenAI.Types; - -namespace Google.GenAI -{ - /// - /// Live class encapsulates the logic for connecting to Google's GenAI Live API. - /// Use to establish a websocket connection session. - /// - public class Live - { - private readonly ApiClient _apiClient; - - public Live(ApiClient apiClient) - { - _apiClient = apiClient; - } - - /// - /// Establishes a websocket connection to the specified model with the given configuration. - /// - /// - /// The name of the model to connect to. For example "gemini-2.0-flash-live-preview-04-09". - /// - /// - /// The parameters for establishing a connection to the model. - /// - /// The cancellation token to use for the connection. - /// - public async Task ConnectAsync(string model, LiveConnectConfig config, CancellationToken cancellationToken = default) - { - var clientWebSocket = new ClientWebSocket(); - bool success = false; - try - { - await SetRequestHeadersAsync(clientWebSocket, cancellationToken); - Uri serverUri = GetServerUri(); - await clientWebSocket.ConnectAsync(serverUri, cancellationToken); - string setupClientMessage = getSetupMessage(model, config); - byte[] buffer = Encoding.UTF8.GetBytes(setupClientMessage); - - await clientWebSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationToken); - - var session = new AsyncSession(clientWebSocket, _apiClient); - success = true; - return session; - } - finally - { - if (!success) - { - clientWebSocket.Dispose(); - } - } - } - - private async Task SetRequestHeadersAsync(ClientWebSocket clientWebSocket, CancellationToken cancellationToken = default) - { - if (_apiClient.VertexAI) - { - if (_apiClient.Credentials == null) - { - throw new InvalidOperationException("GoogleAuth credentials are required for Vertex AI."); - } - - string accessToken = await _apiClient.Credentials.GetAccessTokenForRequestAsync(cancellationToken: cancellationToken); - if (string.IsNullOrEmpty(accessToken)) - { - throw new InvalidOperationException("Failed to retrieve access token from credentials."); - } - clientWebSocket.Options.SetRequestHeader("Authorization", $"Bearer {accessToken}"); - } - else - { - if (string.IsNullOrEmpty(_apiClient.ApiKey)) - { - throw new InvalidOperationException("An API key is required for Gemini API connections."); - } - clientWebSocket.Options.SetRequestHeader("x-goog-api-key", _apiClient.ApiKey); - } - - foreach (var header in _apiClient?.HttpOptions?.Headers ?? new Dictionary()) - { - clientWebSocket.Options.SetRequestHeader(header.Key, header.Value); - } - } - - private Uri GetServerUri() - { - string baseUrl = _apiClient.HttpOptions?.BaseUrl; - if (string.IsNullOrEmpty(baseUrl)) - { - throw new InvalidOperationException("BaseUrl is not set in Client."); - } - try - { - bool hasSufficientAuth = (_apiClient.Project != null && _apiClient.Location != null) || _apiClient.ApiKey != null; - if (_apiClient.CustomBaseUrl != null && !_apiClient.CustomBaseUrl.EndsWith(".googleapis.com") && !hasSufficientAuth) - { - var customUri = new Uri(_apiClient.CustomBaseUrl); - return new UriBuilder( customUri ) { Scheme = customUri.Scheme == "http" ? "ws" : "wss" }.Uri; - } - - var baseUri = new Uri(baseUrl); - var uriBuilder = new UriBuilder(baseUri) - { - Scheme = baseUri.Scheme == "http" ? "ws" : "wss" - }; - - string wsBaseUrl = uriBuilder.Uri.ToString().TrimEnd('/'); - - if (_apiClient.VertexAI) - { - string apiVersion = _apiClient.HttpOptions?.ApiVersion ?? "v1beta1"; - return new Uri($"{wsBaseUrl}/ws/google.cloud.aiplatform.{apiVersion}.LlmBidiService/BidiGenerateContent"); - } - else - { - string apiVersion = _apiClient.HttpOptions?.ApiVersion ?? "v1beta"; - return new Uri($"{wsBaseUrl}/ws/google.ai.generativelanguage.{apiVersion}.GenerativeService.BidiGenerateContent"); - } - } - catch (UriFormatException e) - { - throw new InvalidOperationException("Failed to parse URL.", e); - } - } - private string getSetupMessage(string model, LiveConnectConfig config) - { - var transformedModel = Transformers.TModel(this._apiClient, model); - bool shouldPrependVertexProjectPath = _apiClient.VertexAI && string.IsNullOrEmpty(_apiClient.ApiKey) && - !((_apiClient.HttpOptions?.BaseUrlResourceScope == Types.ResourceScope.Collection) && !string.IsNullOrEmpty(_apiClient.HttpOptions?.BaseUrl)); - - if (shouldPrependVertexProjectPath && transformedModel != null && transformedModel.StartsWith("publishers/")) - { - transformedModel = string.Format( - "projects/{0}/locations/{1}/{2}", - _apiClient.Project, - _apiClient.Location, - transformedModel - ); - } - LiveConnectParameters parameters = new LiveConnectParameters - { - Model = transformedModel, - Config = config, - }; - LiveConverters liveConverters = new LiveConverters(_apiClient); - string jsonString = JsonSerializer.Serialize(parameters); - JsonNode? parameterNode = JsonNode.Parse(jsonString); - if (parameterNode == null) - { - throw new InvalidOperationException("Failed to parse jsonString into a JsonNode."); - } - JsonNode body; - if (_apiClient.VertexAI) - { - body = liveConverters.LiveConnectParametersToVertex(_apiClient, parameterNode, new JsonObject()); - } - else - { - body = liveConverters.LiveConnectParametersToMldev(_apiClient, parameterNode, new JsonObject()); - } - body?.AsObject().Remove("config"); - return JsonSerializer.Serialize(body, JsonConfig.JsonSerializerOptions); - } - - } - /// - /// Represents a websocket connection to the Google's GenAI Live API. - /// This class is not meant to be instantiated directly. - /// Instead, use to create an instance. - /// - public class AsyncSession : IAsyncDisposable - { - private readonly WebSocket _webSocket; - private readonly ApiClient _apiClient; - private int _isDisposed = 0; // 0 = false, 1 = true. Used with Interlocked. - - public AsyncSession(WebSocket webSocket, ApiClient apiClient) - { - _webSocket = webSocket; - _apiClient = apiClient; - } - - /// - /// Sends non-realtime, turn-based content to the model. - /// - /// There are two ways to send messages to the live API: - /// SendClientContentAsync and SendRealtimeInputAsync. - /// - /// - /// SendClientContentAsync messages are added to the model context - /// in order. Because SendClientContentAsync guarantees the order - /// of messages between the client and the server, the model cannot respond as - /// quickly as with SendRealtimeInputAsync. This is most noticeable when - /// sending objects that require significant preprocessing time (typically images). - /// - /// - /// SendRealtimeInputAsync sends a list of objects, - /// which offers more options than the objects sent by - /// SendClientContentAsync. - /// - /// - /// The main use cases for SendClientContentAsync over - /// SendRealtimeInputAsync are: - /// - /// - /// Prefilling a conversation context (including sending anything that can't be - /// represented as a realtime message) before starting a realtime conversation. - /// - /// - /// Conducting a non-realtime conversation with the live API. - /// - /// - /// Caution: Interleaving SendClientContentAsync and - /// SendRealtimeInputAsync in the same conversation is not recommended and - /// can lead to unexpected behavior. - /// - /// - /// - /// The client content to send to the model. - /// - /// The cancellation token to use for the send operation. - /// - public async Task SendClientContentAsync(LiveSendClientContentParameters clientContent, CancellationToken cancellationToken = default) - { - LiveClientMessage liveClientMessage = new LiveClientMessage(); - liveClientMessage.ClientContent = new LiveClientContent(); - liveClientMessage.ClientContent.Turns = clientContent.Turns; - liveClientMessage.ClientContent.TurnComplete = clientContent.TurnComplete; - - await send(liveClientMessage, cancellationToken); - } - - /// - /// Sends realtime input to the model. With SendRealtimeInputAsync, - /// Google's GenAI Live API will respond to audio automatically based on voice - /// activity detection (VAD). SendRealtimeInputAsync is optimized for - /// responsiveness at the expense of deterministic ordering of the conversation - /// messages. Response tokens are added to the context as they become available. - /// - /// - /// The realtime input to send to the model. - /// - /// The cancellation token to use for the send operation. - /// - public async Task SendRealtimeInputAsync(LiveSendRealtimeInputParameters realtimeInput, CancellationToken cancellationToken = default) - { - LiveClientMessage liveClientMessage = new LiveClientMessage(); - liveClientMessage.RealtimeInputParameters = realtimeInput; - await send(liveClientMessage, cancellationToken); - } - - public async Task SendToolResponseAsync(LiveSendToolResponseParameters toolResponse, CancellationToken cancellationToken = default) - { - LiveClientMessage liveClientMessage = new LiveClientMessage(); - liveClientMessage.ToolResponse = new LiveClientToolResponse - { - FunctionResponses = toolResponse.FunctionResponses - }; - await send(liveClientMessage, cancellationToken); - } - - /// - /// Receives model responses from the server. - /// - /// - /// A containing the model's response, or null if the - /// connection has been gracefully closed. - /// - /// Thrown if an empty or invalid message is received. - /// Thrown for underlying WebSocket errors that are not a graceful close. - public async Task ReceiveAsync(CancellationToken cancellationToken = default) - { - if (_isDisposed == 1) - { - return null; - } - - switch (_webSocket.State) - { - case WebSocketState.Connecting: - throw new InvalidOperationException("Cannot receive data while the WebSocket is still connecting. Ensure that the ConnectAsync method has completed."); - case WebSocketState.Open: - break; // Proceed with receiving. - default: - return null; - } - - var buffer = new byte[4096]; - var messageBuilder = new StringBuilder(); - WebSocketReceiveResult result; - - try - { - do - { - result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); - if (result.MessageType == WebSocketMessageType.Close) - { - return null; - } - messageBuilder.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); - } - while (!result.EndOfMessage); - } - catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) - { - return null; - } - - var messageString = messageBuilder.ToString(); - if (string.IsNullOrEmpty(messageString)) - { - throw new InvalidOperationException("Received an empty message from the server."); - } - - JsonNode? serverMessageNode = JsonNode.Parse(messageString); - if (serverMessageNode == null) - { - throw new InvalidOperationException("Failed to deserialize server message because it is null."); - } - LiveConverters liveConverters = new LiveConverters(_apiClient); - JsonNode transformedNode; - if (_apiClient.VertexAI) - { - transformedNode = liveConverters.LiveServerMessageFromVertex(serverMessageNode, new JsonObject()); - } - else - { - transformedNode = liveConverters.LiveServerMessageFromMldev(serverMessageNode, new JsonObject()); - } - var serverMessage = JsonSerializer.Deserialize(transformedNode, JsonConfig.JsonSerializerOptions); - if (serverMessage == null) - { - throw new InvalidOperationException("Failed to deserialize server message because it is null."); - } - return serverMessage; - } - - /// - /// Closes the WebSocket connection gracefully. This method is thread-safe and idempotent. - /// - public async Task CloseAsync() - { - // Atomically check and set the disposed flag to ensure this block runs only once. - // Critical to avoid race conditions in multi-threaded scenarios. - if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) != 0) - { - return; - } - - using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - - try - { - if (_webSocket.State == WebSocketState.Open || _webSocket.State == WebSocketState.Connecting) - { - await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", timeoutCts.Token); - } - else if (_webSocket.State == WebSocketState.CloseReceived) - { - await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Acknowledging server close", timeoutCts.Token); - } - // For other states (None, CloseSent, Closed, Aborted), no action is needed. - } - catch (Exception ex) when (ex is ObjectDisposedException || ex is InvalidOperationException || - ex is WebSocketException || ex is OperationCanceledException || - ex is IOException) - { - // Suppress exceptions during cleanup as the primary goal is to release resources. - // Optionally, these exceptions can be logged for debugging purposes. - } - finally - { - _webSocket.Dispose(); - } - } - - /// - /// Asynchronously disposes the session by closing the WebSocket connection. - /// - public async ValueTask DisposeAsync() - { - await CloseAsync(); - } - - private async Task send(LiveClientMessage liveClientMessage, CancellationToken cancellationToken = default) - { - JsonNode? liveClientMessageNode = JsonNode.Parse(JsonSerializer.Serialize(liveClientMessage, JsonConfig.JsonSerializerOptions)); - if (liveClientMessageNode == null) - { - throw new InvalidOperationException("Failed to parse liveClientMessage into a JsonNode."); - } - LiveConverters liveConverters = new LiveConverters(_apiClient); - JsonNode body; - if (_apiClient.VertexAI) - { - body = liveConverters.LiveClientMessageToVertex(liveClientMessageNode, new JsonObject()); - } - else - { - body = liveConverters.LiveClientMessageToMldev(liveClientMessageNode, new JsonObject()); - } - string jsonMessage = JsonSerializer.Serialize(body, JsonConfig.JsonSerializerOptions); - byte[] buffer = Encoding.UTF8.GetBytes(jsonMessage); - - if (_webSocket.State != WebSocketState.Open) - { - throw new InvalidOperationException($"WebSocket is not open. State: {_webSocket.State}"); - } - await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationToken); - } - } -} +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; + +using Google.GenAI.Types; + +namespace Google.GenAI +{ + /// + /// Live class encapsulates the logic for connecting to Google's GenAI Live API. + /// Use to establish a websocket connection session. + /// + public class Live + { + private readonly ApiClient _apiClient; + + public Live(ApiClient apiClient) + { + _apiClient = apiClient; + } + + /// + /// Establishes a websocket connection to the specified model with the given configuration. + /// + /// + /// The name of the model to connect to. For example "gemini-2.0-flash-live-preview-04-09". + /// + /// + /// The parameters for establishing a connection to the model. + /// + /// The cancellation token to use for the connection. + /// + public async Task ConnectAsync(string model, LiveConnectConfig config, CancellationToken cancellationToken = default) + { + var clientWebSocket = new ClientWebSocket(); + bool success = false; + try + { + await SetRequestHeadersAsync(clientWebSocket, cancellationToken); + Uri serverUri = GetServerUri(); + await clientWebSocket.ConnectAsync(serverUri, cancellationToken); + string setupClientMessage = getSetupMessage(model, config); + byte[] buffer = Encoding.UTF8.GetBytes(setupClientMessage); + + await clientWebSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationToken); + + var session = new AsyncSession(clientWebSocket, _apiClient); + success = true; + return session; + } + finally + { + if (!success) + { + clientWebSocket.Dispose(); + } + } + } + + private async Task SetRequestHeadersAsync(ClientWebSocket clientWebSocket, CancellationToken cancellationToken = default) + { + if (_apiClient.VertexAI) + { + if (_apiClient.Credentials == null) + { + throw new InvalidOperationException("GoogleAuth credentials are required for Vertex AI."); + } + + string accessToken = await _apiClient.Credentials.GetAccessTokenForRequestAsync(cancellationToken: cancellationToken); + if (string.IsNullOrEmpty(accessToken)) + { + throw new InvalidOperationException("Failed to retrieve access token from credentials."); + } + clientWebSocket.Options.SetRequestHeader("Authorization", $"Bearer {accessToken}"); + } + else + { + if (string.IsNullOrEmpty(_apiClient.ApiKey)) + { + throw new InvalidOperationException("An API key is required for Gemini API connections."); + } + clientWebSocket.Options.SetRequestHeader("x-goog-api-key", _apiClient.ApiKey); + } + + foreach (var header in _apiClient?.HttpOptions?.Headers ?? new Dictionary()) + { + clientWebSocket.Options.SetRequestHeader(header.Key, header.Value); + } + } + + private Uri GetServerUri() + { + string baseUrl = _apiClient.HttpOptions?.BaseUrl; + if (string.IsNullOrEmpty(baseUrl)) + { + throw new InvalidOperationException("BaseUrl is not set in Client."); + } + try + { + bool hasSufficientAuth = (_apiClient.Project != null && _apiClient.Location != null) || _apiClient.ApiKey != null; + if (_apiClient.CustomBaseUrl != null && !_apiClient.CustomBaseUrl.EndsWith(".googleapis.com") && !hasSufficientAuth) + { + var customUri = new Uri(_apiClient.CustomBaseUrl); + return new UriBuilder( customUri ) { Scheme = customUri.Scheme == "http" ? "ws" : "wss" }.Uri; + } + + var baseUri = new Uri(baseUrl); + var uriBuilder = new UriBuilder(baseUri) + { + Scheme = baseUri.Scheme == "http" ? "ws" : "wss" + }; + + string wsBaseUrl = uriBuilder.Uri.ToString().TrimEnd('/'); + + if (_apiClient.VertexAI) + { + string apiVersion = _apiClient.HttpOptions?.ApiVersion ?? "v1beta1"; + return new Uri($"{wsBaseUrl}/ws/google.cloud.aiplatform.{apiVersion}.LlmBidiService/BidiGenerateContent"); + } + else + { + string apiVersion = _apiClient.HttpOptions?.ApiVersion ?? "v1beta"; + return new Uri($"{wsBaseUrl}/ws/google.ai.generativelanguage.{apiVersion}.GenerativeService.BidiGenerateContent"); + } + } + catch (UriFormatException e) + { + throw new InvalidOperationException("Failed to parse URL.", e); + } + } + private string getSetupMessage(string model, LiveConnectConfig config) + { + var transformedModel = Transformers.TModel(this._apiClient, model); + bool shouldPrependVertexProjectPath = _apiClient.VertexAI && string.IsNullOrEmpty(_apiClient.ApiKey) && + !((_apiClient.HttpOptions?.BaseUrlResourceScope == Types.ResourceScope.Collection) && !string.IsNullOrEmpty(_apiClient.HttpOptions?.BaseUrl)); + + if (shouldPrependVertexProjectPath && transformedModel != null && transformedModel.StartsWith("publishers/")) + { + transformedModel = string.Format( + "projects/{0}/locations/{1}/{2}", + _apiClient.Project, + _apiClient.Location, + transformedModel + ); + } + LiveConnectParameters parameters = new LiveConnectParameters + { + Model = transformedModel, + Config = config, + }; + LiveConverters liveConverters = new LiveConverters(_apiClient); + string jsonString = JsonSerializer.Serialize(parameters, JsonConfig.InternalSerializerOptions); + JsonNode? parameterNode = JsonNode.Parse(jsonString); + if (parameterNode == null) + { + throw new InvalidOperationException("Failed to parse jsonString into a JsonNode."); + } + JsonNode body; + if (_apiClient.VertexAI) + { + body = liveConverters.LiveConnectParametersToVertex(_apiClient, parameterNode, new JsonObject()); + } + else + { + body = liveConverters.LiveConnectParametersToMldev(_apiClient, parameterNode, new JsonObject()); + } + body?.AsObject().Remove("config"); + return JsonSerializer.Serialize(body, JsonConfig.JsonSerializerOptions); + } + + } + /// + /// Represents a websocket connection to the Google's GenAI Live API. + /// This class is not meant to be instantiated directly. + /// Instead, use to create an instance. + /// + public class AsyncSession : IAsyncDisposable + { + private readonly WebSocket _webSocket; + private readonly ApiClient _apiClient; + private int _isDisposed = 0; // 0 = false, 1 = true. Used with Interlocked. + + public AsyncSession(WebSocket webSocket, ApiClient apiClient) + { + _webSocket = webSocket; + _apiClient = apiClient; + } + + /// + /// Sends non-realtime, turn-based content to the model. + /// + /// There are two ways to send messages to the live API: + /// SendClientContentAsync and SendRealtimeInputAsync. + /// + /// + /// SendClientContentAsync messages are added to the model context + /// in order. Because SendClientContentAsync guarantees the order + /// of messages between the client and the server, the model cannot respond as + /// quickly as with SendRealtimeInputAsync. This is most noticeable when + /// sending objects that require significant preprocessing time (typically images). + /// + /// + /// SendRealtimeInputAsync sends a list of objects, + /// which offers more options than the objects sent by + /// SendClientContentAsync. + /// + /// + /// The main use cases for SendClientContentAsync over + /// SendRealtimeInputAsync are: + /// + /// + /// Prefilling a conversation context (including sending anything that can't be + /// represented as a realtime message) before starting a realtime conversation. + /// + /// + /// Conducting a non-realtime conversation with the live API. + /// + /// + /// Caution: Interleaving SendClientContentAsync and + /// SendRealtimeInputAsync in the same conversation is not recommended and + /// can lead to unexpected behavior. + /// + /// + /// + /// The client content to send to the model. + /// + /// The cancellation token to use for the send operation. + /// + public async Task SendClientContentAsync(LiveSendClientContentParameters clientContent, CancellationToken cancellationToken = default) + { + LiveClientMessage liveClientMessage = new LiveClientMessage(); + liveClientMessage.ClientContent = new LiveClientContent(); + liveClientMessage.ClientContent.Turns = clientContent.Turns; + liveClientMessage.ClientContent.TurnComplete = clientContent.TurnComplete; + + await send(liveClientMessage, cancellationToken); + } + + /// + /// Sends realtime input to the model. With SendRealtimeInputAsync, + /// Google's GenAI Live API will respond to audio automatically based on voice + /// activity detection (VAD). SendRealtimeInputAsync is optimized for + /// responsiveness at the expense of deterministic ordering of the conversation + /// messages. Response tokens are added to the context as they become available. + /// + /// + /// The realtime input to send to the model. + /// + /// The cancellation token to use for the send operation. + /// + public async Task SendRealtimeInputAsync(LiveSendRealtimeInputParameters realtimeInput, CancellationToken cancellationToken = default) + { + LiveClientMessage liveClientMessage = new LiveClientMessage(); + liveClientMessage.RealtimeInputParameters = realtimeInput; + await send(liveClientMessage, cancellationToken); + } + + public async Task SendToolResponseAsync(LiveSendToolResponseParameters toolResponse, CancellationToken cancellationToken = default) + { + LiveClientMessage liveClientMessage = new LiveClientMessage(); + liveClientMessage.ToolResponse = new LiveClientToolResponse + { + FunctionResponses = toolResponse.FunctionResponses + }; + await send(liveClientMessage, cancellationToken); + } + + /// + /// Receives model responses from the server. + /// + /// + /// A containing the model's response, or null if the + /// connection has been gracefully closed. + /// + /// Thrown if an empty or invalid message is received. + /// Thrown for underlying WebSocket errors that are not a graceful close. + public async Task ReceiveAsync(CancellationToken cancellationToken = default) + { + if (_isDisposed == 1) + { + return null; + } + + switch (_webSocket.State) + { + case WebSocketState.Connecting: + throw new InvalidOperationException("Cannot receive data while the WebSocket is still connecting. Ensure that the ConnectAsync method has completed."); + case WebSocketState.Open: + break; // Proceed with receiving. + default: + return null; + } + + var buffer = new byte[4096]; + using var messageStream = new MemoryStream(); + WebSocketReceiveResult result; + + try + { + do + { + result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); + if (result.MessageType == WebSocketMessageType.Close) + { + return null; + } + messageStream.Write(buffer, 0, result.Count); + } + while (!result.EndOfMessage); + } + catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) + { + return null; + } + + var messageString = Encoding.UTF8.GetString(messageStream.GetBuffer(), 0, (int)messageStream.Length); + if (string.IsNullOrEmpty(messageString)) + { + throw new InvalidOperationException("Received an empty message from the server."); + } + + JsonNode? serverMessageNode = JsonNode.Parse(messageString); + if (serverMessageNode == null) + { + throw new InvalidOperationException("Failed to deserialize server message because it is null."); + } + LiveConverters liveConverters = new LiveConverters(_apiClient); + JsonNode transformedNode; + if (_apiClient.VertexAI) + { + transformedNode = liveConverters.LiveServerMessageFromVertex(serverMessageNode, new JsonObject()); + } + else + { + transformedNode = liveConverters.LiveServerMessageFromMldev(serverMessageNode, new JsonObject()); + } + var serverMessage = JsonSerializer.Deserialize(transformedNode, JsonConfig.JsonSerializerOptions); + if (serverMessage == null) + { + throw new InvalidOperationException("Failed to deserialize server message because it is null."); + } + return serverMessage; + } + + /// + /// Closes the WebSocket connection gracefully. This method is thread-safe and idempotent. + /// + public async Task CloseAsync() + { + // Atomically check and set the disposed flag to ensure this block runs only once. + // Critical to avoid race conditions in multi-threaded scenarios. + if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) != 0) + { + return; + } + + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + try + { + if (_webSocket.State == WebSocketState.Open || _webSocket.State == WebSocketState.Connecting) + { + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", timeoutCts.Token); + } + else if (_webSocket.State == WebSocketState.CloseReceived) + { + await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Acknowledging server close", timeoutCts.Token); + } + // For other states (None, CloseSent, Closed, Aborted), no action is needed. + } + catch (Exception ex) when (ex is ObjectDisposedException || ex is InvalidOperationException || + ex is WebSocketException || ex is OperationCanceledException || + ex is IOException) + { + // Suppress exceptions during cleanup as the primary goal is to release resources. + // Optionally, these exceptions can be logged for debugging purposes. + } + finally + { + _webSocket.Dispose(); + } + } + + /// + /// Asynchronously disposes the session by closing the WebSocket connection. + /// + public async ValueTask DisposeAsync() + { + await CloseAsync(); + } + + private async Task send(LiveClientMessage liveClientMessage, CancellationToken cancellationToken = default) + { + JsonNode? liveClientMessageNode = JsonNode.Parse(JsonSerializer.Serialize(liveClientMessage, JsonConfig.JsonSerializerOptions)); + if (liveClientMessageNode == null) + { + throw new InvalidOperationException("Failed to parse liveClientMessage into a JsonNode."); + } + LiveConverters liveConverters = new LiveConverters(_apiClient); + JsonNode body; + if (_apiClient.VertexAI) + { + body = liveConverters.LiveClientMessageToVertex(liveClientMessageNode, new JsonObject()); + } + else + { + body = liveConverters.LiveClientMessageToMldev(liveClientMessageNode, new JsonObject()); + } + string jsonMessage = JsonSerializer.Serialize(body, JsonConfig.JsonSerializerOptions); + byte[] buffer = Encoding.UTF8.GetBytes(jsonMessage); + + if (_webSocket.State != WebSocketState.Open) + { + throw new InvalidOperationException($"WebSocket is not open. State: {_webSocket.State}"); + } + await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationToken); + } + } +} diff --git a/Google.GenAI/Transformers.cs b/Google.GenAI/Transformers.cs index 1197eac5..7f9dd8c3 100644 --- a/Google.GenAI/Transformers.cs +++ b/Google.GenAI/Transformers.cs @@ -1,732 +1,732 @@ -/* - * Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.RegularExpressions; - -using Google.GenAI.Types; - - -namespace Google.GenAI -{ - /// - /// Transformers for GenAI SDK. - /// - internal static class Transformers - { - /// - /// Transforms a model name to the correct format for the API. - /// - /// The API client to use for transformation. - /// The model name to transform, can only be a string. - /// The transformed model name. - /// If the object is not a supported type. - internal static string? TModel(ApiClient apiClient, object origin) - { - string? model; - if (origin == null) - { - return null; - } - else if (origin is string strModel) - { - model = strModel; - } - else if (origin is JsonNode jsonNode) - { - model = jsonNode.ToString(); - model = model.Replace("\"", ""); - } - else - { - throw new ArgumentException($"Unsupported model type: {origin.GetType()}"); - } - - if (model.Length == 0) - { - throw new ArgumentException("model is required."); - } - if (model.Contains("..") || model.Contains("?") || model.Contains("&")) - { - throw new ArgumentException("invalid model parameter."); - } - if (apiClient.VertexAI) - { - if (model.StartsWith("publishers/") - || model.StartsWith("projects/") - || model.StartsWith("models/")) - { - return model; - } - else if (model.Contains("/")) - { -#if NETSTANDARD2_0 - string[] parts = model.Split(new[] { '/' }, 2, StringSplitOptions.RemoveEmptyEntries); -#else - string[] parts = model.Split('/', 2, StringSplitOptions.RemoveEmptyEntries); -#endif - return string.Format("publishers/{0}/models/{1}", parts[0], parts[1]); - } - else - { - return "publishers/google/models/" + model; - } - } - else - { - if (model.StartsWith("models/") || model.StartsWith("tunedModels/")) - { - return model; - } - else - { - return "models/" + model; - } - } - } - - /// - /// Determines the appropriate models URL based on the API client type and whether base models are - /// requested. - /// - /// The API client to use for transformation. - /// True if base models are requested, false otherwise. - /// The transformed model name - internal static string TModelsUrl(ApiClient apiClient, object? baseModels) - { - bool queryBase = true; - if (baseModels is JsonValue val) - { - queryBase = val.GetValue(); - } - if (queryBase) - { - return apiClient.VertexAI ? "publishers/google/models" : "models"; - } - else - { - return apiClient.VertexAI ? "models" : "tunedModels"; - } - } - - /// - /// Transforms an object to a list of Content for the API. - /// - /// The object to transform, can be a string, Content, or List<Content> - /// The transformed list of Content - /// If the object is not a supported type - internal static List? TContents(object? contents) - { - if (contents == null) - { - return null; - } - if (contents is string contentString) - { - Content content = new Content(); - content.Role = "user"; - Part part = new Part(); - part.Text = contentString; - content.Parts = new List { part }; - return new List { content }; - } - else if (contents is Content singleContent) - { - return new List { singleContent }; - } - else if (contents is List contentList) - { - return contentList; - } - else if (contents is JsonObject jsonObject) - { - return JsonSerializer.Deserialize>(jsonObject.ToString()); - } - else if (contents is JsonNode jsonNode) - { - return JsonSerializer.Deserialize>(jsonNode.ToString()); - } - - throw new ArgumentException($"Unsupported contents type: {contents.GetType()}"); - } - - /// - /// Transforms an object to a Content for the API. - /// - /// The object to transform, can be a string or Content - /// The transformed Content - /// If the object is not a supported type - internal static Content? TContent(object content) - { - if (content == null) - { - return null; - } - else if (content is string contentString) - { - Content contentObject = new Content(); - contentObject.Role = "user"; - Part part = new Part(); - part.Text = contentString; - contentObject.Parts = new List { part }; - return contentObject; - } - else if (content is Content singleContent) - { - return singleContent; - } - else if (content is JsonObject jsonObject) - { - return JsonSerializer.Deserialize(jsonObject.ToString()); - } - - throw new ArgumentException($"Unsupported content type: {content.GetType()}"); - } - - /// Transforms an object to a Schema for the API. - /// If the object is not a supported type. - internal static Schema? TSchema(object origin) - { - if (origin == null) - { - return null; - } - else if (origin is Schema schema) - { - return schema; - } - else if (origin is JsonObject jsonObject) - { - return JsonSerializer.Deserialize(jsonObject.ToString()); - } - throw new ArgumentException($"Unsupported schema type: {origin.GetType()}"); - } - - internal static SpeechConfig? TSpeechConfig(object speechConfig) - { - if (speechConfig == null) - { - return null; - } - else if (speechConfig is string speechConfigString) - { - return null; - } - else if (speechConfig is SpeechConfig config) - { - return config; - } - else if (speechConfig is JsonObject jsonObject) - { - return JsonSerializer.Deserialize(jsonObject.ToString()); - } - - throw new ArgumentException($"Unsupported speechConfig type:{speechConfig.GetType()}"); - } - - /// Transforms an object to a list of Tools for the API. - /// If the object is not a supported type. - internal static List? TTools(object origin) - { - if (origin == null) - { - return null; - } - else if (origin is List tools) - { - List transformedTools = new List(); - foreach (Tool tool in tools) - { - transformedTools.Add(TTool(tool)!); - } - return transformedTools; - } - else if (origin is JsonArray jsonArray) - { - List toolList = new List(); - foreach(JsonNode? toolNode in jsonArray) - { - if(toolNode != null) - { - toolList.Add(TTool(toolNode)!); - } - } - return toolList; - } - else if (origin is JsonNode jsonNode) - { - return JsonSerializer.Deserialize>(jsonNode.ToJsonString()); - } - - throw new ArgumentException($"Unsupported tools type: {origin.GetType()}"); - } - - /// Transforms an object to a Tool for the API. - /// If the object is not a supported type. - internal static Tool? TTool(object origin) - { - if (origin == null) - { - return null; - } - else if (origin is Tool tool) - { - return tool; - } - else if (origin is JsonNode jsonNode) - { - return JsonSerializer.Deserialize(jsonNode.ToJsonString()); - } - throw new ArgumentException($"Unsupported tool type: {origin.GetType()}"); - } - - /// Dummy Blobs transformer. - internal static JsonArray TBlobs(object origin) - { - JsonNode inputNode; - - if (origin is not JsonNode) - { - inputNode = JsonNode.Parse(JsonSerializer.Serialize(origin, JsonConfig.JsonSerializerOptions))!; - } - else - { - inputNode = (JsonNode)origin; - } - - if (inputNode is JsonArray existingArray) - { - return existingArray; - } - - JsonArray arrayNode = new JsonArray(); - arrayNode.Add(JsonNode.Parse(JsonSerializer.Serialize(TBlob(origin), JsonConfig.JsonSerializerOptions))); - return arrayNode; - } - - internal static Blob TBlob(object blob) - { - if (blob is JsonObject jsonObject) - { - blob = JsonSerializer.Deserialize(jsonObject.ToString()); - } - - if (blob is Blob b) - { - return b; - } - else - { - throw new ArgumentException($"Unsupported blob type: {blob.GetType()}"); - } - } - - /// - /// Transforms a blob to an image blob, validating its mime type. - /// - /// The object to transform, can be a Blob or a dictionary. - /// The transformed Blob if it is an image. - /// If the blob is not an image. - internal static Blob TImageBlob(object blob) - { - Blob transformedBlob = TBlob(blob); - if (!string.IsNullOrEmpty(transformedBlob.MimeType) - && transformedBlob.MimeType.StartsWith("image/")) - { - return transformedBlob; - } - throw new ArgumentException( - $"Unsupported mime type for image blob: {transformedBlob.MimeType ?? "null"}"); - } - - /// - /// Transforms a blob to an audio blob, validating its mime type. - /// - /// The object to transform, can be a Blob or a dictionary. - /// The transformed Blob if it is an audio. - /// If the blob is not an audio. - internal static Blob TAudioBlob(object blob) - { - Blob transformedBlob = TBlob(blob); - if (!string.IsNullOrEmpty(transformedBlob.MimeType) - && transformedBlob.MimeType.StartsWith("audio/")) - { - return transformedBlob; - } - throw new ArgumentException( - $"Unsupported mime type for audio blob: {transformedBlob.MimeType ?? "null"}"); - } - - /// Dummy bytes transformer. - internal static object TBytes(object origin) - { - // TODO(b/389133914): Remove dummy bytes converter. - return origin; - } - - /// Transforms a list models response object to a list of models. - internal static JsonArray TExtractModels(JsonNode models) - { - if (models == null) - { - return new JsonArray(); - } - if (models is JsonObject modelsObj) - { - if (modelsObj.ContainsKey("models")) - { - return (JsonArray)modelsObj["models"]!; - } - if (modelsObj.ContainsKey("tunedModels")) - { - return (JsonArray)modelsObj["tunedModels"]!; - } - if (modelsObj.ContainsKey("publisherModels")) - { - return (JsonArray)modelsObj["publisherModels"]!; - } - } - return new JsonArray(); - } - - /// Transforms an object to a cached content name for the API. - internal static string? TCachedContentName(ApiClient apiClient, object origin) - { - if (origin == null) - { - return null; - } - else if (origin is string strOrigin) - { - return GetResourceName(apiClient, strOrigin, "cachedContents"); - } - else if (origin is JsonNode jsonNode) - { - string cachedContentName = jsonNode.ToString(); - cachedContentName = cachedContentName.Replace("\"", ""); - return GetResourceName(apiClient, cachedContentName, "cachedContents"); - } - - throw new ArgumentException( - $"Unsupported cached content name type: {origin.GetType()}"); - } - - /// Transforms an object to a list of Content for the embedding API. - internal static List? TContentsForEmbed(ApiClient apiClient, object origin) - { - if (origin == null) - { - return null; - } - - List? contents; - if (origin is List contentList) - { - contents = contentList; - } - else if (origin is JsonNode jsonNode) - { - contents = JsonSerializer.Deserialize>(jsonNode.ToJsonString()); - } - else - { - throw new ArgumentException($"Unsupported contents type: {origin.GetType()}"); - } - - List result = new List(); - if (contents != null) - { - foreach (Content content in contents) - { - if (!apiClient.VertexAI) - { - result.Add(content); - } - else - { - if (content.Parts != null) - { - foreach (Part part in content.Parts) - { - if (part.Text != null) - { - result.Add(part.Text); - } - } - } - } - } - } - return result; - } - - /// - /// Transforms a model name to the correct format for the Caches API. - /// - /// The API client to use for transformation - /// The model name to transform, can be a string or JsonNode - /// The transformed model name, or null if the input is null - /// If the object is not a supported type - internal static string? TCachesModel(ApiClient apiClient, object origin) - { - string? model = TModel(apiClient, origin); - if (model == null) - { - return null; - } - - if (apiClient.VertexAI) - { - if (model.StartsWith("publishers/")) - { - // Vertex caches only support model names starting with projects. - return string.Format( - "projects/{0}/locations/{1}/{2}", apiClient.Project, apiClient.Location, model); - } - else if (model.StartsWith("models/")) - { - return string.Format( - "projects/{0}/locations/{1}/publishers/google/{2}", - apiClient.Project, apiClient.Location, model); - } - } - return model; - } - - internal static string? TFileName(object? origin) - { - string? name = null; - - if (origin is string strName) - { - name = strName; - } - else if (origin == null) - { - return null; - } - else if (origin is JsonNode jsonNode) - { - name = jsonNode.ToString(); - name = name.Replace("\"", ""); - } - else - { - throw new ArgumentException($"Unsupported file name type: {origin.GetType()}"); - } - - if (name.StartsWith("https://")) - { -#if NET - string suffix = name.Split("files/")[1]; -#else - string suffix = name.Split(new[] { "files/" }, StringSplitOptions.None)[1]; -#endif - Match match = Regex.Match(suffix, "[a-z0-9]+"); - if (match.Success) - { - name = match.Value; - } - else - { - throw new ArgumentException($"Could not extract file name from {name}"); - } - } - else if (name.StartsWith("files/")) - { -#if NET - name = name.Split("files/")[1]; -#else - name = name.Split(new[] { "files/" }, StringSplitOptions.None)[1]; -#endif - } - - return name; - } - - internal static bool TIsVertexEmbedContentModel(string model) - { - // Gemini Embeddings except gemini-embedding-001. - return (model.Contains("gemini") && model != "gemini-embedding-001") - // Open-source MaaS embedding models. - || model.Contains("maas"); - } - - /// Formats a resource name given the resource name and resource prefix. - internal static string GetResourceName( - ApiClient apiClient, string resourceName, string resourcePrefix) - { - if (apiClient.VertexAI) - { - if (resourceName.StartsWith("projects/")) - { - return resourceName; - } - else if (resourceName.StartsWith("locations/")) - { - return string.Format("projects/{0}/{1}", apiClient.Project, resourceName); - } - else if (resourceName.StartsWith(resourcePrefix + "/")) - { - return string.Format( - "projects/{0}/locations/{1}/{2}", apiClient.Project, apiClient.Location, resourceName); - } - else - { - return string.Format( - "projects/{0}/locations/{1}/{2}/{3}", - apiClient.Project, apiClient.Location, resourcePrefix, resourceName); - } - } - else - { - if (resourceName.StartsWith(resourcePrefix + "/")) - { - return resourceName; - } - else - { - return string.Format("{0}/{1}", resourcePrefix, resourceName); - } - } - } - - internal static JsonNode TTuningJobStatus(JsonNode origin) - { - string? status = origin.GetValue(); - switch (status) - { - case "ACTIVE": - return JsonValue.Create("JOB_STATE_SUCCEEDED")!; - case "CREATING": - return JsonValue.Create("JOB_STATE_RUNNING")!; - case "FAILED": - return JsonValue.Create("JOB_STATE_FAILED")!; - case "STATE_UNSPECIFIED": - return JsonValue.Create("JOB_STATE_UNSPECIFIED")!; - default: - return origin; - } - } - - internal static JsonNode TBatchJobName(ApiClient apiClient, JsonNode origin) - { - string nameStr = origin.ToString().Replace("\"", ""); - if (!apiClient.VertexAI) - { - Regex mldevRegex = new Regex(@"batches/[^/]+$"); - if (mldevRegex.IsMatch(nameStr)) - { - return JsonValue.Create(nameStr.Split('/').Last()); - } - else - { - throw new ArgumentException($"Invalid batch job name: {nameStr}."); - } - } - - Regex vertexRegex = new Regex(@"^projects/[^/]+/locations/[^/]+/batchPredictionJobs/[^/]+$"); - nameStr = GetResourceName(apiClient, nameStr, "batchPredictionJobs"); - if (vertexRegex.IsMatch(nameStr)) - { - return JsonValue.Create(nameStr.Split('/').Last()); - } - else if (nameStr.All(char.IsDigit)) - { - return JsonValue.Create(nameStr); - } - else - { - throw new ArgumentException($"Invalid batch job name: {nameStr}."); - } - } - - internal static JsonNode TBatchJobSource(JsonNode origin) - { - return origin; - } - - internal static JsonNode TBatchJobDestination(JsonNode origin) - { - return origin; - } - - internal static JsonNode TJobState(JsonNode origin) - { - string? stateStr = origin.GetValue(); - switch (stateStr) - { - case "BATCH_STATE_UNSPECIFIED": - return JsonValue.Create("JOB_STATE_UNSPECIFIED"); - case "BATCH_STATE_PENDING": - return JsonValue.Create("JOB_STATE_PENDING"); - case "BATCH_STATE_RUNNING": - return JsonValue.Create("JOB_STATE_RUNNING"); - case "BATCH_STATE_SUCCEEDED": - return JsonValue.Create("JOB_STATE_SUCCEEDED"); - case "BATCH_STATE_FAILED": - return JsonValue.Create("JOB_STATE_FAILED"); - case "BATCH_STATE_CANCELLED": - return JsonValue.Create("JOB_STATE_CANCELLED"); - case "BATCH_STATE_EXPIRED": - return JsonValue.Create("JOB_STATE_EXPIRED"); - default: - return origin; - } - } - - internal static JsonNode TRecvBatchJobDestination(JsonNode origin) - { - return origin; - } - - /// - /// Transforms a SpeechConfig object for the live API, validating it. - /// - /// The object to transform, can be a SpeechConfig or a JsonNode. - /// The transformed SpeechConfig. - /// If the object is not a supported type. - /// If multiSpeakerVoiceConfig is present (as it's not supported in the live API). - internal static SpeechConfig? TLiveSpeechConfig(object origin) - { - SpeechConfig? speechConfig; - if (origin == null) - { - return null; - } - else if (origin is SpeechConfig config) - { - speechConfig = config; - } - else if (origin is JsonNode jsonNode) - { - speechConfig = JsonSerializer.Deserialize(jsonNode.ToJsonString()); - } - else - { - throw new ArgumentException($"Unsupported speechConfig type: {origin.GetType()}"); - } - - if (speechConfig?.MultiSpeakerVoiceConfig != null) - { - throw new NotSupportedException("multiSpeakerVoiceConfig parameter is not supported in the live API."); - } - - return speechConfig; - } - } -} +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; + +using Google.GenAI.Types; + + +namespace Google.GenAI +{ + /// + /// Transformers for GenAI SDK. + /// + internal static class Transformers + { + /// + /// Transforms a model name to the correct format for the API. + /// + /// The API client to use for transformation. + /// The model name to transform, can only be a string. + /// The transformed model name. + /// If the object is not a supported type. + internal static string? TModel(ApiClient apiClient, object origin) + { + string? model; + if (origin == null) + { + return null; + } + else if (origin is string strModel) + { + model = strModel; + } + else if (origin is JsonNode jsonNode) + { + model = jsonNode.ToString(); + model = model.Replace("\"", ""); + } + else + { + throw new ArgumentException($"Unsupported model type: {origin.GetType()}"); + } + + if (model.Length == 0) + { + throw new ArgumentException("model is required."); + } + if (model.Contains("..") || model.Contains("?") || model.Contains("&")) + { + throw new ArgumentException("invalid model parameter."); + } + if (apiClient.VertexAI) + { + if (model.StartsWith("publishers/") + || model.StartsWith("projects/") + || model.StartsWith("models/")) + { + return model; + } + else if (model.Contains("/")) + { +#if NETSTANDARD2_0 + string[] parts = model.Split(new[] { '/' }, 2, StringSplitOptions.RemoveEmptyEntries); +#else + string[] parts = model.Split('/', 2, StringSplitOptions.RemoveEmptyEntries); +#endif + return string.Format("publishers/{0}/models/{1}", parts[0], parts[1]); + } + else + { + return "publishers/google/models/" + model; + } + } + else + { + if (model.StartsWith("models/") || model.StartsWith("tunedModels/")) + { + return model; + } + else + { + return "models/" + model; + } + } + } + + /// + /// Determines the appropriate models URL based on the API client type and whether base models are + /// requested. + /// + /// The API client to use for transformation. + /// True if base models are requested, false otherwise. + /// The transformed model name + internal static string TModelsUrl(ApiClient apiClient, object? baseModels) + { + bool queryBase = true; + if (baseModels is JsonValue val) + { + queryBase = val.GetValue(); + } + if (queryBase) + { + return apiClient.VertexAI ? "publishers/google/models" : "models"; + } + else + { + return apiClient.VertexAI ? "models" : "tunedModels"; + } + } + + /// + /// Transforms an object to a list of Content for the API. + /// + /// The object to transform, can be a string, Content, or List<Content> + /// The transformed list of Content + /// If the object is not a supported type + internal static List? TContents(object? contents) + { + if (contents == null) + { + return null; + } + if (contents is string contentString) + { + Content content = new Content(); + content.Role = "user"; + Part part = new Part(); + part.Text = contentString; + content.Parts = new List { part }; + return new List { content }; + } + else if (contents is Content singleContent) + { + return new List { singleContent }; + } + else if (contents is List contentList) + { + return contentList; + } + else if (contents is JsonObject jsonObject) + { + return JsonSerializer.Deserialize>(jsonObject.ToString(), JsonConfig.JsonSerializerOptions); + } + else if (contents is JsonNode jsonNode) + { + return JsonSerializer.Deserialize>(jsonNode.ToString(), JsonConfig.JsonSerializerOptions); + } + + throw new ArgumentException($"Unsupported contents type: {contents.GetType()}"); + } + + /// + /// Transforms an object to a Content for the API. + /// + /// The object to transform, can be a string or Content + /// The transformed Content + /// If the object is not a supported type + internal static Content? TContent(object content) + { + if (content == null) + { + return null; + } + else if (content is string contentString) + { + Content contentObject = new Content(); + contentObject.Role = "user"; + Part part = new Part(); + part.Text = contentString; + contentObject.Parts = new List { part }; + return contentObject; + } + else if (content is Content singleContent) + { + return singleContent; + } + else if (content is JsonObject jsonObject) + { + return JsonSerializer.Deserialize(jsonObject.ToString(), JsonConfig.JsonSerializerOptions); + } + + throw new ArgumentException($"Unsupported content type: {content.GetType()}"); + } + + /// Transforms an object to a Schema for the API. + /// If the object is not a supported type. + internal static Schema? TSchema(object origin) + { + if (origin == null) + { + return null; + } + else if (origin is Schema schema) + { + return schema; + } + else if (origin is JsonObject jsonObject) + { + return JsonSerializer.Deserialize(jsonObject.ToString(), JsonConfig.JsonSerializerOptions); + } + throw new ArgumentException($"Unsupported schema type: {origin.GetType()}"); + } + + internal static SpeechConfig? TSpeechConfig(object speechConfig) + { + if (speechConfig == null) + { + return null; + } + else if (speechConfig is string speechConfigString) + { + return null; + } + else if (speechConfig is SpeechConfig config) + { + return config; + } + else if (speechConfig is JsonObject jsonObject) + { + return JsonSerializer.Deserialize(jsonObject.ToString(), JsonConfig.JsonSerializerOptions); + } + + throw new ArgumentException($"Unsupported speechConfig type:{speechConfig.GetType()}"); + } + + /// Transforms an object to a list of Tools for the API. + /// If the object is not a supported type. + internal static List? TTools(object origin) + { + if (origin == null) + { + return null; + } + else if (origin is List tools) + { + List transformedTools = new List(); + foreach (Tool tool in tools) + { + transformedTools.Add(TTool(tool)!); + } + return transformedTools; + } + else if (origin is JsonArray jsonArray) + { + List toolList = new List(); + foreach(JsonNode? toolNode in jsonArray) + { + if(toolNode != null) + { + toolList.Add(TTool(toolNode)!); + } + } + return toolList; + } + else if (origin is JsonNode jsonNode) + { + return JsonSerializer.Deserialize>(jsonNode.ToJsonString(), JsonConfig.JsonSerializerOptions); + } + + throw new ArgumentException($"Unsupported tools type: {origin.GetType()}"); + } + + /// Transforms an object to a Tool for the API. + /// If the object is not a supported type. + internal static Tool? TTool(object origin) + { + if (origin == null) + { + return null; + } + else if (origin is Tool tool) + { + return tool; + } + else if (origin is JsonNode jsonNode) + { + return JsonSerializer.Deserialize(jsonNode.ToJsonString(), JsonConfig.JsonSerializerOptions); + } + throw new ArgumentException($"Unsupported tool type: {origin.GetType()}"); + } + + /// Dummy Blobs transformer. + internal static JsonArray TBlobs(object origin) + { + JsonNode inputNode; + + if (origin is not JsonNode) + { + inputNode = JsonNode.Parse(JsonSerializer.Serialize(origin, JsonConfig.JsonSerializerOptions))!; + } + else + { + inputNode = (JsonNode)origin; + } + + if (inputNode is JsonArray existingArray) + { + return existingArray; + } + + JsonArray arrayNode = new JsonArray(); + arrayNode.Add(JsonNode.Parse(JsonSerializer.Serialize(TBlob(origin), JsonConfig.InternalSerializerOptions))); + return arrayNode; + } + + internal static Blob TBlob(object blob) + { + if (blob is JsonObject jsonObject) + { + blob = JsonSerializer.Deserialize(jsonObject.ToString(), JsonConfig.JsonSerializerOptions); + } + + if (blob is Blob b) + { + return b; + } + else + { + throw new ArgumentException($"Unsupported blob type: {blob.GetType()}"); + } + } + + /// + /// Transforms a blob to an image blob, validating its mime type. + /// + /// The object to transform, can be a Blob or a dictionary. + /// The transformed Blob if it is an image. + /// If the blob is not an image. + internal static Blob TImageBlob(object blob) + { + Blob transformedBlob = TBlob(blob); + if (!string.IsNullOrEmpty(transformedBlob.MimeType) + && transformedBlob.MimeType.StartsWith("image/")) + { + return transformedBlob; + } + throw new ArgumentException( + $"Unsupported mime type for image blob: {transformedBlob.MimeType ?? "null"}"); + } + + /// + /// Transforms a blob to an audio blob, validating its mime type. + /// + /// The object to transform, can be a Blob or a dictionary. + /// The transformed Blob if it is an audio. + /// If the blob is not an audio. + internal static Blob TAudioBlob(object blob) + { + Blob transformedBlob = TBlob(blob); + if (!string.IsNullOrEmpty(transformedBlob.MimeType) + && transformedBlob.MimeType.StartsWith("audio/")) + { + return transformedBlob; + } + throw new ArgumentException( + $"Unsupported mime type for audio blob: {transformedBlob.MimeType ?? "null"}"); + } + + /// Dummy bytes transformer. + internal static object TBytes(object origin) + { + // TODO(b/389133914): Remove dummy bytes converter. + return origin; + } + + /// Transforms a list models response object to a list of models. + internal static JsonArray TExtractModels(JsonNode models) + { + if (models == null) + { + return new JsonArray(); + } + if (models is JsonObject modelsObj) + { + if (modelsObj.ContainsKey("models")) + { + return (JsonArray)modelsObj["models"]!; + } + if (modelsObj.ContainsKey("tunedModels")) + { + return (JsonArray)modelsObj["tunedModels"]!; + } + if (modelsObj.ContainsKey("publisherModels")) + { + return (JsonArray)modelsObj["publisherModels"]!; + } + } + return new JsonArray(); + } + + /// Transforms an object to a cached content name for the API. + internal static string? TCachedContentName(ApiClient apiClient, object origin) + { + if (origin == null) + { + return null; + } + else if (origin is string strOrigin) + { + return GetResourceName(apiClient, strOrigin, "cachedContents"); + } + else if (origin is JsonNode jsonNode) + { + string cachedContentName = jsonNode.ToString(); + cachedContentName = cachedContentName.Replace("\"", ""); + return GetResourceName(apiClient, cachedContentName, "cachedContents"); + } + + throw new ArgumentException( + $"Unsupported cached content name type: {origin.GetType()}"); + } + + /// Transforms an object to a list of Content for the embedding API. + internal static List? TContentsForEmbed(ApiClient apiClient, object origin) + { + if (origin == null) + { + return null; + } + + List? contents; + if (origin is List contentList) + { + contents = contentList; + } + else if (origin is JsonNode jsonNode) + { + contents = JsonSerializer.Deserialize>(jsonNode.ToJsonString(), JsonConfig.JsonSerializerOptions); + } + else + { + throw new ArgumentException($"Unsupported contents type: {origin.GetType()}"); + } + + List result = new List(); + if (contents != null) + { + foreach (Content content in contents) + { + if (!apiClient.VertexAI) + { + result.Add(content); + } + else + { + if (content.Parts != null) + { + foreach (Part part in content.Parts) + { + if (part.Text != null) + { + result.Add(part.Text); + } + } + } + } + } + } + return result; + } + + /// + /// Transforms a model name to the correct format for the Caches API. + /// + /// The API client to use for transformation + /// The model name to transform, can be a string or JsonNode + /// The transformed model name, or null if the input is null + /// If the object is not a supported type + internal static string? TCachesModel(ApiClient apiClient, object origin) + { + string? model = TModel(apiClient, origin); + if (model == null) + { + return null; + } + + if (apiClient.VertexAI) + { + if (model.StartsWith("publishers/")) + { + // Vertex caches only support model names starting with projects. + return string.Format( + "projects/{0}/locations/{1}/{2}", apiClient.Project, apiClient.Location, model); + } + else if (model.StartsWith("models/")) + { + return string.Format( + "projects/{0}/locations/{1}/publishers/google/{2}", + apiClient.Project, apiClient.Location, model); + } + } + return model; + } + + internal static string? TFileName(object? origin) + { + string? name = null; + + if (origin is string strName) + { + name = strName; + } + else if (origin == null) + { + return null; + } + else if (origin is JsonNode jsonNode) + { + name = jsonNode.ToString(); + name = name.Replace("\"", ""); + } + else + { + throw new ArgumentException($"Unsupported file name type: {origin.GetType()}"); + } + + if (name.StartsWith("https://")) + { +#if NET + string suffix = name.Split("files/")[1]; +#else + string suffix = name.Split(new[] { "files/" }, StringSplitOptions.None)[1]; +#endif + Match match = Regex.Match(suffix, "[a-z0-9]+"); + if (match.Success) + { + name = match.Value; + } + else + { + throw new ArgumentException($"Could not extract file name from {name}"); + } + } + else if (name.StartsWith("files/")) + { +#if NET + name = name.Split("files/")[1]; +#else + name = name.Split(new[] { "files/" }, StringSplitOptions.None)[1]; +#endif + } + + return name; + } + + internal static bool TIsVertexEmbedContentModel(string model) + { + // Gemini Embeddings except gemini-embedding-001. + return (model.Contains("gemini") && model != "gemini-embedding-001") + // Open-source MaaS embedding models. + || model.Contains("maas"); + } + + /// Formats a resource name given the resource name and resource prefix. + internal static string GetResourceName( + ApiClient apiClient, string resourceName, string resourcePrefix) + { + if (apiClient.VertexAI) + { + if (resourceName.StartsWith("projects/")) + { + return resourceName; + } + else if (resourceName.StartsWith("locations/")) + { + return string.Format("projects/{0}/{1}", apiClient.Project, resourceName); + } + else if (resourceName.StartsWith(resourcePrefix + "/")) + { + return string.Format( + "projects/{0}/locations/{1}/{2}", apiClient.Project, apiClient.Location, resourceName); + } + else + { + return string.Format( + "projects/{0}/locations/{1}/{2}/{3}", + apiClient.Project, apiClient.Location, resourcePrefix, resourceName); + } + } + else + { + if (resourceName.StartsWith(resourcePrefix + "/")) + { + return resourceName; + } + else + { + return string.Format("{0}/{1}", resourcePrefix, resourceName); + } + } + } + + internal static JsonNode TTuningJobStatus(JsonNode origin) + { + string? status = origin.GetValue(); + switch (status) + { + case "ACTIVE": + return JsonValue.Create("JOB_STATE_SUCCEEDED")!; + case "CREATING": + return JsonValue.Create("JOB_STATE_RUNNING")!; + case "FAILED": + return JsonValue.Create("JOB_STATE_FAILED")!; + case "STATE_UNSPECIFIED": + return JsonValue.Create("JOB_STATE_UNSPECIFIED")!; + default: + return origin; + } + } + + internal static JsonNode TBatchJobName(ApiClient apiClient, JsonNode origin) + { + string nameStr = origin.ToString().Replace("\"", ""); + if (!apiClient.VertexAI) + { + Regex mldevRegex = new Regex(@"batches/[^/]+$"); + if (mldevRegex.IsMatch(nameStr)) + { + return JsonValue.Create(nameStr.Split('/').Last()); + } + else + { + throw new ArgumentException($"Invalid batch job name: {nameStr}."); + } + } + + Regex vertexRegex = new Regex(@"^projects/[^/]+/locations/[^/]+/batchPredictionJobs/[^/]+$"); + nameStr = GetResourceName(apiClient, nameStr, "batchPredictionJobs"); + if (vertexRegex.IsMatch(nameStr)) + { + return JsonValue.Create(nameStr.Split('/').Last()); + } + else if (nameStr.All(char.IsDigit)) + { + return JsonValue.Create(nameStr); + } + else + { + throw new ArgumentException($"Invalid batch job name: {nameStr}."); + } + } + + internal static JsonNode TBatchJobSource(JsonNode origin) + { + return origin; + } + + internal static JsonNode TBatchJobDestination(JsonNode origin) + { + return origin; + } + + internal static JsonNode TJobState(JsonNode origin) + { + string? stateStr = origin.GetValue(); + switch (stateStr) + { + case "BATCH_STATE_UNSPECIFIED": + return JsonValue.Create("JOB_STATE_UNSPECIFIED"); + case "BATCH_STATE_PENDING": + return JsonValue.Create("JOB_STATE_PENDING"); + case "BATCH_STATE_RUNNING": + return JsonValue.Create("JOB_STATE_RUNNING"); + case "BATCH_STATE_SUCCEEDED": + return JsonValue.Create("JOB_STATE_SUCCEEDED"); + case "BATCH_STATE_FAILED": + return JsonValue.Create("JOB_STATE_FAILED"); + case "BATCH_STATE_CANCELLED": + return JsonValue.Create("JOB_STATE_CANCELLED"); + case "BATCH_STATE_EXPIRED": + return JsonValue.Create("JOB_STATE_EXPIRED"); + default: + return origin; + } + } + + internal static JsonNode TRecvBatchJobDestination(JsonNode origin) + { + return origin; + } + + /// + /// Transforms a SpeechConfig object for the live API, validating it. + /// + /// The object to transform, can be a SpeechConfig or a JsonNode. + /// The transformed SpeechConfig. + /// If the object is not a supported type. + /// If multiSpeakerVoiceConfig is present (as it's not supported in the live API). + internal static SpeechConfig? TLiveSpeechConfig(object origin) + { + SpeechConfig? speechConfig; + if (origin == null) + { + return null; + } + else if (origin is SpeechConfig config) + { + speechConfig = config; + } + else if (origin is JsonNode jsonNode) + { + speechConfig = JsonSerializer.Deserialize(jsonNode.ToJsonString(), JsonConfig.JsonSerializerOptions); + } + else + { + throw new ArgumentException($"Unsupported speechConfig type: {origin.GetType()}"); + } + + if (speechConfig?.MultiSpeakerVoiceConfig != null) + { + throw new NotSupportedException("multiSpeakerVoiceConfig parameter is not supported in the live API."); + } + + return speechConfig; + } + } +} diff --git a/Google.GenAI/packages.lock.json b/Google.GenAI/packages.lock.json index 59d68c6c..75ae053f 100644 --- a/Google.GenAI/packages.lock.json +++ b/Google.GenAI/packages.lock.json @@ -1,273 +1,240 @@ -{ - "version": 2, - "dependencies": { - ".NETStandard,Version=v2.0": { - "Google.Apis.Auth": { - "type": "Direct", - "requested": "[1.69.0, )", - "resolved": "1.69.0", - "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", - "dependencies": { - "Google.Apis": "1.69.0", - "Google.Apis.Core": "1.69.0", - "System.Management": "7.0.2" - } - }, - "Microsoft.Extensions.AI.Abstractions": { - "type": "Direct", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "t3S2H4do4YeNheIfE3GEl3MnKIrnxpbLu7a88spfApYR3in9ddhIq/GMtxgMaFjn/PUMTCFv5YH7Y6Q91dsDXQ==", - "dependencies": { - "System.Text.Json": "10.0.4" - } - }, - "MimeTypes": { - "type": "Direct", - "requested": "[2.5.2, )", - "resolved": "2.5.2", - "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" - }, - "NETStandard.Library": { - "type": "Direct", - "requested": "[2.0.3, )", - "resolved": "2.0.3", - "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0" - } - }, - "System.Collections.Immutable": { - "type": "Direct", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==", - "dependencies": { - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.Net.ServerSentEvents": { - "type": "Direct", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "VTWjeyx9nPb4+hkjGcAaDw1nOckypMtvABmxSWm6PPYwrXoIiVG3jwtNlAGhaGVjDkBrERABox67wYTAcHxg7Q==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "9.0.0", - "System.Memory": "4.5.5", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Google.Apis": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", - "dependencies": { - "Google.Apis.Core": "1.69.0" - } - }, - "Google.Apis.Core": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", - "dependencies": { - "Newtonsoft.Json": "13.0.3" - } - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.6.1", - "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==", - "dependencies": { - "System.Buffers": "4.6.1", - "System.Memory": "4.6.3", - "System.Threading.Tasks.Extensions": "4.6.3" - } - }, - "System.Management": { - "type": "Transitive", - "resolved": "7.0.2", - "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", - "dependencies": { - "System.CodeDom": "7.0.0" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.6.3", - "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", - "dependencies": { - "System.Buffers": "4.6.1", - "System.Numerics.Vectors": "4.6.1", - "System.Runtime.CompilerServices.Unsafe": "6.1.2" - } - }, - "System.Numerics.Vectors": { - "type": "Transitive", - "resolved": "4.6.1", - "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.1.2", - "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==", - "dependencies": { - "System.Buffers": "4.6.1", - "System.Memory": "4.6.3", - "System.Runtime.CompilerServices.Unsafe": "6.1.2" - } - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.6.3", - "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.1.2" - } - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "2JmoSZ1wDf1/TUyTtLTXeicXCnWxXkeStGnzRRmAw+5CBIGhg6q9ieJXu4FjeLzawSGd5PMhcropNa3lPJDaKA==", - "dependencies": { - "System.Threading.Tasks.Extensions": "4.6.3" - } - }, - "System.Text.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "10.0.4", - "System.Buffers": "4.6.1", - "System.IO.Pipelines": "10.0.4", - "System.Memory": "4.6.3", - "System.Runtime.CompilerServices.Unsafe": "6.1.2", - "System.Text.Encodings.Web": "10.0.4", - "System.Threading.Tasks.Extensions": "4.6.3" - } - } - }, - "net8.0": { - "Google.Apis.Auth": { - "type": "Direct", - "requested": "[1.69.0, )", - "resolved": "1.69.0", - "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", - "dependencies": { - "Google.Apis": "1.69.0", - "Google.Apis.Core": "1.69.0", - "System.Management": "7.0.2" - } - }, - "Microsoft.Extensions.AI.Abstractions": { - "type": "Direct", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "t3S2H4do4YeNheIfE3GEl3MnKIrnxpbLu7a88spfApYR3in9ddhIq/GMtxgMaFjn/PUMTCFv5YH7Y6Q91dsDXQ==", - "dependencies": { - "System.Text.Json": "10.0.4" - } - }, - "MimeTypes": { - "type": "Direct", - "requested": "[2.5.2, )", - "resolved": "2.5.2", - "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" - }, - "System.Collections.Immutable": { - "type": "Direct", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==" - }, - "System.Net.ServerSentEvents": { - "type": "Direct", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "VTWjeyx9nPb4+hkjGcAaDw1nOckypMtvABmxSWm6PPYwrXoIiVG3jwtNlAGhaGVjDkBrERABox67wYTAcHxg7Q==" - }, - "Google.Apis": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", - "dependencies": { - "Google.Apis.Core": "1.69.0" - } - }, - "Google.Apis.Core": { - "type": "Transitive", - "resolved": "1.69.0", - "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", - "dependencies": { - "Newtonsoft.Json": "13.0.3" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" - }, - "System.Management": { - "type": "Transitive", - "resolved": "7.0.2", - "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", - "dependencies": { - "System.CodeDom": "7.0.0" - } - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" - }, - "System.Text.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", - "dependencies": { - "System.IO.Pipelines": "10.0.4", - "System.Text.Encodings.Web": "10.0.4" - } - } - } - } +{ + "version": 2, + "dependencies": { + ".NETStandard,Version=v2.0": { + "Google.Apis.Auth": { + "type": "Direct", + "requested": "[1.69.0, )", + "resolved": "1.69.0", + "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", + "dependencies": { + "Google.Apis": "1.69.0", + "Google.Apis.Core": "1.69.0", + "System.Management": "7.0.2" + } + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "Direct", + "requested": "[10.4.1, )", + "resolved": "10.4.1", + "contentHash": "ZxhU/wg9BOc3ohibhLl18toPLWm96ysQoE+3OhCgrZ0TUPZd7bsUmGteeatz08yweyuPIEhtyUzEZTF+3bMWEQ==", + "dependencies": { + "System.Text.Json": "10.0.4" + } + }, + "MimeTypes": { + "type": "Direct", + "requested": "[2.5.2, )", + "resolved": "2.5.2", + "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" + }, + "NETStandard.Library": { + "type": "Direct", + "requested": "[2.0.3, )", + "resolved": "2.0.3", + "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "System.Text.Json": { + "type": "Direct", + "requested": "[10.0.4, )", + "resolved": "10.0.4", + "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "10.0.4", + "System.Buffers": "4.6.1", + "System.IO.Pipelines": "10.0.4", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2", + "System.Text.Encodings.Web": "10.0.4", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "Google.Apis": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", + "dependencies": { + "Google.Apis.Core": "1.69.0" + } + }, + "Google.Apis.Core": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "System.Management": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", + "dependencies": { + "System.CodeDom": "7.0.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Numerics.Vectors": "4.6.1", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.4", + "contentHash": "2JmoSZ1wDf1/TUyTtLTXeicXCnWxXkeStGnzRRmAw+5CBIGhg6q9ieJXu4FjeLzawSGd5PMhcropNa3lPJDaKA==", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.6.3" + } + } + }, + "net8.0": { + "Google.Apis.Auth": { + "type": "Direct", + "requested": "[1.69.0, )", + "resolved": "1.69.0", + "contentHash": "ar07yxn/s41jdqQ3sMh8EAehiSvXQ9yE1MS4McmZINeSWvolnLHmIZ9Yxj4tHVIYYz0c7H/lpToVqm7C2aYx9g==", + "dependencies": { + "Google.Apis": "1.69.0", + "Google.Apis.Core": "1.69.0", + "System.Management": "7.0.2" + } + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "Direct", + "requested": "[10.4.1, )", + "resolved": "10.4.1", + "contentHash": "ZxhU/wg9BOc3ohibhLl18toPLWm96ysQoE+3OhCgrZ0TUPZd7bsUmGteeatz08yweyuPIEhtyUzEZTF+3bMWEQ==", + "dependencies": { + "System.Text.Json": "10.0.4" + } + }, + "MimeTypes": { + "type": "Direct", + "requested": "[2.5.2, )", + "resolved": "2.5.2", + "contentHash": "vm4xrNt+i6OVRQ8vhfCcmDIUg3qvjyCTkSTNVTDFohsG6CXEpMaVFkidECL6yRYpHDnz4TqXhDoEQAcnHCu/tw==" + }, + "System.Text.Json": { + "type": "Direct", + "requested": "[10.0.4, )", + "resolved": "10.0.4", + "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", + "dependencies": { + "System.IO.Pipelines": "10.0.4", + "System.Text.Encodings.Web": "10.0.4" + } + }, + "Google.Apis": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "1TfjsXFejwIf7iWaE7A0FbnOEsk8FPlbdFAt1r+I8aSMQfLLdSVWCLdZz6TzuWVwoCGEuJUHTZ/FXdptdU3qWw==", + "dependencies": { + "Google.Apis.Core": "1.69.0" + } + }, + "Google.Apis.Core": { + "type": "Transitive", + "resolved": "1.69.0", + "contentHash": "SXUcurNUPxYMtOnawvB2Av18VrPBC9W7So9q9ikmXIXLGiv4RX7Zbu4kc+8PbwTdd8wLt54r0PBGOT5RaKoTjQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==", + "dependencies": { + "System.CodeDom": "7.0.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==" + } + } + } } \ No newline at end of file diff --git a/local-packages/Google.GenAI.1.0.0-local.nupkg b/local-packages/Google.GenAI.1.0.0-local.nupkg new file mode 100644 index 00000000..453e9418 Binary files /dev/null and b/local-packages/Google.GenAI.1.0.0-local.nupkg differ diff --git a/nuget.config b/nuget.config new file mode 100644 index 00000000..95e879ef --- /dev/null +++ b/nuget.config @@ -0,0 +1,6 @@ + + + + + +