The provider-agnostic half of PR #105863, so a CLI-driven subscription provider can ship as a standalone `kind: model-provider` plugin instead of a bundled one: - ProviderProfile: `native_reasoning_details_type`, `model_aliases`, `get_model_context_length`, `get_usage_cost`, `setup_status`, `discover_models` hooks (all default None / no-op). - Chat Completions transport: provider-native `reasoning_details` carriers follow only their declaring profile; standard records still replay on OpenRouter-style routes, strict routes drop the field wholesale (#70233). Relay/stream accumulate `delta.reasoning_details` verbatim. - `hermes model`: the generic plugin flow gates an external-process row on the CLI's own login status (inline `login_command` on a TTY), offers `discover_models()` rows with per-row notes, and never writes config when the executable is missing. - `/model` and the pickers: process providers list their live catalog merged with the pinned one, declared aliases/ids resolve inside the provider, and validation accepts a listed id without probing `process://`. - Delegation keeps the selected external-process provider and protocol for the child. - Model metadata / usage pricing consult the profile's bound and cost hooks first. - Desktop: `[1m]` renders as a "1M" tag and hyphenated Anthropic versions read "Haiku 4.5". The bespoke `_model_flow_external_process` and hard-coded `hermes_cli/main.py` paths from the PR were dropped in favour of main's `_model_flow_plugin_provider`. Co-authored-by: unsupportedpastels <unsupportedpastels@users.noreply.github.com>
63 lines
1.6 KiB
Python
63 lines
1.6 KiB
Python
"""Request-owned non-HTTP transports must participate in interruption."""
|
|
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
|
|
from agent.client_lifecycle import ClientLifecycleMixin
|
|
|
|
|
|
class Agent(ClientLifecycleMixin):
|
|
socket_sweeps = 0
|
|
|
|
def _client_log_context(self):
|
|
return "provider=test"
|
|
|
|
def _force_close_tcp_sockets(self, client):
|
|
self.socket_sweeps += 1
|
|
return 0
|
|
|
|
|
|
def test_cross_thread_cancel_stops_real_provider_process():
|
|
proc = subprocess.Popen(
|
|
[sys.executable, "-c", "import time; time.sleep(60)"],
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
|
|
class Client:
|
|
def cancel(self):
|
|
proc.terminate()
|
|
|
|
agent = Agent()
|
|
client = Client()
|
|
try:
|
|
thread = threading.Thread(
|
|
target=agent._abort_request_openai_client,
|
|
args=(client,),
|
|
kwargs={"reason": "interrupt_abort"},
|
|
)
|
|
thread.start()
|
|
thread.join(timeout=2)
|
|
assert not thread.is_alive()
|
|
assert proc.wait(timeout=2) is not None
|
|
assert agent.socket_sweeps == 0
|
|
finally:
|
|
if proc.poll() is None:
|
|
proc.kill()
|
|
proc.wait(timeout=5)
|
|
|
|
|
|
def test_dynamic_attributes_do_not_opt_http_clients_into_cancel():
|
|
calls = []
|
|
|
|
class Client:
|
|
def __getattr__(self, name):
|
|
return lambda: calls.append(name)
|
|
|
|
agent = Agent()
|
|
agent._abort_request_openai_client(Client(), reason="interrupt_abort")
|
|
assert agent.socket_sweeps == 1
|
|
assert calls == []
|