Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ USER_SUMMARY_EVERY_N=20
MEMORY_PROCESSOR_OWNER=

# ---- AI Foundry / Azure OpenAI ----
# Use the account-level inference endpoint. Both of these forms work:
# https://<your-account>.services.ai.azure.com
# https://<your-account>.openai.azure.com
# A project-scoped Foundry URL (".../api/projects/<name>") is also accepted and
# automatically normalized to the inference base.
AI_FOUNDRY_ENDPOINT=https://<your-account>.openai.azure.com/
AI_FOUNDRY_API_KEY=
AI_FOUNDRY_EMBEDDING_DEPLOYMENT_NAME=text-embedding-3-large
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
## Release History

### Unreleased

#### Other Changes
* `ai_foundry_endpoint` now accepts a project-scoped Azure AI Foundry URL
(`https://<resource>.services.ai.azure.com/api/projects/<name>`) in addition
to the account-level inference endpoint. The project path is automatically
stripped to the inference base, so callers can paste whichever form the
Foundry portal shows them without hitting opaque 404s.

### 0.1.0b2 (2026-06-03)

#### Bugs Fixed
Expand Down
3 changes: 2 additions & 1 deletion azure/cosmos/agent_memory/_base/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
_resolve_cosmos_provisioning_autoscale_max_ru,
_resolve_cosmos_throughput_mode,
_resolve_embedding_dimensions,
normalize_ai_foundry_endpoint,
)
from azure.cosmos.agent_memory.exceptions import CosmosNotConnectedError, MemoryNotFoundError, ValidationError
from azure.cosmos.agent_memory.logging import configure_logging, get_logger
Expand Down Expand Up @@ -69,7 +70,7 @@ def _init_base_config(
autoscale_max_ru=cosmos_autoscale_max_ru,
)

self._ai_foundry_endpoint = ai_foundry_endpoint
self._ai_foundry_endpoint = normalize_ai_foundry_endpoint(ai_foundry_endpoint)
self._ai_foundry_credential = ai_foundry_credential
self._ai_foundry_api_key = ai_foundry_api_key
self._embedding_deployment_name = embedding_deployment_name
Expand Down
35 changes: 35 additions & 0 deletions azure/cosmos/agent_memory/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,41 @@ def _resolve_embedding_dimensions(val: Optional[int]) -> int:
return parsed


_AI_FOUNDRY_PROJECT_PATH_RE = re.compile(r"/api/projects/[^/]+/?.*$", re.IGNORECASE)


def normalize_ai_foundry_endpoint(endpoint: Optional[str]) -> Optional[str]:
"""Normalize an AI Foundry / Azure OpenAI endpoint to the inference base URL.

The toolkit reaches the model inference API through the OpenAI SDK
(``AzureOpenAI(azure_endpoint=...)``), which expects the account-level
inference endpoint, for example::

https://<resource>.services.ai.azure.com
https://<resource>.openai.azure.com

The Azure AI Foundry portal, however, commonly surfaces a *project*-scoped
endpoint of the form::

https://<resource>.services.ai.azure.com/api/projects/<project-name>

For ``*.services.ai.azure.com`` resources the project path lives on the same
host that serves inference, so this helper strips a trailing
``/api/projects/<name>`` segment (plus any surrounding whitespace or trailing
slash) to recover the base inference endpoint. Callers can therefore paste
either form.

Endpoints that don't carry a project path are returned unchanged aside from
whitespace/trailing-slash trimming, so non-Foundry endpoints keep working.
``None``/empty values are passed through untouched.
"""
if not endpoint:
return endpoint
trimmed = endpoint.strip()
trimmed = _AI_FOUNDRY_PROJECT_PATH_RE.sub("", trimmed)
return trimmed.rstrip("/")
Comment on lines +204 to +208


_ALLOWED_EMBEDDING_DATA_TYPES = ("float32", "uint8", "int8")
_ALLOWED_DISTANCE_FUNCTIONS = ("cosine", "dotproduct", "euclidean")

Expand Down
12 changes: 12 additions & 0 deletions tests/unit/_base/test_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ def test_base_config_validates_throughput_mode():
DummyClient(cosmos_throughput_mode="invalid")


def test_base_config_normalizes_ai_foundry_project_endpoint():
client = DummyClient(
ai_foundry_endpoint="https://my-res.services.ai.azure.com/api/projects/my-project"
)
assert client._ai_foundry_endpoint == "https://my-res.services.ai.azure.com"


def test_base_config_leaves_plain_ai_foundry_endpoint_untouched():
client = DummyClient(ai_foundry_endpoint="https://my-res.services.ai.azure.com")
assert client._ai_foundry_endpoint == "https://my-res.services.ai.azure.com"


def test_require_cosmos_guard_and_context_manager():
client = DummyClient()

Expand Down
63 changes: 63 additions & 0 deletions tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
_resolve_embedding_data_type,
_resolve_full_text_language,
compute_content_hash,
normalize_ai_foundry_endpoint,
)
from azure.cosmos.agent_memory.exceptions import ConfigurationError, ValidationError

Expand Down Expand Up @@ -234,3 +235,65 @@ def test_resolve_full_text_language_defaults(monkeypatch):
def test_resolve_full_text_language_from_env(monkeypatch):
monkeypatch.setenv("COSMOS_DB_FULL_TEXT_LANGUAGE", "fr-FR")
assert _resolve_full_text_language(None) == "fr-FR"


# ---------------------------------------------------------------------------
# normalize_ai_foundry_endpoint
# ---------------------------------------------------------------------------


@pytest.mark.parametrize("value", [None, ""])
def test_normalize_ai_foundry_endpoint_passes_through_empty(value):
assert normalize_ai_foundry_endpoint(value) == value


def test_normalize_ai_foundry_endpoint_strips_project_path():
assert (
normalize_ai_foundry_endpoint("https://my-res.services.ai.azure.com/api/projects/my-project")
== "https://my-res.services.ai.azure.com"
)


def test_normalize_ai_foundry_endpoint_strips_project_path_with_trailing_slash():
assert (
normalize_ai_foundry_endpoint("https://my-res.services.ai.azure.com/api/projects/my-project/")
== "https://my-res.services.ai.azure.com"
)


def test_normalize_ai_foundry_endpoint_strips_project_path_with_extra_segments():
assert (
normalize_ai_foundry_endpoint(
"https://my-res.services.ai.azure.com/api/projects/my-project/deployments/x"
)
== "https://my-res.services.ai.azure.com"
)


def test_normalize_ai_foundry_endpoint_leaves_base_services_endpoint():
assert (
normalize_ai_foundry_endpoint("https://my-res.services.ai.azure.com")
== "https://my-res.services.ai.azure.com"
)


def test_normalize_ai_foundry_endpoint_leaves_openai_endpoint():
assert (
normalize_ai_foundry_endpoint("https://my-res.openai.azure.com")
== "https://my-res.openai.azure.com"
)


def test_normalize_ai_foundry_endpoint_trims_whitespace_and_trailing_slash():
assert (
normalize_ai_foundry_endpoint(" https://my-res.services.ai.azure.com/ ")
== "https://my-res.services.ai.azure.com"
)


def test_normalize_ai_foundry_endpoint_is_case_insensitive_on_project_path():
assert (
normalize_ai_foundry_endpoint("https://my-res.services.ai.azure.com/API/Projects/my-project")
== "https://my-res.services.ai.azure.com"
)

Loading