diff --git a/.pipelines/templates/test-cs-steps.yml b/.pipelines/templates/test-cs-steps.yml index 92c9b6ee..32ce661c 100644 --- a/.pipelines/templates/test-cs-steps.yml +++ b/.pipelines/templates/test-cs-steps.yml @@ -20,6 +20,7 @@ steps: $testDataDir = "$(Build.SourcesDirectory)/test-data-shared" Write-Host "##vso[task.setvariable variable=repoRoot]$repoRoot" Write-Host "##vso[task.setvariable variable=testDataDir]$testDataDir" + Write-Host "##vso[task.setvariable variable=FOUNDRY_TESTING_MODE]1" - task: UseDotNet@2 displayName: 'Use .NET 9 SDK' diff --git a/.pipelines/templates/test-js-steps.yml b/.pipelines/templates/test-js-steps.yml index 1814626a..70e2a16b 100644 --- a/.pipelines/templates/test-js-steps.yml +++ b/.pipelines/templates/test-js-steps.yml @@ -20,6 +20,7 @@ steps: $testDataDir = "$(Build.SourcesDirectory)/test-data-shared" Write-Host "##vso[task.setvariable variable=repoRoot]$repoRoot" Write-Host "##vso[task.setvariable variable=testDataDir]$testDataDir" + Write-Host "##vso[task.setvariable variable=FOUNDRY_TESTING_MODE]1" - ${{ if eq(parameters.isWinML, true) }}: - task: PowerShell@2 diff --git a/.pipelines/templates/test-python-steps.yml b/.pipelines/templates/test-python-steps.yml index 1de20b1c..c177efde 100644 --- a/.pipelines/templates/test-python-steps.yml +++ b/.pipelines/templates/test-python-steps.yml @@ -20,6 +20,7 @@ steps: $testDataDir = "$(Build.SourcesDirectory)/test-data-shared" Write-Host "##vso[task.setvariable variable=repoRoot]$repoRoot" Write-Host "##vso[task.setvariable variable=testDataDir]$testDataDir" + Write-Host "##vso[task.setvariable variable=FOUNDRY_TESTING_MODE]1" - ${{ if eq(parameters.isWinML, true) }}: - task: PowerShell@2 diff --git a/.pipelines/templates/test-rust-steps.yml b/.pipelines/templates/test-rust-steps.yml index 31bfd75e..40b36a23 100644 --- a/.pipelines/templates/test-rust-steps.yml +++ b/.pipelines/templates/test-rust-steps.yml @@ -18,6 +18,7 @@ steps: $testDataDir = "$(Build.SourcesDirectory)/test-data-shared" Write-Host "##vso[task.setvariable variable=repoRoot]$repoRoot" Write-Host "##vso[task.setvariable variable=testDataDir]$testDataDir" + Write-Host "##vso[task.setvariable variable=FOUNDRY_TESTING_MODE]1" - ${{ if eq(parameters.isWinML, true) }}: - task: PowerShell@2 diff --git a/samples/cs/tool-calling-foundry-local-sdk/Program.cs b/samples/cs/tool-calling-foundry-local-sdk/Program.cs index 2a568330..a4074233 100644 --- a/samples/cs/tool-calling-foundry-local-sdk/Program.cs +++ b/samples/cs/tool-calling-foundry-local-sdk/Program.cs @@ -141,6 +141,7 @@ await model.DownloadAsync(progress => var response = new ChatMessage { Role = "tool", + ToolCallId = chunk!.Choices[0].Message.ToolCalls![0].Id, Content = result.ToString(), }; messages.Add(response); diff --git a/samples/js/chat-and-audio-foundry-local/src/app.js b/samples/js/chat-and-audio-foundry-local/src/app.js index 87845aa6..12ddabb9 100644 --- a/samples/js/chat-and-audio-foundry-local/src/app.js +++ b/samples/js/chat-and-audio-foundry-local/src/app.js @@ -95,7 +95,7 @@ async function main() { }, { role: "user", content: transcription.text }, ])) { - const content = chunk.choices?.[0]?.message?.content; + const content = chunk.choices?.[0]?.delta?.content; if (content) { process.stdout.write(content); } diff --git a/samples/js/native-chat-completions/app.js b/samples/js/native-chat-completions/app.js index 9e34c90f..2ecc4356 100644 --- a/samples/js/native-chat-completions/app.js +++ b/samples/js/native-chat-completions/app.js @@ -84,7 +84,7 @@ console.log('\nTesting streaming completion...'); for await (const chunk of chatClient.completeStreamingChat( [{ role: 'user', content: 'Write a short poem about programming.' }] )) { - const content = chunk.choices?.[0]?.message?.content; + const content = chunk.choices?.[0]?.delta?.content; if (content) { process.stdout.write(content); } diff --git a/samples/js/tutorial-chat-assistant/app.js b/samples/js/tutorial-chat-assistant/app.js index bb97960d..842db058 100644 --- a/samples/js/tutorial-chat-assistant/app.js +++ b/samples/js/tutorial-chat-assistant/app.js @@ -73,13 +73,13 @@ while (true) { // Stream the response token by token process.stdout.write('Assistant: '); let fullResponse = ''; - await chatClient.completeStreamingChat(messages, (chunk) => { - const content = chunk.choices?.[0]?.message?.content; + for await (const chunk of chatClient.completeStreamingChat(messages)) { + const content = chunk.choices?.[0]?.delta?.content; if (content) { process.stdout.write(content); fullResponse += content; } - }); + } console.log('\n'); // diff --git a/samples/python/native-chat-completions/src/app.py b/samples/python/native-chat-completions/src/app.py index 457d0cf5..eba9df41 100644 --- a/samples/python/native-chat-completions/src/app.py +++ b/samples/python/native-chat-completions/src/app.py @@ -1,11 +1,10 @@ # # -import asyncio from foundry_local_sdk import Configuration, FoundryLocalManager # -async def main(): +def main(): # # Initialize the Foundry Local SDK config = Configuration(app_name="foundry_local_samples") @@ -64,5 +63,5 @@ def ep_progress(ep_name: str, percent: float): if __name__ == "__main__": - asyncio.run(main()) + main() # diff --git a/samples/python/tool-calling/src/app.py b/samples/python/tool-calling/src/app.py index 995900e3..db619550 100644 --- a/samples/python/tool-calling/src/app.py +++ b/samples/python/tool-calling/src/app.py @@ -1,6 +1,5 @@ # # -import asyncio import json from foundry_local_sdk import Configuration, FoundryLocalManager # @@ -130,7 +129,7 @@ def process_tool_calls(messages, response, client): # -async def main(): +def main(): # Initialize the Foundry Local SDK config = Configuration(app_name="foundry_local_samples") FoundryLocalManager.initialize(config) @@ -192,5 +191,5 @@ def ep_progress(ep_name: str, percent: float): if __name__ == "__main__": - asyncio.run(main()) + main() # diff --git a/samples/python/tutorial-chat-assistant/src/app.py b/samples/python/tutorial-chat-assistant/src/app.py index 5aee3ae1..13f1c500 100644 --- a/samples/python/tutorial-chat-assistant/src/app.py +++ b/samples/python/tutorial-chat-assistant/src/app.py @@ -1,11 +1,10 @@ # # -import asyncio from foundry_local_sdk import Configuration, FoundryLocalManager # -async def main(): +def main(): # # Initialize the Foundry Local SDK config = Configuration(app_name="foundry_local_samples") @@ -64,7 +63,7 @@ def ep_progress(ep_name: str, percent: float): print("Assistant: ", end="", flush=True) full_response = "" for chunk in client.complete_streaming_chat(messages): - content = chunk.choices[0].message.content + content = chunk.choices[0].delta.content if content: print(content, end="", flush=True) full_response += content @@ -81,5 +80,5 @@ def ep_progress(ep_name: str, percent: float): if __name__ == "__main__": - asyncio.run(main()) + main() # diff --git a/samples/python/tutorial-document-summarizer/src/app.py b/samples/python/tutorial-document-summarizer/src/app.py index 671057cd..055bb992 100644 --- a/samples/python/tutorial-document-summarizer/src/app.py +++ b/samples/python/tutorial-document-summarizer/src/app.py @@ -1,13 +1,12 @@ # # -import asyncio import sys from pathlib import Path from foundry_local_sdk import Configuration, FoundryLocalManager # -async def summarize_file(client, file_path, system_prompt): +def summarize_file(client, file_path, system_prompt): """Summarize a single file and print the result.""" content = Path(file_path).read_text(encoding="utf-8") messages = [ @@ -18,7 +17,7 @@ async def summarize_file(client, file_path, system_prompt): print(response.choices[0].message.content) -async def summarize_directory(client, directory, system_prompt): +def summarize_directory(client, directory, system_prompt): """Summarize all .txt files in a directory.""" txt_files = sorted(Path(directory).glob("*.txt")) @@ -28,11 +27,11 @@ async def summarize_directory(client, directory, system_prompt): for txt_file in txt_files: print(f"--- {txt_file.name} ---") - await summarize_file(client, txt_file, system_prompt) + summarize_file(client, txt_file, system_prompt) print() -async def main(): +def main(): # # Initialize the Foundry Local SDK config = Configuration(app_name="foundry_local_samples") @@ -76,10 +75,10 @@ def ep_progress(ep_name: str, percent: float): # if target_path.is_dir(): - await summarize_directory(client, target_path, system_prompt) + summarize_directory(client, target_path, system_prompt) else: print(f"--- {target_path.name} ---") - await summarize_file(client, target_path, system_prompt) + summarize_file(client, target_path, system_prompt) # # Clean up @@ -88,5 +87,5 @@ def ep_progress(ep_name: str, percent: float): if __name__ == "__main__": - asyncio.run(main()) + main() # diff --git a/samples/python/tutorial-tool-calling/src/app.py b/samples/python/tutorial-tool-calling/src/app.py index 5fc1cc53..bb22bfe0 100644 --- a/samples/python/tutorial-tool-calling/src/app.py +++ b/samples/python/tutorial-tool-calling/src/app.py @@ -1,6 +1,5 @@ # # -import asyncio import json from foundry_local_sdk import Configuration, FoundryLocalManager # @@ -130,7 +129,7 @@ def process_tool_calls(messages, response, client): # -async def main(): +def main(): # Initialize the Foundry Local SDK config = Configuration(app_name="foundry_local_samples") FoundryLocalManager.initialize(config) @@ -197,5 +196,5 @@ def ep_progress(ep_name: str, percent: float): if __name__ == "__main__": - asyncio.run(main()) + main() # diff --git a/samples/python/tutorial-voice-to-text/src/app.py b/samples/python/tutorial-voice-to-text/src/app.py index 46ea3926..8ebbba1b 100644 --- a/samples/python/tutorial-voice-to-text/src/app.py +++ b/samples/python/tutorial-voice-to-text/src/app.py @@ -1,11 +1,10 @@ # # -import asyncio from foundry_local_sdk import Configuration, FoundryLocalManager # -async def main(): +def main(): # # Initialize the Foundry Local SDK config = Configuration(app_name="foundry_local_samples") @@ -88,5 +87,5 @@ def ep_progress(ep_name: str, percent: float): if __name__ == "__main__": - asyncio.run(main()) + main() # diff --git a/sdk/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs b/sdk/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs index 2624f98a..7e70c683 100644 --- a/sdk/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs +++ b/sdk/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs @@ -196,8 +196,10 @@ public async Task DirectTool_NoStreaming_Succeeds() await Assert.That(response.Choices[0].Message.ToolCalls?[0].FunctionCall?.Arguments).IsEqualTo(expectedArguments); // Add the response from invoking the tool call to the conversation and check if the model can continue correctly + var toolCallId = response.Choices[0].Message.ToolCalls?[0].Id; + await Assert.That(toolCallId).IsNotNull(); var toolCallResponse = "7 x 6 = 42."; - messages.Add(new ChatMessage { Role = "tool", Content = toolCallResponse }); + messages.Add(new ChatMessage { Role = "tool", ToolCallId = toolCallId, Content = toolCallResponse }); // Prompt the model to continue the conversation after the tool call messages.Add(new ChatMessage { Role = "system", Content = "Respond only with the answer generated by the tool." }); @@ -300,8 +302,10 @@ public async Task DirectTool_Streaming_Succeeds() await Assert.That(toolCallResponse?.Choices[0].Message.ToolCalls?[0].FunctionCall?.Arguments).IsEqualTo(expectedArguments); // Add the response from invoking the tool call to the conversation and check if the model can continue correctly + var toolCallId = toolCallResponse?.Choices[0].Message.ToolCalls?[0].Id; + await Assert.That(toolCallId).IsNotNull(); var toolResponse = "7 x 6 = 42."; - messages.Add(new ChatMessage { Role = "tool", Content = toolResponse }); + messages.Add(new ChatMessage { Role = "tool", ToolCallId = toolCallId, Content = toolResponse }); // Prompt the model to continue the conversation after the tool call messages.Add(new ChatMessage { Role = "system", Content = "Respond only with the answer generated by the tool." }); diff --git a/sdk/js/src/openai/chatClient.ts b/sdk/js/src/openai/chatClient.ts index f844da41..e61efcfa 100644 --- a/sdk/js/src/openai/chatClient.ts +++ b/sdk/js/src/openai/chatClient.ts @@ -167,12 +167,15 @@ export class ChatClient { if (typeof tool.type !== 'string' || tool.type.trim() === '') { throw new Error('Each tool must have a "type" property that is a non-empty string.'); } - if (typeof tool.function !== 'object' || tool.function.description.trim() === '') { + if (!tool.function || typeof tool.function !== 'object') { throw new Error('Each tool must have a "function" property that is a non-empty object.'); } if (typeof tool.function.name !== 'string' || tool.function.name.trim() === '') { throw new Error('Each tool\'s function must have a "name" property that is a non-empty string.'); } + if (tool.function.description !== undefined && typeof tool.function.description !== 'string') { + throw new Error('Each tool\'s function "description", if provided, must be a string.'); + } } } diff --git a/sdk/python/test/README.md b/sdk/python/test/README.md index ded38f5b..4d60d557 100644 --- a/sdk/python/test/README.md +++ b/sdk/python/test/README.md @@ -4,7 +4,7 @@ This test suite mirrors the structure of the JS (`sdk_v2/js/test/`) and C# (`sdk ## Prerequisites -1. **Python 3.10+** (tested with 3.12/3.13) +1. **Python 3.11+** (tested with 3.12/3.13) 2. **SDK installed in editable mode** from the `sdk/python` directory: ```bash pip install -e . diff --git a/sdk/rust/docs/api.md b/sdk/rust/docs/api.md index a21c23a0..abfec76f 100644 --- a/sdk/rust/docs/api.md +++ b/sdk/rust/docs/api.md @@ -12,7 +12,6 @@ - [Model Catalog](#model-catalog) - [Catalog](#catalog) - [Model](#model) - - [ModelVariant](#modelvariant) - [OpenAI Clients](#openai-clients) - [ChatClient](#chatclient) - [ChatCompletionStream](#chatcompletionstream) @@ -131,15 +130,15 @@ pub struct Catalog { /* private fields */ } | `update_models` | `async fn update_models(&self) -> Result<(), FoundryLocalError>` | Refresh catalog if cache expired or invalidated. | | `get_models` | `async fn get_models(&self) -> Result>, FoundryLocalError>` | Return all known models. | | `get_model` | `async fn get_model(&self, alias: &str) -> Result, FoundryLocalError>` | Look up a model by alias. | -| `get_model_variant` | `async fn get_model_variant(&self, id: &str) -> Result, FoundryLocalError>` | Look up a variant by unique id. | -| `get_cached_models` | `async fn get_cached_models(&self) -> Result>, FoundryLocalError>` | Return only variants cached on disk. | -| `get_loaded_models` | `async fn get_loaded_models(&self) -> Result>, FoundryLocalError>` | Return model variants currently loaded in memory. | +| `get_model_variant` | `async fn get_model_variant(&self, id: &str) -> Result, FoundryLocalError>` | Look up a variant by unique id. | +| `get_cached_models` | `async fn get_cached_models(&self) -> Result>, FoundryLocalError>` | Return only variants cached on disk. | +| `get_loaded_models` | `async fn get_loaded_models(&self) -> Result>, FoundryLocalError>` | Return model variants currently loaded in memory. | --- ### Model -Groups one or more `ModelVariant`s sharing the same alias. By default, the cached variant is selected. +Groups one or more variants sharing the same alias. By default, the cached variant is selected. ```rust pub struct Model { /* private fields */ } @@ -149,8 +148,7 @@ pub struct Model { /* private fields */ } |--------|-----------|-------------| | `alias` | `fn alias(&self) -> &str` | Alias shared by all variants. | | `id` | `fn id(&self) -> &str` | Unique identifier of the selected variant. | -| `variants` | `fn variants(&self) -> &[Arc]` | All variants in this model. | -| `selected_variant` | `fn selected_variant(&self) -> &ModelVariant` | Currently selected variant. | +| `variants` | `fn variants(&self) -> Vec>` | All variants in this model. | | `select_variant` | `fn select_variant(&self, variant: &Model) -> Result<(), FoundryLocalError>` | Select a variant from `variants()`. | | `select_variant_by_id` | `fn select_variant_by_id(&self, id: &str) -> Result<(), FoundryLocalError>` | Select a variant by its unique id string. | | `is_cached` | `async fn is_cached(&self) -> Result` | Whether the selected variant is cached on disk. | @@ -165,31 +163,6 @@ pub struct Model { /* private fields */ } --- -### ModelVariant - -A single model variant — one specific id within an alias group. - -```rust -pub struct ModelVariant { /* private fields */ } -``` - -| Method | Signature | Description | -|--------|-----------|-------------| -| `info` | `fn info(&self) -> &ModelInfo` | Full metadata for this variant. | -| `id` | `fn id(&self) -> &str` | Unique identifier. | -| `alias` | `fn alias(&self) -> &str` | Alias shared with sibling variants. | -| `is_cached` | `async fn is_cached(&self) -> Result` | Whether cached locally. ⚠️ Full IPC per call — prefer `Catalog::get_cached_models()` for batch use. | -| `is_loaded` | `async fn is_loaded(&self) -> Result` | Whether currently loaded in memory. | -| `download` | `async fn download(&self, progress: Option) -> Result<(), FoundryLocalError>` | Download the variant. `F: FnMut(f64) + Send + 'static` — receives progress as a percentage (0.0–100.0). | -| `path` | `async fn path(&self) -> Result` | Local file-system path. | -| `load` | `async fn load(&self) -> Result<(), FoundryLocalError>` | Load into memory. | -| `unload` | `async fn unload(&self) -> Result` | Unload from memory. | -| `remove_from_cache` | `async fn remove_from_cache(&self) -> Result` | Remove from local cache. | -| `create_chat_client` | `fn create_chat_client(&self) -> ChatClient` | Create a ChatClient bound to this variant. | -| `create_audio_client` | `fn create_audio_client(&self) -> AudioClient` | Create an AudioClient bound to this variant. | - ---- - ## OpenAI Clients ### ChatClient