fix(multimodal): 冻结推理环境并隔离迟到导入错误
This commit is contained in:
@@ -69,9 +69,10 @@ def configure(request):
|
|||||||
return request
|
return request
|
||||||
|
|
||||||
|
|
||||||
def interpreter():
|
def interpreter(config=None):
|
||||||
from app.local_models import components
|
from app.local_models import components
|
||||||
if not os.getenv("APP_MODEL_PYTHON") and configuration().device == "cuda" and components.ready():
|
requested_device = (config or configuration()).device
|
||||||
|
if not os.getenv("APP_MODEL_PYTHON") and requested_device == "cuda" and components.ready():
|
||||||
return components.ROOT / "Scripts/python.exe"
|
return components.ROOT / "Scripts/python.exe"
|
||||||
return Path(os.getenv("APP_MODEL_PYTHON", str(BACKEND_DIR / ".venv-models" / ("Scripts/python.exe" if os.name == "nt" else "bin/python"))))
|
return Path(os.getenv("APP_MODEL_PYTHON", str(BACKEND_DIR / ".venv-models" / ("Scripts/python.exe" if os.name == "nt" else "bin/python"))))
|
||||||
|
|
||||||
@@ -160,7 +161,8 @@ class Runtime:
|
|||||||
async def _execute(self, key, operation, payload, config, diagnostics):
|
async def _execute(self, key, operation, payload, config, diagnostics):
|
||||||
if read_state(key)["status"] != "installed":
|
if read_state(key)["status"] != "installed":
|
||||||
raise ProviderError("LOCAL_MODEL_NOT_INSTALLED", "请先下载本地模型。")
|
raise ProviderError("LOCAL_MODEL_NOT_INSTALLED", "请先下载本地模型。")
|
||||||
if not interpreter().is_file():
|
executable = interpreter(config)
|
||||||
|
if not executable.is_file():
|
||||||
raise ProviderError("LOCAL_RUNTIME_NOT_INSTALLED", "请先安装本地模型运行环境。")
|
raise ProviderError("LOCAL_RUNTIME_NOT_INSTALLED", "请先安装本地模型运行环境。")
|
||||||
from app.services.usage_service import UsageAttempt
|
from app.services.usage_service import UsageAttempt
|
||||||
attempt = UsageAttempt("local-models", CATALOG[key].repository, "local", operation, source="local")
|
attempt = UsageAttempt("local-models", CATALOG[key].repository, "local", operation, source="local")
|
||||||
@@ -170,7 +172,7 @@ class Runtime:
|
|||||||
env = {**os.environ, "HF_HUB_OFFLINE": "1", "TRANSFORMERS_OFFLINE": "1",
|
env = {**os.environ, "HF_HUB_OFFLINE": "1", "TRANSFORMERS_OFFLINE": "1",
|
||||||
"HF_HUB_DISABLE_TELEMETRY": "1", "OMP_NUM_THREADS": str(config.cpu_threads),
|
"HF_HUB_DISABLE_TELEMETRY": "1", "OMP_NUM_THREADS": str(config.cpu_threads),
|
||||||
"PYTHONIOENCODING": "utf-8"}
|
"PYTHONIOENCODING": "utf-8"}
|
||||||
args = (str(interpreter()), str(Path(__file__).with_name("worker.py")))
|
args = (str(executable), str(Path(__file__).with_name("worker.py")))
|
||||||
options = {"env": env, "limit": 16 * 1024 * 1024,
|
options = {"env": env, "limit": 16 * 1024 * 1024,
|
||||||
**({"creationflags": 0x08000000} if os.name == "nt" else {})}
|
**({"creationflags": 0x08000000} if os.name == "nt" else {})}
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ def test_local_model_missing_is_explicit():
|
|||||||
def test_cancel_reaps_active_model_process(monkeypatch):
|
def test_cancel_reaps_active_model_process(monkeypatch):
|
||||||
import app.local_models.runtime as module
|
import app.local_models.runtime as module
|
||||||
monkeypatch.setattr(module,'read_state',lambda key:{'status':'installed'})
|
monkeypatch.setattr(module,'read_state',lambda key:{'status':'installed'})
|
||||||
monkeypatch.setattr(module,'interpreter',lambda:Path(sys.executable))
|
monkeypatch.setattr(module,'interpreter',lambda *_:Path(sys.executable))
|
||||||
class Input:
|
class Input:
|
||||||
def write(self, value):
|
def write(self, value):
|
||||||
request = json.loads(value)
|
request = json.loads(value)
|
||||||
@@ -92,7 +92,7 @@ def test_subprocess_fallback_runs_and_reaps_real_worker(monkeypatch, tmp_path, c
|
|||||||
import app.local_models.process as process_module
|
import app.local_models.process as process_module
|
||||||
|
|
||||||
monkeypatch.setattr(module, 'read_state', lambda key: {'status': 'installed'})
|
monkeypatch.setattr(module, 'read_state', lambda key: {'status': 'installed'})
|
||||||
monkeypatch.setattr(module, 'interpreter', lambda: Path(sys.executable))
|
monkeypatch.setattr(module, 'interpreter', lambda *_: Path(sys.executable))
|
||||||
worker = tmp_path / 'worker.py'
|
worker = tmp_path / 'worker.py'
|
||||||
worker.write_text(
|
worker.write_text(
|
||||||
'import json,sys,time\n'
|
'import json,sys,time\n'
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def test_cuda_retries_only_device_failures_in_reaped_process(monkeypatch, code,
|
|||||||
from app.services.usage_service import connection
|
from app.services.usage_service import connection
|
||||||
monkeypatch.setattr(module, 'configuration', lambda: module.RuntimeConfig(device='cuda'))
|
monkeypatch.setattr(module, 'configuration', lambda: module.RuntimeConfig(device='cuda'))
|
||||||
monkeypatch.setattr(module, 'read_state', lambda key: {'status': 'installed'})
|
monkeypatch.setattr(module, 'read_state', lambda key: {'status': 'installed'})
|
||||||
monkeypatch.setattr(module, 'interpreter', lambda: Path(sys.executable))
|
monkeypatch.setattr(module, 'interpreter', lambda *_: Path(sys.executable))
|
||||||
events = []
|
events = []
|
||||||
|
|
||||||
class Process:
|
class Process:
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ def test_interpreter_keeps_cpu_default_and_respects_explicit_override(monkeypatc
|
|||||||
(components.ROOT / 'ready.json').write_text('{}')
|
(components.ROOT / 'ready.json').write_text('{}')
|
||||||
monkeypatch.setattr(runtime, 'configuration', lambda: runtime.RuntimeConfig(device='cpu'))
|
monkeypatch.setattr(runtime, 'configuration', lambda: runtime.RuntimeConfig(device='cpu'))
|
||||||
assert runtime.interpreter() != python
|
assert runtime.interpreter() != python
|
||||||
monkeypatch.setattr(runtime, 'configuration', lambda: runtime.RuntimeConfig(device='cuda'))
|
# A queued attempt keeps its frozen device even after the saved setting changes.
|
||||||
assert runtime.interpreter() == python
|
assert runtime.interpreter(runtime.RuntimeConfig(device='cuda')) == python
|
||||||
|
assert runtime.interpreter(runtime.RuntimeConfig(device='cpu')) != python
|
||||||
monkeypatch.setenv('APP_MODEL_PYTHON', 'explicit-python.exe')
|
monkeypatch.setenv('APP_MODEL_PYTHON', 'explicit-python.exe')
|
||||||
assert str(runtime.interpreter()) == 'explicit-python.exe'
|
assert str(runtime.interpreter()) == 'explicit-python.exe'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @vitest-environment happy-dom
|
// @vitest-environment happy-dom
|
||||||
import { mount } from '@vue/test-utils'
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
import { expect, it, vi } from 'vitest'
|
import { expect, it, vi } from 'vitest'
|
||||||
import RequestJsonEditor from './RequestJsonEditor.vue'
|
import RequestJsonEditor from './RequestJsonEditor.vue'
|
||||||
import { apiClient } from '@/services/apiClient'
|
import { apiClient } from '@/services/apiClient'
|
||||||
@@ -38,6 +38,21 @@ it('ignores an imported configuration that finishes after a newer edit', async (
|
|||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('ignores an old import failure after a newer edit', async () => {
|
||||||
|
let fail!: (reason: Error) => void
|
||||||
|
vi.mocked(apiClient.post).mockReturnValue(new Promise((_resolve, reject) => { fail = reject }))
|
||||||
|
const wrapper = mount(RequestJsonEditor, {props:{modelValue:[]}})
|
||||||
|
const input = wrapper.get('input[type="file"]')
|
||||||
|
Object.defineProperty(input.element, 'files', {value:[new File(['{}'], 'old.json')], configurable:true})
|
||||||
|
await input.trigger('change')
|
||||||
|
await wrapper.findAll('button').find(button => button.text() === '添加请求规则')!.trigger('click')
|
||||||
|
fail(new Error('旧导入失败'))
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.text()).not.toContain('旧导入失败')
|
||||||
|
expect(wrapper.findAll('textarea')).toHaveLength(1)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
it('restores defaults even from an invalid draft and reflects replacement configurations', async () => {
|
it('restores defaults even from an invalid draft and reflects replacement configurations', async () => {
|
||||||
const wrapper = mount(RequestJsonEditor, {props:{modelValue:[{capability:'chat', body:{enable_thinking:false}}]}})
|
const wrapper = mount(RequestJsonEditor, {props:{modelValue:[{capability:'chat', body:{enable_thinking:false}}]}})
|
||||||
await wrapper.get('textarea').setValue('{invalid')
|
await wrapper.get('textarea').setValue('{invalid')
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ async function importRules(event: Event) {
|
|||||||
if (current !== generation) return
|
if (current !== generation) return
|
||||||
rules.value = validated.request_overrides.map(rule => ({...rule, draft: JSON.stringify(rule.body, null, 2), error: ''}))
|
rules.value = validated.request_overrides.map(rule => ({...rule, draft: JSON.stringify(rule.body, null, 2), error: ''}))
|
||||||
publish()
|
publish()
|
||||||
} catch(e) { transferError.value = (e as Error).message }
|
} catch(e) { if (current === generation) transferError.value = (e as Error).message }
|
||||||
}
|
}
|
||||||
async function exportRules() {
|
async function exportRules() {
|
||||||
transferError.value = ''
|
transferError.value = ''
|
||||||
|
|||||||
Reference in New Issue
Block a user