feat(config): support keyless local model endpoints
- Add an explicit no-key option for local model configurations - Persist keyless profiles while retaining cloud credential validation - Use an internal SDK placeholder only for keyless endpoints - Cover keyless vault storage and environment synchronization
This commit is contained in:
parent
df04f44db4
commit
e504c5aae3
4 changed files with 134 additions and 26 deletions
|
|
@ -66,7 +66,9 @@ class ModelFetchThread(QThread):
|
||||||
|
|
||||||
def _fetch_openai(self):
|
def _fetch_openai(self):
|
||||||
import openai
|
import openai
|
||||||
kwargs = {"api_key": self.api_key}
|
# The SDK requires a non-empty value even when a local server ignores
|
||||||
|
# authentication entirely.
|
||||||
|
kwargs = {"api_key": self.api_key or "not-needed"}
|
||||||
if self.api_url:
|
if self.api_url:
|
||||||
kwargs["base_url"] = self.api_url
|
kwargs["base_url"] = self.api_url
|
||||||
client = openai.OpenAI(**kwargs)
|
client = openai.OpenAI(**kwargs)
|
||||||
|
|
@ -128,6 +130,7 @@ class ApiKeyEditDialog(QDialog):
|
||||||
initial_endpoint: str = "",
|
initial_endpoint: str = "",
|
||||||
*,
|
*,
|
||||||
allow_blank_secret: bool = False,
|
allow_blank_secret: bool = False,
|
||||||
|
initial_keyless: bool = False,
|
||||||
):
|
):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("API Key")
|
self.setWindowTitle("API Key")
|
||||||
|
|
@ -153,6 +156,14 @@ class ApiKeyEditDialog(QDialog):
|
||||||
self.secret_edit.setPlaceholderText("Paste API key secret")
|
self.secret_edit.setPlaceholderText("Paste API key secret")
|
||||||
form.addRow("Secret:", self.secret_edit)
|
form.addRow("Secret:", self.secret_edit)
|
||||||
|
|
||||||
|
self.keyless_cb = QCheckBox("No API key required (local model)")
|
||||||
|
self.keyless_cb.setChecked(initial_keyless)
|
||||||
|
self.keyless_cb.setToolTip(
|
||||||
|
"Use this for a local OpenAI-compatible server that does not require authentication."
|
||||||
|
)
|
||||||
|
self.keyless_cb.toggled.connect(self._on_keyless_toggled)
|
||||||
|
form.addRow("", self.keyless_cb)
|
||||||
|
|
||||||
endpoint_wrap = QWidget()
|
endpoint_wrap = QWidget()
|
||||||
endpoint_row = QHBoxLayout(endpoint_wrap)
|
endpoint_row = QHBoxLayout(endpoint_wrap)
|
||||||
endpoint_row.setContentsMargins(0, 0, 0, 0)
|
endpoint_row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
@ -202,27 +213,47 @@ class ApiKeyEditDialog(QDialog):
|
||||||
self._name = ""
|
self._name = ""
|
||||||
self._secret = ""
|
self._secret = ""
|
||||||
self._endpoint = ""
|
self._endpoint = ""
|
||||||
|
self._keyless = False
|
||||||
|
self._on_keyless_toggled(initial_keyless)
|
||||||
self.secret_edit.setFocus()
|
self.secret_edit.setFocus()
|
||||||
|
|
||||||
|
def _on_keyless_toggled(self, checked: bool):
|
||||||
|
self.secret_edit.setEnabled(not checked)
|
||||||
|
if checked:
|
||||||
|
self.secret_edit.clear()
|
||||||
|
self.secret_edit.setPlaceholderText("Not used for this local endpoint")
|
||||||
|
elif self._allow_blank_secret:
|
||||||
|
self.secret_edit.setPlaceholderText("Leave blank to keep the existing secret")
|
||||||
|
else:
|
||||||
|
self.secret_edit.setPlaceholderText("Paste API key secret")
|
||||||
|
|
||||||
def _accept_if_valid(self):
|
def _accept_if_valid(self):
|
||||||
name = self.name_edit.text().strip()
|
name = self.name_edit.text().strip()
|
||||||
secret = self.secret_edit.text().strip()
|
secret = self.secret_edit.text().strip()
|
||||||
endpoint = self.endpoint_edit.text().strip()
|
endpoint = self.endpoint_edit.text().strip()
|
||||||
|
keyless = self.keyless_cb.isChecked()
|
||||||
if not name:
|
if not name:
|
||||||
QMessageBox.warning(self, "API Key", "Enter a name for this key.")
|
QMessageBox.warning(self, "API Key", "Enter a name for this key.")
|
||||||
self.name_edit.setFocus()
|
self.name_edit.setFocus()
|
||||||
return
|
return
|
||||||
if not secret and not self._allow_blank_secret:
|
if not secret and not keyless and not self._allow_blank_secret:
|
||||||
QMessageBox.warning(self, "API Key", "Enter the API key secret.")
|
QMessageBox.warning(self, "API Key", "Enter the API key secret.")
|
||||||
self.secret_edit.setFocus()
|
self.secret_edit.setFocus()
|
||||||
return
|
return
|
||||||
|
if keyless and not endpoint:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self, "API Key", "Enter the local model server's API endpoint."
|
||||||
|
)
|
||||||
|
self.endpoint_edit.setFocus()
|
||||||
|
return
|
||||||
self._name = name
|
self._name = name
|
||||||
self._secret = secret
|
self._secret = secret
|
||||||
self._endpoint = endpoint
|
self._endpoint = endpoint
|
||||||
|
self._keyless = keyless
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
def result_values(self) -> tuple[str, str, str]:
|
def result_values(self) -> tuple[str, str, str, bool]:
|
||||||
return self._name, self._secret, self._endpoint
|
return self._name, self._secret, self._endpoint, self._keyless
|
||||||
|
|
||||||
|
|
||||||
class ConfigTab(QWidget):
|
class ConfigTab(QWidget):
|
||||||
|
|
@ -792,6 +823,11 @@ class ConfigTab(QWidget):
|
||||||
secret = api_key_vault.get_secret(name)
|
secret = api_key_vault.get_secret(name)
|
||||||
return secret if secret is not None else ""
|
return secret if secret is not None else ""
|
||||||
|
|
||||||
|
def _active_api_key_is_optional(self) -> bool:
|
||||||
|
"""Return whether the selected vault entry is explicitly keyless."""
|
||||||
|
name = self.api_key_combo.currentText().strip()
|
||||||
|
return api_key_vault.is_keyless(name) if name else False
|
||||||
|
|
||||||
def _apply_key_endpoint(self, name: str | None = None):
|
def _apply_key_endpoint(self, name: str | None = None):
|
||||||
"""If the named key has an endpoint, apply it to the API URL field."""
|
"""If the named key has an endpoint, apply it to the API URL field."""
|
||||||
key_name = (name or self.api_key_combo.currentText()).strip()
|
key_name = (name or self.api_key_combo.currentText()).strip()
|
||||||
|
|
@ -846,10 +882,11 @@ class ConfigTab(QWidget):
|
||||||
initial_name=current,
|
initial_name=current,
|
||||||
initial_endpoint=existing_endpoint or self.api_url_edit.text().strip(),
|
initial_endpoint=existing_endpoint or self.api_url_edit.text().strip(),
|
||||||
allow_blank_secret=bool(current and api_key_vault.get_secret(current)),
|
allow_blank_secret=bool(current and api_key_vault.get_secret(current)),
|
||||||
|
initial_keyless=bool(current and api_key_vault.is_keyless(current)),
|
||||||
)
|
)
|
||||||
if dialog.exec_() != QDialog.Accepted:
|
if dialog.exec_() != QDialog.Accepted:
|
||||||
return
|
return
|
||||||
name, secret, endpoint = dialog.result_values()
|
name, secret, endpoint, keyless = dialog.result_values()
|
||||||
try:
|
try:
|
||||||
api_key_vault.upsert_key(
|
api_key_vault.upsert_key(
|
||||||
name,
|
name,
|
||||||
|
|
@ -857,6 +894,7 @@ class ConfigTab(QWidget):
|
||||||
endpoint=endpoint,
|
endpoint=endpoint,
|
||||||
make_active=True,
|
make_active=True,
|
||||||
keep_secret_if_blank=True,
|
keep_secret_if_blank=True,
|
||||||
|
keyless=keyless,
|
||||||
)
|
)
|
||||||
self._apply_key_endpoint(name)
|
self._apply_key_endpoint(name)
|
||||||
api_key_vault.sync_active_to_env(env_path=self.env_file_path)
|
api_key_vault.sync_active_to_env(env_path=self.env_file_path)
|
||||||
|
|
@ -890,7 +928,7 @@ class ConfigTab(QWidget):
|
||||||
api_key = self._active_api_key_secret()
|
api_key = self._active_api_key_secret()
|
||||||
api_url = self.api_url_edit.text().strip()
|
api_url = self.api_url_edit.text().strip()
|
||||||
|
|
||||||
if not api_key:
|
if not api_key and not self._active_api_key_is_optional():
|
||||||
if not silent:
|
if not silent:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self, "No API Key",
|
self, "No API Key",
|
||||||
|
|
@ -1107,6 +1145,9 @@ class ConfigTab(QWidget):
|
||||||
config = {
|
config = {
|
||||||
"api": self.api_url_edit.text().strip(),
|
"api": self.api_url_edit.text().strip(),
|
||||||
"key": self._active_api_key_secret(),
|
"key": self._active_api_key_secret(),
|
||||||
|
"API_KEY_OPTIONAL": (
|
||||||
|
"true" if self._active_api_key_is_optional() else "false"
|
||||||
|
),
|
||||||
"model": self.model_combo.currentText(),
|
"model": self.model_combo.currentText(),
|
||||||
"language": self.language_combo.currentText(),
|
"language": self.language_combo.currentText(),
|
||||||
"timeout": str(self.timeout_spin.value()),
|
"timeout": str(self.timeout_spin.value()),
|
||||||
|
|
@ -1241,7 +1282,7 @@ class ConfigTab(QWidget):
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
# Check required fields
|
# Check required fields
|
||||||
if not self._active_api_key_secret():
|
if not self._active_api_key_secret() and not self._active_api_key_is_optional():
|
||||||
errors.append("API Key is required")
|
errors.append("API Key is required")
|
||||||
|
|
||||||
if not self.model_combo.currentText().strip():
|
if not self.model_combo.currentText().strip():
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ class ApiKeyVaultTests(unittest.TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(secret, "sk-work")
|
self.assertEqual(secret, "sk-work")
|
||||||
self.assertEqual(os.environ.get("key"), "sk-work")
|
self.assertEqual(os.environ.get("key"), "sk-work")
|
||||||
|
self.assertEqual(os.environ.get("API_KEY_OPTIONAL"), "false")
|
||||||
self.assertEqual(os.environ.get("api"), "https://api.work.test/v1")
|
self.assertEqual(os.environ.get("api"), "https://api.work.test/v1")
|
||||||
text = self.env_path.read_text(encoding="utf-8")
|
text = self.env_path.read_text(encoding="utf-8")
|
||||||
self.assertIn("sk-work", text)
|
self.assertIn("sk-work", text)
|
||||||
|
|
@ -162,6 +163,36 @@ class ApiKeyVaultTests(unittest.TestCase):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
api_keys.upsert_key("Name", " ", path=self.vault_path)
|
api_keys.upsert_key("Name", " ", path=self.vault_path)
|
||||||
|
|
||||||
|
def test_keyless_local_endpoint_roundtrip_and_sync(self):
|
||||||
|
api_keys.upsert_key(
|
||||||
|
"Local",
|
||||||
|
"",
|
||||||
|
endpoint="http://127.0.0.1:1234/v1",
|
||||||
|
keyless=True,
|
||||||
|
path=self.vault_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(api_keys.list_names(self.vault_path), ["Local"])
|
||||||
|
self.assertEqual(api_keys.get_active_secret(self.vault_path), "")
|
||||||
|
self.assertTrue(api_keys.is_keyless("Local", self.vault_path))
|
||||||
|
self.assertTrue(api_keys.is_active_keyless(self.vault_path))
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {}, clear=False):
|
||||||
|
api_keys.sync_active_to_env(
|
||||||
|
vault_path=self.vault_path,
|
||||||
|
env_path=self.env_path,
|
||||||
|
)
|
||||||
|
self.assertEqual(os.environ.get("key"), "")
|
||||||
|
self.assertEqual(os.environ.get("API_KEY_OPTIONAL"), "true")
|
||||||
|
text = self.env_path.read_text(encoding="utf-8")
|
||||||
|
self.assertIn("API_KEY_OPTIONAL='true'", text)
|
||||||
|
|
||||||
|
def test_keyless_entry_requires_endpoint(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
api_keys.upsert_key(
|
||||||
|
"Local", "", keyless=True, path=self.vault_path
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@
|
||||||
Secrets live in ``data/api_keys.json`` (user-local). The active secret is also
|
Secrets live in ``data/api_keys.json`` (user-local). The active secret is also
|
||||||
mirrored to ``.env`` ``key=`` so translation and subprocesses keep working.
|
mirrored to ``.env`` ``key=`` so translation and subprocesses keep working.
|
||||||
|
|
||||||
Each named entry may optionally include an ``endpoint`` (API base URL). When the
|
Each named entry may optionally include an ``endpoint`` (API base URL), and may
|
||||||
active key has one, it is mirrored to ``.env`` ``api=`` as well.
|
be marked ``keyless`` for a local server that does not authenticate requests.
|
||||||
|
When the active entry has an endpoint, it is mirrored to ``.env`` ``api=`` as
|
||||||
|
well.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -25,18 +27,19 @@ def _empty_vault() -> dict[str, Any]:
|
||||||
return {"active": "", "keys": {}}
|
return {"active": "", "keys": {}}
|
||||||
|
|
||||||
|
|
||||||
def _normalize_entry(raw: Any) -> dict[str, str]:
|
def _normalize_entry(raw: Any) -> dict[str, Any]:
|
||||||
"""Accept legacy plain-string secrets or ``{secret, endpoint}`` objects."""
|
"""Accept legacy plain-string secrets or ``{secret, endpoint}`` objects."""
|
||||||
if isinstance(raw, str):
|
if isinstance(raw, str):
|
||||||
return {"secret": raw, "endpoint": ""}
|
return {"secret": raw, "endpoint": "", "keyless": False}
|
||||||
if isinstance(raw, dict):
|
if isinstance(raw, dict):
|
||||||
secret = raw.get("secret", raw.get("key", ""))
|
secret = raw.get("secret", raw.get("key", ""))
|
||||||
endpoint = raw.get("endpoint", raw.get("api", ""))
|
endpoint = raw.get("endpoint", raw.get("api", ""))
|
||||||
return {
|
return {
|
||||||
"secret": "" if secret is None else str(secret),
|
"secret": "" if secret is None else str(secret),
|
||||||
"endpoint": ("" if endpoint is None else str(endpoint)).strip(),
|
"endpoint": ("" if endpoint is None else str(endpoint)).strip(),
|
||||||
|
"keyless": bool(raw.get("keyless", False)),
|
||||||
}
|
}
|
||||||
return {"secret": "", "endpoint": ""}
|
return {"secret": "", "endpoint": "", "keyless": False}
|
||||||
|
|
||||||
|
|
||||||
def load_vault(path: Path | None = None) -> dict[str, Any]:
|
def load_vault(path: Path | None = None) -> dict[str, Any]:
|
||||||
|
|
@ -53,12 +56,12 @@ def load_vault(path: Path | None = None) -> dict[str, Any]:
|
||||||
keys = data.get("keys")
|
keys = data.get("keys")
|
||||||
if not isinstance(keys, dict):
|
if not isinstance(keys, dict):
|
||||||
keys = {}
|
keys = {}
|
||||||
cleaned: dict[str, dict[str, str]] = {}
|
cleaned: dict[str, dict[str, Any]] = {}
|
||||||
for name, raw in keys.items():
|
for name, raw in keys.items():
|
||||||
if not isinstance(name, str) or not name.strip():
|
if not isinstance(name, str) or not name.strip():
|
||||||
continue
|
continue
|
||||||
entry = _normalize_entry(raw)
|
entry = _normalize_entry(raw)
|
||||||
if not entry["secret"]:
|
if not entry["secret"] and not entry["keyless"]:
|
||||||
continue
|
continue
|
||||||
cleaned[name.strip()] = entry
|
cleaned[name.strip()] = entry
|
||||||
active = data.get("active") or ""
|
active = data.get("active") or ""
|
||||||
|
|
@ -76,14 +79,15 @@ def save_vault(vault: dict[str, Any], path: Path | None = None) -> None:
|
||||||
"""Write vault JSON with restrictive permissions when the OS allows."""
|
"""Write vault JSON with restrictive permissions when the OS allows."""
|
||||||
vault_path = path or API_KEYS_PATH
|
vault_path = path or API_KEYS_PATH
|
||||||
vault_path.parent.mkdir(parents=True, exist_ok=True)
|
vault_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
keys_out: dict[str, dict[str, str]] = {}
|
keys_out: dict[str, dict[str, Any]] = {}
|
||||||
for name, raw in dict(vault.get("keys") or {}).items():
|
for name, raw in dict(vault.get("keys") or {}).items():
|
||||||
entry = _normalize_entry(raw)
|
entry = _normalize_entry(raw)
|
||||||
if not entry["secret"]:
|
if not entry["secret"] and not entry["keyless"]:
|
||||||
continue
|
continue
|
||||||
keys_out[str(name)] = {
|
keys_out[str(name)] = {
|
||||||
"secret": entry["secret"],
|
"secret": entry["secret"],
|
||||||
"endpoint": entry["endpoint"],
|
"endpoint": entry["endpoint"],
|
||||||
|
"keyless": entry["keyless"],
|
||||||
}
|
}
|
||||||
payload = {
|
payload = {
|
||||||
"active": str(vault.get("active") or ""),
|
"active": str(vault.get("active") or ""),
|
||||||
|
|
@ -112,7 +116,7 @@ def get_active_name(path: Path | None = None) -> str:
|
||||||
return str(load_vault(path).get("active") or "")
|
return str(load_vault(path).get("active") or "")
|
||||||
|
|
||||||
|
|
||||||
def get_entry(name: str, path: Path | None = None) -> dict[str, str] | None:
|
def get_entry(name: str, path: Path | None = None) -> dict[str, Any] | None:
|
||||||
keys = load_vault(path).get("keys") or {}
|
keys = load_vault(path).get("keys") or {}
|
||||||
entry = keys.get(name)
|
entry = keys.get(name)
|
||||||
if entry is None:
|
if entry is None:
|
||||||
|
|
@ -144,6 +148,18 @@ def get_endpoint(name: str, path: Path | None = None) -> str | None:
|
||||||
return entry["endpoint"]
|
return entry["endpoint"]
|
||||||
|
|
||||||
|
|
||||||
|
def is_keyless(name: str, path: Path | None = None) -> bool:
|
||||||
|
"""Return whether *name* explicitly represents a keyless local endpoint."""
|
||||||
|
entry = get_entry(name, path)
|
||||||
|
return bool(entry and entry["keyless"])
|
||||||
|
|
||||||
|
|
||||||
|
def is_active_keyless(path: Path | None = None) -> bool:
|
||||||
|
vault = load_vault(path)
|
||||||
|
active = str(vault.get("active") or "")
|
||||||
|
return is_keyless(active, path) if active else False
|
||||||
|
|
||||||
|
|
||||||
def get_active_endpoint(path: Path | None = None) -> str:
|
def get_active_endpoint(path: Path | None = None) -> str:
|
||||||
vault = load_vault(path)
|
vault = load_vault(path)
|
||||||
active = vault.get("active") or ""
|
active = vault.get("active") or ""
|
||||||
|
|
@ -160,11 +176,13 @@ def upsert_key(
|
||||||
make_active: bool = True,
|
make_active: bool = True,
|
||||||
path: Path | None = None,
|
path: Path | None = None,
|
||||||
keep_secret_if_blank: bool = False,
|
keep_secret_if_blank: bool = False,
|
||||||
|
keyless: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Create or replace a named key. Optionally make it active.
|
"""Create or replace a named key. Optionally make it active.
|
||||||
|
|
||||||
When *keep_secret_if_blank* is true and *secret* is empty, an existing
|
When *keep_secret_if_blank* is true and *secret* is empty, an existing
|
||||||
entry's secret is preserved (useful for editing endpoint only).
|
entry's secret is preserved (useful for editing endpoint only). A keyless
|
||||||
|
entry deliberately stores no secret and must provide a local endpoint.
|
||||||
"""
|
"""
|
||||||
name = (name or "").strip()
|
name = (name or "").strip()
|
||||||
if not name:
|
if not name:
|
||||||
|
|
@ -173,12 +191,21 @@ def upsert_key(
|
||||||
endpoint = (endpoint or "").strip()
|
endpoint = (endpoint or "").strip()
|
||||||
vault = load_vault(path)
|
vault = load_vault(path)
|
||||||
existing = vault["keys"].get(name)
|
existing = vault["keys"].get(name)
|
||||||
if not secret:
|
keyless = bool(keyless)
|
||||||
|
if keyless:
|
||||||
|
secret = ""
|
||||||
|
if not endpoint:
|
||||||
|
raise ValueError("A local API endpoint is required when no API key is used")
|
||||||
|
elif not secret:
|
||||||
if keep_secret_if_blank and existing:
|
if keep_secret_if_blank and existing:
|
||||||
secret = _normalize_entry(existing)["secret"]
|
secret = _normalize_entry(existing)["secret"]
|
||||||
else:
|
else:
|
||||||
raise ValueError("API key secret is required")
|
raise ValueError("API key secret is required")
|
||||||
vault["keys"][name] = {"secret": secret, "endpoint": endpoint}
|
vault["keys"][name] = {
|
||||||
|
"secret": secret,
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"keyless": keyless,
|
||||||
|
}
|
||||||
if make_active or not vault.get("active"):
|
if make_active or not vault.get("active"):
|
||||||
vault["active"] = name
|
vault["active"] = name
|
||||||
save_vault(vault, path)
|
save_vault(vault, path)
|
||||||
|
|
@ -214,7 +241,9 @@ def sync_active_to_env(
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Write the active vault secret to ``.env`` ``key=`` and ``os.environ``.
|
"""Write the active vault secret to ``.env`` ``key=`` and ``os.environ``.
|
||||||
|
|
||||||
When the active key has an endpoint, also write ``api=``.
|
When the active key has an endpoint, also write ``api=``. The explicit
|
||||||
|
``API_KEY_OPTIONAL`` flag lets OpenAI-compatible clients use a harmless
|
||||||
|
placeholder internally for local servers whose HTTP API needs no key.
|
||||||
Returns the secret that was written (may be empty).
|
Returns the secret that was written (may be empty).
|
||||||
"""
|
"""
|
||||||
from dotenv import set_key
|
from dotenv import set_key
|
||||||
|
|
@ -226,6 +255,9 @@ def sync_active_to_env(
|
||||||
target.touch()
|
target.touch()
|
||||||
set_key(target, "key", secret)
|
set_key(target, "key", secret)
|
||||||
os.environ["key"] = secret
|
os.environ["key"] = secret
|
||||||
|
key_optional = "true" if is_active_keyless(vault_path) else "false"
|
||||||
|
set_key(target, "API_KEY_OPTIONAL", key_optional)
|
||||||
|
os.environ["API_KEY_OPTIONAL"] = key_optional
|
||||||
if endpoint:
|
if endpoint:
|
||||||
set_key(target, "api", endpoint)
|
set_key(target, "api", endpoint)
|
||||||
os.environ["api"] = endpoint
|
os.environ["api"] = endpoint
|
||||||
|
|
|
||||||
|
|
@ -392,8 +392,12 @@ else:
|
||||||
if org:
|
if org:
|
||||||
openai.organization = org.strip()
|
openai.organization = org.strip()
|
||||||
|
|
||||||
# Always set API key from 'key' env var (trim whitespace)
|
# The SDK itself requires a non-empty value even when a local compatible server
|
||||||
openai.api_key = os.getenv("key", "").strip()
|
# does not authenticate requests. Only use the placeholder for an explicitly
|
||||||
|
# keyless vault entry so missing cloud credentials still fail normally.
|
||||||
|
_env_key = os.getenv("key", "").strip()
|
||||||
|
_key_optional = os.getenv("API_KEY_OPTIONAL", "").strip().lower() in ("1", "true", "yes")
|
||||||
|
openai.api_key = _env_key or ("not-needed" if _key_optional else "")
|
||||||
|
|
||||||
# Translation cache management
|
# Translation cache management
|
||||||
CACHE_FILE = Path("log/translation_cache.json")
|
CACHE_FILE = Path("log/translation_cache.json")
|
||||||
|
|
@ -2166,8 +2170,8 @@ def translateText(system, user, history, penalty, formatType, model, numLines=No
|
||||||
openai.base_url = "https://api.mistral.ai/v1/"
|
openai.base_url = "https://api.mistral.ai/v1/"
|
||||||
elif _live_api:
|
elif _live_api:
|
||||||
openai.base_url = _normalize_openai_base_url(_live_api)
|
openai.base_url = _normalize_openai_base_url(_live_api)
|
||||||
if _live_key:
|
_live_key_optional = os.getenv("API_KEY_OPTIONAL", "").strip().lower() in ("1", "true", "yes")
|
||||||
openai.api_key = _live_key
|
openai.api_key = _live_key or ("not-needed" if _live_key_optional else "")
|
||||||
|
|
||||||
api_provider = _live_provider
|
api_provider = _live_provider
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue