Skip to content

Commit aa6c9be

Browse files
baijumeswaniPrathik Rao
andauthored
Fix Linux Python SDK, Pydantic warning and updates the ExecuteCommandWithCallback signature (#593)
This pull-request has 3 changes: - Fixes the pydantic warnings by intializing `model_config = ConfigDict(protected_namespaces=())` - Fixes Linux python sdk by depending on onnxruntime-gpu and onnxruntime-genai-cuda on Linux - Updates the callback signature because FLC updated the callback signature to return an int instead of void. Returning 1 means that the execute command operation should be cancelled, whereas 0 means to continue. The actual model download cancellation support is not part of this pull-request. Will add it in a subsequent PR. --------- Co-authored-by: Prathik Rao <prathikrao@microsoft.com>
1 parent f85ab46 commit aa6c9be

File tree

9 files changed

+77
-31
lines changed

9 files changed

+77
-31
lines changed

sdk/cs/src/Detail/CoreInterop.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ public CallbackHelper(CallbackFn callback)
203203
}
204204
}
205205

206-
private static void HandleCallback(nint data, int length, nint callbackHelper)
206+
private static int HandleCallback(nint data, int length, nint callbackHelper)
207207
{
208208
var callbackData = string.Empty;
209209
CallbackHelper? helper = null;
@@ -221,14 +221,24 @@ private static void HandleCallback(nint data, int length, nint callbackHelper)
221221

222222
helper = (CallbackHelper)GCHandle.FromIntPtr(callbackHelper).Target!;
223223
helper.Callback.Invoke(callbackData);
224+
return 0; // continue
224225
}
225-
catch (Exception ex) when (ex is not OperationCanceledException)
226+
catch (OperationCanceledException ex)
227+
{
228+
if (helper != null && helper.Exception == null)
229+
{
230+
helper.Exception = ex;
231+
}
232+
return 1; // cancel
233+
}
234+
catch (Exception ex)
226235
{
227236
FoundryLocalManager.Instance.Logger.LogError(ex, $"Error in callback. Callback data: {callbackData}");
228237
if (helper != null && helper.Exception == null)
229238
{
230239
helper.Exception = ex;
231240
}
241+
return 1; // cancel on error
232242
}
233243
}
234244

sdk/cs/src/Detail/ICoreInterop.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ protected unsafe struct ResponseBuffer
4040
}
4141

4242
// native callback function signature
43+
// Return: 0 = continue, 1 = cancel
4344
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
44-
protected unsafe delegate void NativeCallbackFn(nint data, int length, nint userData);
45+
protected unsafe delegate int NativeCallbackFn(nint data, int length, nint userData);
4546

4647
Response ExecuteCommand(string commandName, CoreInteropRequest? commandInput = null);
4748
Response ExecuteCommandWithCallback(string commandName, CoreInteropRequest? commandInput, CallbackFn callback);

sdk/js/src/detail/coreInterop.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ koffi.struct('StreamingRequestBuffer', {
2929
BinaryDataLength: 'int32_t',
3030
});
3131

32-
const CallbackType = koffi.proto('void CallbackType(void *data, int32_t length, void *userData)');
32+
const CallbackType = koffi.proto('int32_t CallbackType(void *data, int32_t length, void *userData)');
3333

