From 94e33e311dbb9d56b113a64eba3f3b9e04a9fbcd Mon Sep 17 00:00:00 2001 From: Mohammed Alkindi Date: Wed, 7 Oct 2026 16:01:07 -0700 Subject: [PATCH 1/3] fix(python): treat blank API keys as unset when choosing a model A blank OPENAI_API_KEY, as left by copying .env.example and filling in only the Azure section, chose OpenAI and sent an empty bearer token. Keys are now chosen by value, as typescript/src/model.ts does. --- python/src/typechat/_internal/model.py | 8 ++-- python/tests/test_create_language_model.py | 50 ++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 python/tests/test_create_language_model.py diff --git a/python/src/typechat/_internal/model.py b/python/src/typechat/_internal/model.py index 471404f0..b1206b0e 100644 --- a/python/src/typechat/_internal/model.py +++ b/python/src/typechat/_internal/model.py @@ -157,10 +157,10 @@ def create_language_model(vals: dict[str, str | None]) -> HttpxLanguageModel: Creates a language model encapsulation of an OpenAI or Azure OpenAI REST API endpoint chosen by a dictionary of variables (typically just `os.environ`). - If an `OPENAI_API_KEY` environment variable exists, an OpenAI model is constructed. + If an `OPENAI_API_KEY` environment variable is set and non-empty, an OpenAI model is constructed. The `OPENAI_ENDPOINT` and `OPENAI_MODEL` environment variables must also be defined or an error will be raised. - If an `AZURE_OPENAI_API_KEY` environment variable exists, an Azure OpenAI model is constructed. + Otherwise, if an `AZURE_OPENAI_API_KEY` environment variable is set and non-empty, an Azure OpenAI model is constructed. The `AZURE_OPENAI_ENDPOINT` environment variable must also be defined or an exception will be thrown. If none of these key variables are defined, an exception is thrown. @@ -176,14 +176,14 @@ def required_var(name: str) -> str: raise ValueError(f"Missing environment variable {name}.") return val - if "OPENAI_API_KEY" in vals: + if vals.get("OPENAI_API_KEY", None): api_key = required_var("OPENAI_API_KEY") model = required_var("OPENAI_MODEL") endpoint = vals.get("OPENAI_ENDPOINT", None) or "https://api.openai.com/v1/chat/completions" org = vals.get("OPENAI_ORG", None) or "" return create_openai_language_model(api_key, model, endpoint, org) - elif "AZURE_OPENAI_API_KEY" in vals: + elif vals.get("AZURE_OPENAI_API_KEY", None): api_key=required_var("AZURE_OPENAI_API_KEY") endpoint=required_var("AZURE_OPENAI_ENDPOINT") return create_azure_openai_language_model(api_key, endpoint) diff --git a/python/tests/test_create_language_model.py b/python/tests/test_create_language_model.py new file mode 100644 index 00000000..2e2a40b4 --- /dev/null +++ b/python/tests/test_create_language_model.py @@ -0,0 +1,50 @@ +""" +Tests for create_language_model's selection of a model from environment variables. + +These only inspect the constructed model, so no request is sent. +""" + +import pytest + +import typechat + +_AZURE_ENDPOINT = "https://myres.openai.azure.com/openai/deployments/gpt4/chat/completions?api-version=2023-05-15" + + +def test_blank_openai_key_with_azure_configured_selects_azure(): + # The shape produced by copying .env.example and filling in only the Azure section. + model = typechat.create_language_model( + { + "OPENAI_MODEL": "", + "OPENAI_API_KEY": "", + "AZURE_OPENAI_ENDPOINT": _AZURE_ENDPOINT, + "AZURE_OPENAI_API_KEY": "azure-secret", + } + ) + assert model.url == _AZURE_ENDPOINT + assert model.headers["api-key"] == "azure-secret" + + +def test_openai_key_takes_precedence_over_azure_key(): + model = typechat.create_language_model( + { + "OPENAI_MODEL": "gpt-4o", + "OPENAI_API_KEY": "openai-secret", + "AZURE_OPENAI_ENDPOINT": _AZURE_ENDPOINT, + "AZURE_OPENAI_API_KEY": "azure-secret", + } + ) + assert model.url == "https://api.openai.com/v1/chat/completions" + assert model.headers["Authorization"] == "Bearer openai-secret" + assert "api-key" not in model.headers + + +def test_blank_keys_are_treated_as_missing(): + with pytest.raises(ValueError, match="OPENAI_API_KEY or AZURE_OPENAI_API_KEY"): + typechat.create_language_model( + { + "OPENAI_API_KEY": "", + "AZURE_OPENAI_ENDPOINT": _AZURE_ENDPOINT, + "AZURE_OPENAI_API_KEY": "", + } + ) From 3e45a953021ac07a6beb1b5e5d123bcdcbcc746e Mon Sep 17 00:00:00 2001 From: Mohammed Alkindi Date: Wed, 7 Oct 2026 16:01:44 -0700 Subject: [PATCH 2/3] fix(python): read OPENAI_ORGANIZATION for the organization header The Python port read only OPENAI_ORG, while the README and the TypeScript port use OPENAI_ORGANIZATION. OPENAI_ORG is still read as a fallback. --- python/src/typechat/_internal/model.py | 2 +- python/tests/test_create_language_model.py | 34 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/python/src/typechat/_internal/model.py b/python/src/typechat/_internal/model.py index b1206b0e..46d4c832 100644 --- a/python/src/typechat/_internal/model.py +++ b/python/src/typechat/_internal/model.py @@ -180,7 +180,7 @@ def required_var(name: str) -> str: api_key = required_var("OPENAI_API_KEY") model = required_var("OPENAI_MODEL") endpoint = vals.get("OPENAI_ENDPOINT", None) or "https://api.openai.com/v1/chat/completions" - org = vals.get("OPENAI_ORG", None) or "" + org = vals.get("OPENAI_ORGANIZATION", None) or vals.get("OPENAI_ORG", None) or "" return create_openai_language_model(api_key, model, endpoint, org) elif vals.get("AZURE_OPENAI_API_KEY", None): diff --git a/python/tests/test_create_language_model.py b/python/tests/test_create_language_model.py index 2e2a40b4..22e55faf 100644 --- a/python/tests/test_create_language_model.py +++ b/python/tests/test_create_language_model.py @@ -48,3 +48,37 @@ def test_blank_keys_are_treated_as_missing(): "AZURE_OPENAI_API_KEY": "", } ) + + +def test_openai_organization_is_sent_as_organization_header(): + model = typechat.create_language_model( + { + "OPENAI_MODEL": "gpt-4o", + "OPENAI_API_KEY": "openai-secret", + "OPENAI_ORGANIZATION": "org-123", + } + ) + assert model.headers["OpenAI-Organization"] == "org-123" + + +def test_openai_org_is_still_sent_as_organization_header(): + model = typechat.create_language_model( + { + "OPENAI_MODEL": "gpt-4o", + "OPENAI_API_KEY": "openai-secret", + "OPENAI_ORG": "org-legacy", + } + ) + assert model.headers["OpenAI-Organization"] == "org-legacy" + + +def test_openai_organization_takes_precedence_over_openai_org(): + model = typechat.create_language_model( + { + "OPENAI_MODEL": "gpt-4o", + "OPENAI_API_KEY": "openai-secret", + "OPENAI_ORGANIZATION": "org-123", + "OPENAI_ORG": "org-legacy", + } + ) + assert model.headers["OpenAI-Organization"] == "org-123" From f61c6bf915788880b4dcc18b6b6f274dad00e4ef Mon Sep 17 00:00:00 2001 From: Mohammed Alkindi Date: Mon, 12 Oct 2026 11:01:29 -0700 Subject: [PATCH 3/3] fix(python): keep presence-based model selection Reverts the blank-key change from 94e33e3 so a present but empty OPENAI_API_KEY still selects OpenAI, as it did before. Callers that supply credentials later or rely on the current selection keep working. This PR now only adds the OPENAI_ORGANIZATION fallback. --- python/src/typechat/_internal/model.py | 8 ++-- python/tests/test_create_language_model.py | 46 +--------------------- 2 files changed, 5 insertions(+), 49 deletions(-) diff --git a/python/src/typechat/_internal/model.py b/python/src/typechat/_internal/model.py index 46d4c832..42e412a1 100644 --- a/python/src/typechat/_internal/model.py +++ b/python/src/typechat/_internal/model.py @@ -157,10 +157,10 @@ def create_language_model(vals: dict[str, str | None]) -> HttpxLanguageModel: Creates a language model encapsulation of an OpenAI or Azure OpenAI REST API endpoint chosen by a dictionary of variables (typically just `os.environ`). - If an `OPENAI_API_KEY` environment variable is set and non-empty, an OpenAI model is constructed. + If an `OPENAI_API_KEY` environment variable exists, an OpenAI model is constructed. The `OPENAI_ENDPOINT` and `OPENAI_MODEL` environment variables must also be defined or an error will be raised. - Otherwise, if an `AZURE_OPENAI_API_KEY` environment variable is set and non-empty, an Azure OpenAI model is constructed. + If an `AZURE_OPENAI_API_KEY` environment variable exists, an Azure OpenAI model is constructed. The `AZURE_OPENAI_ENDPOINT` environment variable must also be defined or an exception will be thrown. If none of these key variables are defined, an exception is thrown. @@ -176,14 +176,14 @@ def required_var(name: str) -> str: raise ValueError(f"Missing environment variable {name}.") return val - if vals.get("OPENAI_API_KEY", None): + if "OPENAI_API_KEY" in vals: api_key = required_var("OPENAI_API_KEY") model = required_var("OPENAI_MODEL") endpoint = vals.get("OPENAI_ENDPOINT", None) or "https://api.openai.com/v1/chat/completions" org = vals.get("OPENAI_ORGANIZATION", None) or vals.get("OPENAI_ORG", None) or "" return create_openai_language_model(api_key, model, endpoint, org) - elif vals.get("AZURE_OPENAI_API_KEY", None): + elif "AZURE_OPENAI_API_KEY" in vals: api_key=required_var("AZURE_OPENAI_API_KEY") endpoint=required_var("AZURE_OPENAI_ENDPOINT") return create_azure_openai_language_model(api_key, endpoint) diff --git a/python/tests/test_create_language_model.py b/python/tests/test_create_language_model.py index 22e55faf..62dbe01c 100644 --- a/python/tests/test_create_language_model.py +++ b/python/tests/test_create_language_model.py @@ -1,55 +1,11 @@ """ -Tests for create_language_model's selection of a model from environment variables. +Tests for create_language_model's handling of the OpenAI organization variables. These only inspect the constructed model, so no request is sent. """ -import pytest - import typechat -_AZURE_ENDPOINT = "https://myres.openai.azure.com/openai/deployments/gpt4/chat/completions?api-version=2023-05-15" - - -def test_blank_openai_key_with_azure_configured_selects_azure(): - # The shape produced by copying .env.example and filling in only the Azure section. - model = typechat.create_language_model( - { - "OPENAI_MODEL": "", - "OPENAI_API_KEY": "", - "AZURE_OPENAI_ENDPOINT": _AZURE_ENDPOINT, - "AZURE_OPENAI_API_KEY": "azure-secret", - } - ) - assert model.url == _AZURE_ENDPOINT - assert model.headers["api-key"] == "azure-secret" - - -def test_openai_key_takes_precedence_over_azure_key(): - model = typechat.create_language_model( - { - "OPENAI_MODEL": "gpt-4o", - "OPENAI_API_KEY": "openai-secret", - "AZURE_OPENAI_ENDPOINT": _AZURE_ENDPOINT, - "AZURE_OPENAI_API_KEY": "azure-secret", - } - ) - assert model.url == "https://api.openai.com/v1/chat/completions" - assert model.headers["Authorization"] == "Bearer openai-secret" - assert "api-key" not in model.headers - - -def test_blank_keys_are_treated_as_missing(): - with pytest.raises(ValueError, match="OPENAI_API_KEY or AZURE_OPENAI_API_KEY"): - typechat.create_language_model( - { - "OPENAI_API_KEY": "", - "AZURE_OPENAI_ENDPOINT": _AZURE_ENDPOINT, - "AZURE_OPENAI_API_KEY": "", - } - ) - - def test_openai_organization_is_sent_as_organization_header(): model = typechat.create_language_model( {