Skip to content

Commit c60483b

Browse files
fix(llm): consolidate Azure-OpenAI key resolution across 4 call sites
Pre-fix, the Azure-OpenAI provider in rag/llm had 4 unfixed call sites with 3 different patterns for parsing the key: - chat (chat_model.py:1649-1651): bare json.loads(key).get(...) with no try/except; crashes with JSONDecodeError on a plain Portal API key (the most common user mistake) and AttributeError on any JSON non-object input. - vision / CV (cv_model.py:380): local helper with silent fallback for non-object JSON, silently using the raw key string. - embed (embedding_model.py:325): identical local helper, a duplicate copy of the CV one. - seq2txt (sequence2txt_model.py:386): raw key passed straight to AzureOpenAI; a JSON string was used as the api_key and the call silently failed at the API with a 401. Unify all 4 through a single _resolve_azure_credentials helper in rag/llm/key_utils.py that follows the same pattern as the other 5 helpers in the JSON-decode family (Bedrock, BaiduYiyan, VolcEngine, OpenRouter, GoogleCV): 1. Accepts a dict (returned verbatim) or a JSON-string-encoded dict. 2. On non-JSON input, raises a clear ModelException (retryable=False) naming the required fields and pointing at conf/models/azure.json, instead of letting json.loads bubble up as JSONDecodeError. 3. On a JSON top-level type that is not a dict (list, string, number, bool, null), raises the same clear ModelException instead of calling .get('api_key') on the value and getting AttributeError. Returns (api_key, api_version) where api_version defaults to '2024-02-01' and api_key defaults to '' if missing. The two duplicate _resolve_azure_credentials definitions in cv_model.py and embedding_model.py are removed in favor of the shared helper. Fixes infiniflow#17675. Supersedes the partial infiniflow#17215 (which only touched the chat branch and used a silent-fallback helper).
1 parent c87aa3b commit c60483b

5 files changed

Lines changed: 101 additions & 28 deletions

File tree

rag/llm/chat_model.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from common.llm_request_context import current_llm_user
3737
from common.token_utils import num_tokens_from_string, total_token_count_from_response, usage_from_response
3838
from rag.llm import FACTORY_DEFAULT_BASE_URL, LITELLM_PROVIDER_PREFIX, SupportedLiteLLMProvider
39-
from rag.llm.key_utils import _normalize_replicate_key
39+
from rag.llm.key_utils import _normalize_replicate_key, _resolve_azure_credentials
4040
from rag.llm.mws_utils import mws_api_url, require_mws_token
4141
from rag.llm.tool_decorator import FunctionToolSession, is_tool
4242
from rag.nlp import is_chinese, is_english
@@ -1835,8 +1835,17 @@ def __init__(self, key, model_name, base_url=None, **kwargs):
18351835
self.api_key = key
18361836
self.provider_order = ""
18371837
elif self.provider == SupportedLiteLLMProvider.Azure_OpenAI:
1838-
self.api_key = json.loads(key).get("api_key", "")
1839-
self.api_version = json.loads(key).get("api_version", "2024-02-01")
1838+
# Parse the key via the shared helper. Pre-fix, the bare
1839+
# ``json.loads(key).get(...)`` calls crashed with
1840+
# ``json.decoder.JSONDecodeError`` on a plain API key
1841+
# (the most common Azure Portal mistake) and
1842+
# ``AttributeError`` on any JSON non-object input. The
1843+
# helper raises a clear ``ModelException`` instead.
1844+
# PR #17215 was a partial fix that only modified this
1845+
# branch and used a silent-fallback helper. This PR
1846+
# consolidates the Azure-OpenAI pattern with the other 5
1847+
# helpers in ``rag/llm/key_utils.py``. See #17675.
1848+
self.api_key, self.api_version = _resolve_azure_credentials(key)
18401849
elif self.provider == SupportedLiteLLMProvider.MiniMax:
18411850
# MiniMax requires GroupId as a query parameter for API authentication
18421851
try:

rag/llm/cv_model.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
from common.aimlapi_utils import attribution_headers
3434
from common.token_utils import num_tokens_from_string, total_token_count_from_response
35+
from rag.llm.key_utils import _resolve_azure_credentials
3536
from rag.nlp import is_english
3637
from rag.prompts.generator import vision_llm_describe_prompt
3738
from rag.utils.url_utils import ensure_v1
@@ -362,21 +363,17 @@ def describe_with_prompt(self, image, prompt=None):
362363
return res.choices[0].message.content.strip(), total_token_count_from_response(res)
363364

