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