diff --git a/sdk/cs/samples/TestApp/Program.cs b/sdk/cs/samples/TestApp/Program.cs index 0aee181f..8d3ccca1 100644 --- a/sdk/cs/samples/TestApp/Program.cs +++ b/sdk/cs/samples/TestApp/Program.cs @@ -15,7 +15,7 @@ public class TestApp { public static async Task Main(string[] args) { - var app = new TestApp(); // Create an instance of TestApp + var app = new TestApp(); // Create an instance of TestApp Console.WriteLine(new string('=', 80)); // Separator for clarity Console.WriteLine("Testing catalog integration..."); @@ -84,7 +84,7 @@ private async Task TestService() } private async Task TestCatalog() - // First test catalog listing + // First test catalog listing { using var manager = new FoundryLocalManager(); foreach (var m in await manager.ListCatalogModelsAsync()) diff --git a/sdk/cs/src/FoundryLocalManager.cs b/sdk/cs/src/FoundryLocalManager.cs index b1a817d0..7e681794 100644 --- a/sdk/cs/src/FoundryLocalManager.cs +++ b/sdk/cs/src/FoundryLocalManager.cs @@ -13,6 +13,7 @@ namespace Microsoft.AI.Foundry.Local; using System.Linq; using System.Net.Http.Json; using System.Net.Mime; +using System.Reflection; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; @@ -47,6 +48,7 @@ public partial class FoundryLocalManager : IDisposable, IAsyncDisposable private List? _catalogModels; private Dictionary? _catalogDictionary; private readonly Dictionary _priorityMap; + private static readonly string AssemblyVersion = typeof(FoundryLocalManager).Assembly.GetName().Version?.ToString() ?? "unknown"; // Gets the service URI public Uri ServiceUri => _serviceUri ?? throw new InvalidOperationException("Service URI is not set. Call StartServiceAsync() first."); @@ -111,6 +113,8 @@ public async Task StartServiceAsync(CancellationToken ct = default) // set the timeout to 2 hours (for downloading large models) Timeout = TimeSpan.FromSeconds(7200) }; + + _serviceClient.DefaultRequestHeaders.UserAgent.ParseAdd($"foundry-local-cs-sdk/{AssemblyVersion}"); } } diff --git a/sdk/js/package-lock.json b/sdk/js/package-lock.json index bfd3da47..f96952fb 100644 --- a/sdk/js/package-lock.json +++ b/sdk/js/package-lock.json @@ -20,6 +20,7 @@ "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jsdoc": "^46.8.2", + "genversion": "^3.2.0", "prettier": "^3.2.4", "typescript": "^5.2.2", "unbuild": "^3.5.0", @@ -1756,6 +1757,12 @@ "node": ">=12" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -2581,6 +2588,21 @@ "node": ">= 0.4" } }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.154", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.154.tgz", @@ -3266,6 +3288,27 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3279,6 +3322,15 @@ "node": ">=8" } }, + "node_modules/find-package": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-package/-/find-package-1.0.0.tgz", + "integrity": "sha512-yVn71XCCaNgxz58ERTl8nA/8YYtIQDY9mHSrgFBfiFtdNNfY0h183Vh8BRkKxD8x9TUw3ec290uJKhDVxqGZBw==", + "dev": true, + "dependencies": { + "parents": "^1.0.1" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3423,6 +3475,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/genversion": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/genversion/-/genversion-3.2.0.tgz", + "integrity": "sha512-OIYSX6XYA8PHecLDCTri30hadSZfAjZ8Iq1+BBDXqLWP4dRLuJNLoNjsSWtTpw97IccK2LDWzkEstxAB8GdN7g==", + "dev": true, + "dependencies": { + "commander": "^7.2.0", + "ejs": "^3.1.9", + "find-package": "^1.0.0" + }, + "bin": { + "genversion": "bin/genversion.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4220,6 +4289,46 @@ "dev": true, "license": "ISC" }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -4802,6 +4911,15 @@ "node": ">=6" } }, + "node_modules/parents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha512-mXKF3xkoUt5td2DoxpLmtOmZvko9VfFpwRwkKDHSNvgmpLAeBo18YDhcPbBzJq+QLCHMbGOfzia2cX4U+0v9Mg==", + "dev": true, + "dependencies": { + "path-platform": "~0.11.15" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4839,6 +4957,15 @@ "dev": true, "license": "MIT" }, + "node_modules/path-platform": { + "version": "0.11.15", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", diff --git a/sdk/js/package.json b/sdk/js/package.json index 3bb2f510..0e04daf3 100644 --- a/sdk/js/package.json +++ b/sdk/js/package.json @@ -12,7 +12,7 @@ "types": "dist/index.d.ts", "license": "MIT", "scripts": { - "build": "unbuild", + "build": "npx genversion -e src/version.ts && unbuild", "format": "prettier --write .", "format:check": "prettier --check .", "lint": "eslint ./src/*", @@ -42,6 +42,7 @@ "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jsdoc": "^46.8.2", + "genversion": "^3.2.0", "prettier": "^3.2.4", "typescript": "^5.2.2", "unbuild": "^3.5.0", diff --git a/sdk/js/src/client.ts b/sdk/js/src/client.ts index 60740751..9883191d 100644 --- a/sdk/js/src/client.ts +++ b/sdk/js/src/client.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { Fetch } from './types.js' +import { version } from './version.js' /** * Handles fetch requests with error handling. @@ -71,7 +72,7 @@ export const postWithProgress = async ( // Sending a POST request and getting a streamable response const response = await fetchWithErrorHandling(fetch, host, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'User-Agent': `foundry-local-js-sdk/${version}` }, body: body ? JSON.stringify(body) : undefined, }) diff --git a/sdk/js/src/version.ts b/sdk/js/src/version.ts new file mode 100644 index 00000000..5e7afbbe --- /dev/null +++ b/sdk/js/src/version.ts @@ -0,0 +1,2 @@ +// Generated by genversion. +export const version = '0.4.0' diff --git a/sdk/js/test/client.test.ts b/sdk/js/test/client.test.ts index 5481571a..bcfc2eab 100644 --- a/sdk/js/test/client.test.ts +++ b/sdk/js/test/client.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest' import * as client from '../src/client' import type { Fetch } from '../src/types' +import { version } from '../src/version' describe('Client', () => { const mockFetch = vi.fn() as unknown as Fetch @@ -60,7 +61,10 @@ describe('Client', () => { expect(result).toEqual({ ok: true }) expect(mockFetch).toHaveBeenCalledWith('http://example.com', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + 'User-Agent': `foundry-local-js-sdk/${version}`, + }, body: JSON.stringify(body), }) }) diff --git a/sdk/python/foundry_local/__init__.py b/sdk/python/foundry_local/__init__.py index b3681488..2b37eb80 100644 --- a/sdk/python/foundry_local/__init__.py +++ b/sdk/python/foundry_local/__init__.py @@ -19,5 +19,3 @@ _logger.propagate = False __all__ = ["FoundryLocalManager"] - -__version__ = "0.4.0" diff --git a/sdk/python/foundry_local/client.py b/sdk/python/foundry_local/client.py index 5d21ce59..312fd06c 100644 --- a/sdk/python/foundry_local/client.py +++ b/sdk/python/foundry_local/client.py @@ -11,6 +11,8 @@ import httpx from tqdm import tqdm +from foundry_local.version import __version__ as sdk_version + logger = logging.getLogger(__name__) @@ -34,7 +36,8 @@ def __init__(self, host: str, timeout: float | httpx.Timeout | None = None) -> N host (str): Base URL of the host. timeout (float | httpx.Timeout | None): Timeout for the HTTP client. """ - self._client = httpx.Client(base_url=host, timeout=timeout) + headers = {"user-agent": f"foundry-local-python-sdk/{sdk_version}"} + self._client = httpx.Client(base_url=host, timeout=timeout, headers=headers) def _request(self, *args, **kwargs) -> httpx.Response: """ diff --git a/sdk/python/foundry_local/version.py b/sdk/python/foundry_local/version.py new file mode 100644 index 00000000..6a9beea8 --- /dev/null +++ b/sdk/python/foundry_local/version.py @@ -0,0 +1 @@ +__version__ = "0.4.0" diff --git a/sdk/python/test/test_api.py b/sdk/python/test/test_api.py index a301a2fc..576e56b7 100644 --- a/sdk/python/test/test_api.py +++ b/sdk/python/test/test_api.py @@ -6,6 +6,7 @@ from unittest import mock import pytest + from foundry_local.api import FoundryLocalManager from foundry_local.client import HttpResponseError from foundry_local.models import FoundryModelInfo diff --git a/sdk/python/test/test_client.py b/sdk/python/test/test_client.py index 03317a2d..28fb8ca8 100644 --- a/sdk/python/test/test_client.py +++ b/sdk/python/test/test_client.py @@ -6,18 +6,28 @@ import httpx import pytest + from foundry_local.client import HttpResponseError, HttpxClient +from foundry_local.version import __version__ as sdk_version def test_initialization(): """Test initialization of HttpxClient.""" with mock.patch("httpx.Client") as mock_client: HttpxClient("http://localhost:5273") - mock_client.assert_called_once_with(base_url="http://localhost:5273", timeout=None) + mock_client.assert_called_once_with( + base_url="http://localhost:5273", + timeout=None, + headers={"user-agent": f"foundry-local-python-sdk/{sdk_version}"}, + ) # Test with timeout HttpxClient("http://localhost:5273", timeout=30.0) - mock_client.assert_called_with(base_url="http://localhost:5273", timeout=30.0) + mock_client.assert_called_with( + base_url="http://localhost:5273", + timeout=30.0, + headers={"user-agent": f"foundry-local-python-sdk/{sdk_version}"}, + ) # pylint: disable=protected-access diff --git a/sdk/python/test/test_service.py b/sdk/python/test/test_service.py index 397b369f..2701205b 100644 --- a/sdk/python/test/test_service.py +++ b/sdk/python/test/test_service.py @@ -5,6 +5,7 @@ from unittest import mock import pytest + from foundry_local.service import assert_foundry_installed, get_service_uri, start_service diff --git a/sdk/rust/src/client.rs b/sdk/rust/src/client.rs index 8779d4f4..68684fae 100644 --- a/sdk/rust/src/client.rs +++ b/sdk/rust/src/client.rs @@ -46,7 +46,9 @@ impl HttpClient { /// A new HttpClient instance. pub fn new(host: &str, timeout_secs: Option) -> Self { let timeout = timeout_secs.map(Duration::from_secs); - let mut client_builder = Client::builder().user_agent("foundry-local-rust-sdk/0.2.0"); + const VERSION: &str = env!("CARGO_PKG_VERSION"); + let mut client_builder = + Client::builder().user_agent(&format!("foundry-local-rust-sdk/{}", VERSION)); if let Some(timeout) = timeout { client_builder = client_builder.timeout(timeout);