364365

365-
def _resolve_azure_credentials(key):
366-
try:
367-
key_obj = json.loads(key)
368-
if isinstance(key_obj, dict):
369-
return key_obj.get("api_key", ""), key_obj.get("api_version", "2024-02-01")
370-
logging.warning("Azure credential payload parsed as JSON but is not an object; using raw api_key string")
371-
except (json.JSONDecodeError, TypeError):
372-
logging.warning("Azure credential payload is not valid JSON; using raw api_key string")
373-
return key, "2024-02-01"
374-
375-
376366
class AzureGptV4(GptV4):
377367
_FACTORY_NAME = "Azure-OpenAI"
378368

379369
def __init__(self, key, model_name, lang="Chinese", **kwargs):
370+
# Parse the key via the shared helper in rag.llm.key_utils. The
371+
# local copy of ``_resolve_azure_credentials`` was removed in
372+
# favor of the shared helper to consolidate the Azure-OpenAI
373+
# pattern with the other 5 providers (Bedrock, BaiduYiyan,
374+
# VolcEngine, OpenRouter, GoogleCV) and align the silent
375+
# fallback for JSON non-object input with a clear
376+
# ``ModelException``. See #17675.
380377
api_key, api_version = _resolve_azure_credentials(key)
381378
self.base_url = ensure_v1(kwargs["base_url"])
382379
self.client = AzureOpenAI(api_key=api_key, azure_endpoint=self.base_url, api_version=api_version)

rag/llm/embedding_model.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from common.aimlapi_utils import attribution_headers
3333
from common.exceptions import ModelException
3434
from common.token_utils import num_tokens_from_string, truncate, total_token_count_from_response
35-
from rag.llm.key_utils import _normalize_replicate_key
35+
from rag.llm.key_utils import _normalize_replicate_key, _resolve_azure_credentials
3636
from rag.llm.mws_utils import mws_api_url, require_mws_token
3737
from rag.utils.url_utils import append_api_path, ensure_v1
3838
import logging
@@ -306,23 +306,19 @@ def encode_queries(self, text):
306306
return vectors[0], token_count
307307

308308

309-
def _resolve_azure_credentials(key):
310-
try:
311-
key_obj = json.loads(key)
312-
if isinstance(key_obj, dict):
313-
return key_obj.get("api_key", ""), key_obj.get("api_version", "2024-02-01")
314-
logging.warning("Azure credential payload parsed as JSON but is not an object; using raw api_key string")
315-
except (json.JSONDecodeError, TypeError):
316-
logging.warning("Azure credential payload is not valid JSON; using raw api_key string")
317-
return key, "2024-02-01"
318-
319-
320309
class AzureEmbed(OpenAIEmbed):
321310
_FACTORY_NAME = "Azure-OpenAI"
322311

323312
def __init__(self, key, model_name, **kwargs):
324313
from openai.lib.azure import AzureOpenAI
325314

315+
# Parse the key via the shared helper in rag.llm.key_utils. The
316+
# local copy of ``_resolve_azure_credentials`` was removed in
317+
# favor of the shared helper to consolidate the Azure-OpenAI
318+
# pattern with the other 5 providers (Bedrock, BaiduYiyan,
319+
# VolcEngine, OpenRouter, GoogleCV) and align the silent
320+
# fallback for JSON non-object input with a clear
321+
# ``ModelException``. See #17675.
326322
api_key, api_version = _resolve_azure_credentials(key)
327323
self.base_url = ensure_v1(kwargs["base_url"])
328324
self.client = AzureOpenAI(api_key=api_key, azure_endpoint=self.base_url, api_version=api_version)

rag/llm/key_utils.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
# limitations under the License.
1515
#
1616
import json
17+
import logging
18+
19+
from common.exceptions import ModelException
1720

1821

1922
def _normalize_replicate_key(key):
@@ -31,4 +34,63 @@ def _normalize_replicate_key(key):
3134
return key
3235

3336