3434
const __filename = fileURLToPath(import.meta.url);
3535
const __dirname = path.dirname(__filename);
@@ -198,8 +198,13 @@ export class CoreInterop {
198198
koffi.encode(dataBuf, 'char', dataStr, dataBytes.length + 1);
199199

200200
const cb = koffi.register((data: any, length: number, userData: any) => {
201-
const chunk = koffi.decode(data, 'char', length);
202-
callback(chunk);
201+
try {
202+
const chunk = koffi.decode(data, 'char', length);
203+
callback(chunk);
204+
return 0; // continue
205+
} catch {
206+
return 1; // cancel on error
207+
}
203208
}, koffi.pointer(CallbackType));
204209

205210
return new Promise<string>((resolve, reject) => {

sdk/python/requirements-winml.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ pydantic>=2.0.0
22
requests>=2.32.4
33
openai>=2.24.0
44
# WinML native binary packages from the ORT-Nightly PyPI feed.
5-
foundry-local-core-winml==0.9.0.dev20260331004032
5+
foundry-local-core-winml==1.0.0rc1
66
onnxruntime-core==1.23.2.3
77
onnxruntime-genai-core==0.13.0

sdk/python/requirements.txt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ pydantic>=2.0.0
22
requests>=2.32.4
33
openai>=2.24.0
44
# Standard native binary packages from the ORT-Nightly PyPI feed.
5-
foundry-local-core==0.9.0.dev20260327060216
6-
onnxruntime-core==1.24.4
7-
onnxruntime-genai-core==0.13.0
5+
foundry-local-core==1.0.0rc1
6+
onnxruntime-core==1.24.4; sys_platform != "linux"
7+
onnxruntime-gpu==1.24.4; sys_platform == "linux"
8+
onnxruntime-genai-core==0.13.0; sys_platform != "linux"
9+
onnxruntime-genai-cuda==0.13.0; sys_platform == "linux"

sdk/python/src/detail/core_interop.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,11 @@ def callback(data_ptr, length, self_ptr):
7979
data_bytes = ctypes.string_at(data_ptr, length)
8080
data_str = data_bytes.decode('utf-8')
8181
self._py_callback(data_str)
82+
return 0 # continue
8283
except Exception as e:
8384
if self is not None and self.exception is None:
8485
self.exception = e # keep the first only as they are likely all the same
86+
return 1 # cancel on error
8587

8688
def __init__(self, py_callback: Callable[[str], None]):
8789
self._py_callback = py_callback
@@ -103,8 +105,8 @@ class CoreInterop:
103105
instance = None
104106

105107
# Callback function for native interop.
106-
# This returns a string and its length, and an optional user provided object.
107-
CALLBACK_TYPE = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p)
108+
# Returns c_int: 0 = continue, 1 = cancel.
109+
CALLBACK_TYPE = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p)
108110

109111
@staticmethod
110112
def _initialize_native_libraries() -> 'NativeBinaryPaths':
@@ -129,8 +131,9 @@ def _initialize_native_libraries() -> 'NativeBinaryPaths':
129131
logger.info("Native libraries found — Core: %s ORT: %s GenAI: %s",
130132
paths.core, paths.ort, paths.genai)
131133

132-
# Create the onnxruntime.dll symlink on Linux/macOS if needed.
133-
# create_ort_symlinks(paths)
134+
# Create compatibility symlinks on Linux/macOS so Core can resolve
135+
# ORT/GenAI names regardless of package layout.
136+
create_ort_symlinks(paths)
134137
os.environ["ORT_LIB_PATH"] = str(paths.ort) # For ORT-GENAI to find ORT dependency
135138

136139
if sys.platform.startswith("win"):

sdk/python/src/detail/model_data_types.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# --------------------------------------------------------------------------
55

66
from typing import Optional, List
7-
from pydantic import BaseModel, Field
7+
from pydantic import BaseModel, ConfigDict, Field
88

99
from enum import StrEnum
1010

@@ -53,6 +53,8 @@ class ModelInfo(BaseModel):
5353
Fields are populated from the JSON response of the ``get_model_list`` command.
5454
"""
5555

56+
model_config = ConfigDict(protected_namespaces=())
57+
5658
id: str = Field(alias="id", description="Unique identifier of the model. Generally <name>:<version>")
5759
name: str = Field(alias="name", description="Model variant name")
5860
version: int = Field(alias="version")

sdk/python/src/detail/utils.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import argparse
1414
import importlib.util
15-
import json
1615
import logging
1716
import os
1817
import sys
@@ -90,9 +89,9 @@ def _find_file_in_package(package_name: str, filename: str) -> Path | None:
9089

9190
# Quick checks for well-known sub-directories first
9291
for candidate_dir in (pkg_root, pkg_root / "capi", pkg_root / "native", pkg_root / "lib", pkg_root / "bin"):
93-
candidate = candidate_dir / filename
94-
if candidate.exists():
95-
return candidate
92+
candidates = list(candidate_dir.glob(f"*{filename}*"))
93+
if candidates:
94+
return candidates[0]
9695

9796
# Recursive fallback
9897
for match in pkg_root.rglob(filename):
@@ -144,8 +143,18 @@ def get_native_binary_paths() -> NativeBinaryPaths | None:
144143

145144
# Probe WinML packages first; fall back to standard if not installed.
146145
core_path = _find_file_in_package("foundry-local-core-winml", core_name) or _find_file_in_package("foundry-local-core", core_name)
147-
ort_path = _find_file_in_package("onnxruntime-core", ort_name)
148-
genai_path = _find_file_in_package("onnxruntime-genai-core", genai_name)
146+
147+
# On Linux, ORT is shipped by onnxruntime-gpu (libonnxruntime.so in capi/).
148+
if sys.platform.startswith("linux"):
149+
ort_path = _find_file_in_package("onnxruntime", ort_name) or _find_file_in_package("onnxruntime-core", ort_name)
150+
else:
151+
ort_path = _find_file_in_package("onnxruntime-core", ort_name)
152+
153+
# On Linux, ORTGenAI is shipped by onnxruntime-genai-cuda (libonnxruntime-genai.so in the package root).
154+
if sys.platform.startswith("linux"):
155+
genai_path = _find_file_in_package("onnxruntime-genai", genai_name) or _find_file_in_package("onnxruntime-genai-core", genai_name)
156+
else:
157+
genai_path = _find_file_in_package("onnxruntime-genai-core", genai_name)
149158

150159
if core_path and ort_path and genai_path:
151160
return NativeBinaryPaths(core=core_path, ort=ort_path, genai=genai_path)
@@ -254,6 +263,9 @@ def foundry_local_install(args: list[str] | None = None) -> None:
254263
if parsed.winml:
255264
variant = "WinML"
256265
packages = ["foundry-local-core-winml", "onnxruntime-core", "onnxruntime-genai-core"]
266+
elif sys.platform.startswith("linux"):
267+
variant = "Linux (GPU)"
268+
packages = ["foundry-local-core", "onnxruntime-gpu", "onnxruntime-genai-cuda"]
257269
else:
258270
variant = "standard"
259271
packages = ["foundry-local-core", "onnxruntime-core", "onnxruntime-genai-core"]
@@ -271,10 +283,18 @@ def foundry_local_install(args: list[str] | None = None) -> None:
271283
else:
272284
if _find_file_in_package("foundry-local-core", core_name) is None:
273285
missing.append("foundry-local-core")
274-
if _find_file_in_package("onnxruntime-core", ort_name) is None:
286+
if sys.platform.startswith("linux"):
287+
if _find_file_in_package("onnxruntime", ort_name) is None:
288+
missing.append("onnxruntime-gpu")
289+
else:
290+
if _find_file_in_package("onnxruntime-core", ort_name) is None:
275291
missing.append("onnxruntime-core")
276-
if _find_file_in_package("onnxruntime-genai-core", genai_name) is None:
277-
missing.append("onnxruntime-genai-core")
292+
if sys.platform.startswith("linux"):
293+
if _find_file_in_package("onnxruntime-genai", genai_name) is None:
294+
missing.append("onnxruntime-genai-cuda")
295+
else:
296+
if _find_file_in_package("onnxruntime-genai-core", genai_name) is None:
297+
missing.append("onnxruntime-genai-core")
278298
print(
279299
"[foundry-local] ERROR: Could not locate native binaries after installation. "
280300
f"Missing: {', '.join(missing)}",
@@ -289,6 +309,3 @@ def foundry_local_install(args: list[str] | None = None) -> None:
289309
print(f" Core : {paths.core}")
290310
print(f" ORT : {paths.ort}")
291311
print(f" GenAI : {paths.genai}")
292-
293-
294-

sdk/rust/src/detail/core_interop.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ impl ResponseBuffer {
5252
type ExecuteCommandFn = unsafe extern "C" fn(*const RequestBuffer, *mut ResponseBuffer);
5353

5454
/// Signature for the streaming callback invoked by the native library.
55-
type CallbackFn = unsafe extern "C" fn(*const u8, i32, *mut std::ffi::c_void);
55+
/// Returns 0 to continue, 1 to cancel.
56+
type CallbackFn = unsafe extern "C" fn(*const u8, i32, *mut std::ffi::c_void) -> i32;
5657

5758
/// Signature for `execute_command_with_callback`.
5859
type ExecuteCommandWithCallbackFn = unsafe extern "C" fn(
@@ -197,12 +198,12 @@ unsafe extern "C" fn streaming_trampoline(
197198
data: *const u8,
198199
length: i32,
199200
user_data: *mut std::ffi::c_void,
200-
) {
201+
) -> i32 {
201202
if data.is_null() || length <= 0 {
202-
return;
203+
return 0;
203204
}
204205
// catch_unwind prevents UB if the closure panics across the FFI boundary.
205-
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
206+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
206207
// SAFETY: `user_data` points to a `StreamingCallbackState` kept alive
207208
// by the caller of `execute_command_with_callback` for the duration of
208209
// the native call.
@@ -212,6 +213,11 @@ unsafe extern "C" fn streaming_trampoline(
212213
let slice = std::slice::from_raw_parts(data, length as usize);
213214
state.push(slice);
214215
}));
216+
if result.is_err() {
217+
1
218+
} else {
219+
0
220+
}
215221
}
216222

217223
// ── CoreInterop ──────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)