Skip to content
Merged
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
3 changes: 3 additions & 0 deletions docs/wayflowcore/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def filter(self, record):
"sphinx.ext.napoleon",
"sphinx.ext.autodoc",
"sphinx.ext.mathjax",
"sphinx_prompt",
"sphinx_substitution_extensions",
"sphinx.ext.extlinks",
"sphinx_toolbox.collapse",
Expand Down Expand Up @@ -249,6 +250,8 @@ def filter(self, record):
("py:class", r"wayflowcore\..*\.Annotated"),
# Failing one one case in `SoftTokenLimitExecutionInterrupt`
("py:class", r"MetadataType"),
# Pydantic field metadata marker rendered as a bogus cross-reference.
("py:class", r"'?SENSITIVE_FIELD_MARKER'?"),
# Purposely ignoring classes:
("py:class", r"wayflowcore.executors._events.event.Event"),
("py:class", r"(?:wayflowcore\.executors\._executor\.)?ConversationExecutor"),
Expand Down
7 changes: 7 additions & 0 deletions docs/wayflowcore/source/core/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ WayFlow |current_version|
New features
^^^^^^^^^^^^

* **Custom TLS certificates for OpenAI-compatible models:**

:ref:`OpenAICompatibleModel <openaicompatiblemodel>` and :ref:`OpenAICompatibleEmbeddingModel <openaicompatibleembeddingmodel>`
now support optional ``key_file``, ``cert_file`` and ``ca_file`` parameters for custom CAs and mutual TLS.

For more information read :ref:`how to configure custom TLS certificates for OpenAI-compatible endpoints <openaicompatible_custom_tls>`.

* **OAuth support for MCP Clients:**

MCP Clients now support OAuth-based authorization.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@
# print(f"OpenAI embeddings dimension: {len(embeddings[0])}")
# .. openai-embedding-end
"""
"""
# .. openai-compatible-embedding-tls-start
from wayflowcore.embeddingmodels import OpenAICompatibleEmbeddingModel

if __name__ == "__main__":

model = OpenAICompatibleEmbeddingModel(
model_id="your-embedding-model",
base_url="https://internal-embeddings.example.com",
key_file="/etc/certs/client.key",
cert_file="/etc/certs/client.pem",
ca_file="/etc/certs/ca.pem",
)
# .. openai-compatible-embedding-tls-end
"""

"""
# .. vllm-embedding-start
from wayflowcore.embeddingmodels import VllmEmbeddingModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,20 @@
llm = LlmModelFactory.from_config(OPENAI_CONFIG)
# .. openai-llmfactory-end

# .. openai-compatible-tls-start
from wayflowcore.models import OpenAICompatibleModel

if __name__ == "__main__":

llm = OpenAICompatibleModel(
model_id="your-model",
base_url="https://internal-llm.example.com",
key_file="/etc/certs/client.key",
cert_file="/etc/certs/client.pem",
ca_file="/etc/certs/ca.pem",
)
# .. openai-compatible-tls-end

# .. ollama-start
from wayflowcore.models import OllamaModel

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ For the embedding models shown in this guide:

The following sections provide detailed information about each supported embedding model type, with examples showing how to instantiate and use them.

.. _openaicompatible_embedding_custom_tls:

Custom TLS Certificates for OpenAI-Compatible Embedding Endpoints
-----------------------------------------------------------------

``OpenAICompatibleEmbeddingModel``, ``VllmEmbeddingModel`` and ``OllamaEmbeddingModel`` accept optional
TLS certificate parameters for HTTPS endpoints that use private certificate authorities or mutual TLS.

.. literalinclude:: ../code_examples/example_initialize_embedding_models.py
:language: python
:start-after: .. openai-compatible-embedding-tls-start
:end-before: .. openai-compatible-embedding-tls-end

Use ``ca_file`` to trust a custom certificate authority, and use ``key_file`` together with ``cert_file`` for mutual TLS (mTLS).
These certificate fields are runtime-only and are intentionally omitted from WayFlow serialization output.

OCI GenAI Embedding Model
-------------------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ However, you can also achieve this by directly instantiating the model classes,

You can find a detailed description of each supported model type in this guide, demonstrating both methods — using the configuration dictionary and direct instantiation — for each model.

.. _openaicompatible_custom_tls:

Custom TLS Certificates for OpenAI-Compatible Endpoints
-------------------------------------------------------

``OpenAICompatibleModel``, ``VllmModel`` and ``OllamaModel`` accept optional TLS certificate parameters for HTTPS endpoints that are not covered by the system CA store.

.. literalinclude:: ../code_examples/example_initialize_llms.py
:language: python
:start-after: .. openai-compatible-tls-start
:end-before: .. openai-compatible-tls-end

Use ``ca_file`` to trust a custom certificate authority, and use ``key_file`` together with ``cert_file`` for mutual TLS (mTLS).
These certificate fields are runtime-only and are intentionally omitted from WayFlow serialization output.

OCI GenAI Model
---------------

Expand Down
2 changes: 1 addition & 1 deletion wayflowcore/constraints/constraints_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ PyYAML==6.0.3
pydantic==2.12.4
httpx==0.28.1
mcp==1.24.0
pyagentspec==26.2.0.dev2 # Main branch of pyagentspec
pyagentspec==26.2.0.dev3 # Main branch of pyagentspec
opentelemetry-api==1.36.0
opentelemetry-sdk==1.36.0
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
# (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) or Universal Permissive License
# (UPL) 1.0 (LICENSE-UPL or https://oss.oracle.com/licenses/upl), at your option.

from typing import Optional

from pyagentspec import Component
from pyagentspec.component import SerializeAsEnum
from pyagentspec.llms.ociclientconfig import OciClientConfig
from pyagentspec.sensitive_field import SensitiveField
from pydantic import SerializeAsAny