34-
__all__ = ["_normalize_replicate_key"]
37+
def _resolve_azure_credentials(key):
38+
"""Parse an Azure-OpenAI ``key`` and return ``(api_key, api_version)``.
39+
40+
The Azure-OpenAI provider requires a JSON object key with at least
41+
``api_key``; ``api_version`` defaults to ``"2024-02-01"`` if missing.
42+
See ``conf/llm_factories.json`` for the factory entry and
43+
``conf/models/azure.json`` for the model class.
44+
45+
On non-JSON input -- for example a user pasting a plain Azure API
46+
key like ``"abc...123"`` from the Azure Portal -- we raise a clear
47+
:class:`ModelException` pointing at the required schema, instead of
48+
letting ``json.loads`` bubble up as ``json.decoder.JSONDecodeError:
49+
Expecting value: line 1 column 1 (char 0)`` from inside ``rag/llm``
50+
internals. Mirrors the fix shape of ``_resolve_bedrock_credentials``
51+
for #17373 and the Azure fix for #17204 / #17215.
52+
53+
On a JSON top-level type that is not a dict (e.g. a list, string, or
54+
number), we also raise the same clear error rather than calling
55+
``.get("api_key")`` on the value and getting an ``AttributeError``.
56+
57+
Returns ``(api_key, api_version)``. Both default to ``""`` and
58+
``"2024-02-01"`` respectively if missing from a valid dict.
59+
"""
60+
if isinstance(key, dict):
61+
payload = key
62+
elif isinstance(key, str):
63+
try:
64+
payload = json.loads(key)
65+
except (json.JSONDecodeError, TypeError):
66+
logging.warning(
67+
"Azure-OpenAI key is not valid JSON; expected a JSON object with 'api_key' (and optionally 'api_version') (see conf/models/azure.json).",
68+
)
69+
raise ModelException(
70+
'Azure-OpenAI requires a JSON key with at least \'api_key\'. Example: {"api_key": "...", "api_version": "2024-02-01"}. See conf/models/azure.json for the model class.',
71+
retryable=False,
72+
)
73+
else:
74+
logging.warning(
75+
"Azure-OpenAI key is not a string or dict (got %s); expected a JSON object with 'api_key' (and optionally 'api_version').",
76+
type(key).__name__,
77+
)
78+
raise ModelException(
79+
f"Azure-OpenAI requires a JSON key, got {type(key).__name__}. See conf/models/azure.json for the model class.",
80+
retryable=False,
81+
)
82+
83+
if not isinstance(payload, dict):
84+
logging.warning(
85+
"Azure-OpenAI key parsed as JSON but is not a dict (got %s).",
86+
type(payload).__name__,
87+
)
88+
raise ModelException(
89+
f"Azure-OpenAI key must be a JSON object, got {type(payload).__name__}. See conf/models/azure.json for the model class.",
90+
retryable=False,
91+
)
92+
93+
return payload.get("api_key", ""), payload.get("api_version", "2024-02-01")
94+
95+
96+
__all__ = ["_normalize_replicate_key", "_resolve_azure_credentials"]

rag/llm/sequence2txt_model.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,8 +394,17 @@ class AzureSeq2txt(Base):
394394
_FACTORY_NAME = "Azure-OpenAI"
395395

396396
def __init__(self, key, model_name, lang="Chinese", **kwargs):
397+
from rag.llm.key_utils import _resolve_azure_credentials
398+
399+
# Parse the key via the shared helper. Pre-fix, the raw ``key``
400+
# was passed straight to the AzureOpenAI client, so a JSON
401+
# string like ``"{\"api_key\": \"...\"}"`` was used as the
402+
# ``api_key`` and the call silently failed at the API with a
403+
# 401. The helper raises a clear ``ModelException`` on
404+
# non-JSON or JSON non-object input. See #17675.
405+
api_key, api_version = _resolve_azure_credentials(key)
397406
self.base_url = ensure_v1(kwargs["base_url"])
398-
self.client = AzureOpenAI(api_key=key, azure_endpoint=self.base_url, api_version="2024-02-01")
407+
self.client = AzureOpenAI(api_key=api_key, azure_endpoint=self.base_url, api_version=api_version)
399408
self.model_name = model_name
400409
self.lang = lang
401410

0 commit comments

Comments
 (0)