from wayflowcore.agentspec.components._pydantic_plugins import (
Expand Down Expand Up @@ -50,6 +52,12 @@ class PluginOpenAiCompatibleEmbeddingConfig(PluginEmbeddingConfig):
"""Url of the model deployment"""
model_id: str
"""ID of the model to use"""
key_file: SensitiveField[Optional[str]] = None
"""The path to an optional client private key file (PEM format)."""
cert_file: SensitiveField[Optional[str]] = None
"""The path to an optional client certificate chain file (PEM format)."""
ca_file: SensitiveField[Optional[str]] = None
"""The path to an optional trusted CA certificate file (PEM format) used to verify the server."""


class PluginOllamaEmbeddingConfig(PluginOpenAiCompatibleEmbeddingConfig):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from wayflowcore._utils.async_helpers import run_async_in_sync
from wayflowcore.embeddingmodels.embeddingmodel import EmbeddingModel
from wayflowcore.models._requesthelpers import _RetryStrategy, request_post_with_retries
from wayflowcore.models.openaicompatiblemodel import _resolve_api_key
from wayflowcore.models.openaicompatiblemodel import _build_ssl_verification, _resolve_api_key
from wayflowcore.serialization.context import DeserializationContext, SerializationContext
from wayflowcore.serialization.serializer import SerializableObject

Expand All @@ -31,13 +31,22 @@ class OpenAICompatibleEmbeddingModel(EmbeddingModel, SerializableObject):
API key to use for the request if needed. It will be formatted in the OpenAI format.
(as "Bearer API_KEY" in the request header)
If not provided, will attempt to read from the environment variable OPENAI_API_KEY
key_file:
The path to an optional client private key file (PEM format).
cert_file:
The path to an optional client certificate chain file (PEM format).
ca_file:
The path to an optional trusted CA certificate file (PEM format) to verify the server.
"""

def __init__(
self,
model_id: str,
base_url: str,
api_key: Optional[str] = None,
key_file: Optional[str] = None,
cert_file: Optional[str] = None,
ca_file: Optional[str] = None,
__metadata_info__: Optional[MetadataType] = None,
id: Optional[str] = None,
name: Optional[str] = None,
Expand All @@ -53,6 +62,14 @@ def __init__(
self._base_url = _add_leading_http_if_needed(base_url).rstrip("/")
self._retry_strategy = _RetryStrategy()
self._api_key = _resolve_api_key(api_key)
self.key_file = key_file
self.cert_file = cert_file
self.ca_file = ca_file
self._ssl_verify = _build_ssl_verification(
key_file=key_file,
cert_file=cert_file,
ca_file=ca_file,
)

def _get_headers(self) -> Dict[str, Any]:
headers = {
Expand All @@ -77,6 +94,7 @@ async def embed_async(self, data: List[str]) -> List[List[float]]:
response_data = await request_post_with_retries(
request_params=dict(url=url, headers=headers, json=payload),
retry_strategy=self._retry_strategy,
verify=self._ssl_verify,
)

return [item["embedding"] for item in response_data["data"]]
Expand Down
22 changes: 20 additions & 2 deletions wayflowcore/src/wayflowcore/models/_requesthelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import json
import logging
import ssl
import sys
from contextlib import contextmanager
from dataclasses import dataclass, field
Expand Down Expand Up @@ -35,6 +36,8 @@

logger = logging.getLogger(__name__)

VerifyType = Union[bool, str, ssl.SSLContext]


@dataclass
class _RetryStrategy:
Expand Down Expand Up @@ -94,13 +97,22 @@ async def request_post_with_retries(
request_params: Dict[str, Any],
retry_strategy: _RetryStrategy,
proxy: Optional[str] = None,
verify: VerifyType = True,
) -> Dict[str, Any]:
"""Makes a POST request using requests.post with OpenAI-like retry behavior"""
tries = 0
last_exc = None
while tries <= retry_strategy.max_retries:
try:
async with httpx.AsyncClient(proxy=proxy, timeout=retry_strategy.timeout) as session:
# Ignore ambient proxy environment variables with `trust_env=False` to prevent injected
# HTTPS proxy settings from hijack localhost TLS test traffic and cause the client to
# validate the proxy certificate instead of the test server certificate.
async with httpx.AsyncClient(
proxy=proxy,
timeout=retry_strategy.timeout,
verify=verify,
trust_env=False,
) as session:
response = await session.post(**request_params)
if response.status_code == 200:
try:
Expand Down Expand Up @@ -194,15 +206,21 @@ async def request_streaming_post_with_retries(
request_params: Dict[str, Any],
retry_strategy: _RetryStrategy,
proxy: Optional[str] = None,
verify: VerifyType = True,
) -> AsyncGenerator[str, None]:
tries = 0
last_exc = None

with silence_generator_exit_warnings():
while tries <= retry_strategy.max_retries:
try:
# Match non-streaming behavior: only use the explicit `proxy` argument and do
# not inherit proxy settings from the process environment.
async with httpx.AsyncClient(
proxy=proxy, timeout=retry_strategy.timeout
proxy=proxy,
timeout=retry_strategy.timeout,
verify=verify,
trust_env=False,
) as session:
async with session.stream("POST", **request_params) as response:
if response.status_code == 200:
Expand Down
13 changes: 13 additions & 0 deletions wayflowcore/src/wayflowcore/models/ollamamodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ def __init__(
model_id: str,
host_port: str = DEFAULT_OLLAMA_HOST_PORT,
proxy: Optional[str] = None,
key_file: Optional[str] = None,
cert_file: Optional[str] = None,
ca_file: Optional[str] = None,
generation_config: Optional[LlmGenerationConfig] = None,
supports_structured_generation: Optional[bool] = True,
supports_tool_calling: Optional[bool] = True,
Expand All @@ -55,6 +58,12 @@ def __init__(
By default Ollama binds port 11434.
proxy:
Proxy to use to connect to the remote LLM endpoint
key_file:
The path to an optional client private key file (PEM format).
cert_file:
The path to an optional client certificate chain file (PEM format).
ca_file:
The path to an optional trusted CA certificate file (PEM format) to verify the server.
generation_config:
default parameters for text generation with this model
supports_structured_generation:
Expand Down Expand Up @@ -102,6 +111,9 @@ def __init__(
base_url=host_port,
proxy=proxy,
api_key=EMPTY_API_KEY,
Comment thread
paul-cayet marked this conversation as resolved.
key_file=key_file,
cert_file=cert_file,
ca_file=ca_file,
generation_config=generation_config,
supports_tool_calling=supports_tool_calling,
supports_structured_generation=supports_structured_generation,
Expand All @@ -128,6 +140,7 @@ def _post_process(self, message: "Message") -> "Message":

@property
def config(self) -> Dict[str, Any]:
self._warn_about_runtime_only_configuration()
return {
"model_type": "ollama",
"model_id": self.model_id,
Expand Down
Loading