mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-17 04:34:25 +00:00
* feat(agents): support Databricks Agent Platform agents in the agent gateway Add a databricks_agent provider that speaks the MLflow ResponsesAgent wire (Responses-shaped input, output_text items, output_text.delta SSE) to Model Serving endpoints (/invocations or the unified /serving-endpoints/responses) and Databricks Apps (/responses), with a PAT or OAuth M2M client credentials Register it through the agents config block or the Admin UI agent catalog and call it through /a2a/<agent> (message/send, message/stream) or model: a2a/<agent> chat completions. The chat route now runs every completion-bridge agent through its own provider, and attaches the minted Databricks OAuth token for both bridge and URL agents. Consumed OAuth settings never reach the provider call * fix(agents): harden databricks_agent input, output, and OAuth handling Non-text content parts answer 400 unless drop_params is set, a reply without an output list answers 502, flat OAuth fields resolve an os.environ api_base, the bridge debug log no longer echoes the request model, routing tests stub only the HTTP boundary, and the catalog description fits the dropdown * fix(agents): merge agent and client extra_headers on the a2a chat route, and reject an empty databricks_oauth block * fix(agents): bridge only databricks_agent agents on the a2a chat route, keep the card url over a client api_base, and require workspace_url for a Databricks App * refactor(agents): read the a2a chat bridge providers from a constants set instead of branching on a provider name * fix(databricks_agent): let the configured credential win, reject malformed client extra_headers, and keep a missing endpoint name empty * test(agents): drive the flat OAuth a2a send through the real bridge * fix(agents): keep an empty databricks_oauth block ignored on url agents, as before this change * fix(agents): accept bare App hosts, env service principals, and os.environ refs on Databricks agents * fix(databricks_agent): address review caveats on chat bridging, params, and auth Map max_tokens and max_completion_tokens to max_output_tokens and pass temperature, top_p, and user, so /v1/messages works without drop_params. Send A2A message metadata only with string values, answering 400 or dropping the rest under drop_params. Join message items with a blank line in both stream and non-stream replies. Cache the env service principal token until shortly before it expires. Keep databricks_agent replies out of the response cache. On the chat bridge, let request fields win over the agent's stored params while pinning the agent's api_base and api_key, and apply the agent's static_headers between client and agent extra_headers, as /a2a does. Resolve os.environ/ only for databricks_agent api_base and api_key instead of every bridge provider. Ignore an unusable databricks_oauth block on URL agents on chat, as at the merge base, instead of answering 400. * test(internal_context): list the databricks agent token cache as process-local * fix(databricks_agent): keep the agent's endpoint, input, and target out of client control extra_body model and input answer 400, or are dropped under drop_params. A client api_base or base_url never replaces the agent's card URL on the bridge. Chat to a URL agent keeps its merge-base call, and an incomplete databricks_oauth block on a URL agent keeps its merge-base ValueError. * test(databricks): expect the merge-base 400 for a client api_base on agent chat * fix(databricks): keep agent-owned inputs and identity headers out of a chat caller's reach * fix(databricks): bound the agent-over-caller merge to two levels so it needs no recursion --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
984 lines
44 KiB
Python
984 lines
44 KiB
Python
import asyncio
|
|
import base64
|
|
import json
|
|
import re
|
|
import threading
|
|
import uuid
|
|
from collections.abc import Callable, Iterator, Sequence
|
|
from contextlib import contextmanager
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Final
|
|
from urllib.parse import parse_qs
|
|
|
|
import httpx
|
|
import openai
|
|
import pytest
|
|
import yaml
|
|
from integration._support.client import Gateway, eventually
|
|
from integration._support.database import read_rows
|
|
from integration._support.process import graceful_stop_seconds, owned_proxy_process
|
|
from integration._support.wire import Reply, Request, Wire, wire_server
|
|
from pydantic import JsonValue, TypeAdapter
|
|
|
|
_ENDPOINT: Final = "support-agent"
|
|
_INVOCATIONS: Final = f"/serving-endpoints/{_ENDPOINT}/invocations"
|
|
_UNIFIED: Final = "/serving-endpoints/responses"
|
|
_APP: Final = "/responses"
|
|
_TOKEN: Final = "/oidc/v1/token"
|
|
_PAT: Final = "synthetic-databricks-pat"
|
|
_CLIENT_ID: Final = "synthetic-databricks-sp"
|
|
_CLIENT_SECRET: Final = "synthetic-databricks-sp-secret"
|
|
_MINTED: Final = "synthetic-databricks-minted-token"
|
|
_REPLY: Final = "the export job is waiting on a lock"
|
|
_PROMPT: Final = "Why does the export job hang?"
|
|
_USAGE: Final[dict[str, JsonValue]] = {"input_tokens": 11, "output_tokens": 3, "total_tokens": 14}
|
|
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
|
|
_STARTED_WORKER: Final = re.compile(r"Started server process \[(\d+)\]")
|
|
_OWNED_PROXY_BUDGET: Final = int(2 * graceful_stop_seconds() + 120)
|
|
|
|
|
|
def _basic(client_id: str, client_secret: str) -> str:
|
|
return "Basic " + base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
|
|
|
|
|
|
def _agent_reply(text: str = _REPLY) -> bytes:
|
|
return json.dumps(
|
|
{
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"id": "msg-1",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": text}],
|
|
}
|
|
],
|
|
"usage": _USAGE,
|
|
}
|
|
).encode()
|
|
|
|
|
|
def _agent_stream(text: str = _REPLY) -> bytes:
|
|
head, tail = text[: len(text) // 2], text[len(text) // 2 :]
|
|
events: Final = (
|
|
{"type": "response.output_text.delta", "item_id": "msg-1", "delta": head},
|
|
{"type": "response.output_text.delta", "item_id": "msg-1", "delta": tail},
|
|
{
|
|
"type": "response.output_item.done",
|
|
"item": {"type": "message", "id": "msg-1", "content": [{"type": "output_text", "text": text}]},
|
|
},
|
|
)
|
|
return "".join(f"data: {json.dumps(event)}\n\n" for event in events).encode()
|
|
|
|
|
|
def _token_reply() -> Reply:
|
|
return Reply(body=json.dumps({"access_token": _MINTED, "token_type": "Bearer", "expires_in": 3600}).encode())
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class _Databricks:
|
|
token_status: Callable[[], int] = lambda: 200
|
|
endpoint_reply: Callable[[Request], Reply] | None = None
|
|
|
|
def __call__(self, request: Request) -> Reply:
|
|
if request.target == _TOKEN:
|
|
status: Final = self.token_status()
|
|
if status != 200:
|
|
return Reply(status=status, body=b'{"error":"invalid_client"}')
|
|
return _token_reply()
|
|
if self.endpoint_reply is not None:
|
|
return self.endpoint_reply(request)
|
|
if _JSON_OBJECT.validate_json(request.body).get("stream") is True:
|
|
return Reply(body=_agent_stream(), content_type="text/event-stream")
|
|
return Reply(body=_agent_reply())
|
|
|
|
|
|
def _calls_to(requests: Sequence[Request], target: str) -> tuple[Request, ...]:
|
|
return tuple(request for request in requests if request.target == target)
|
|
|
|
|
|
def _body(request: Request) -> dict[str, JsonValue]:
|
|
return _JSON_OBJECT.validate_json(request.body)
|
|
|
|
|
|
def _sdk_base_url(gateway: Gateway) -> str:
|
|
return f"{str(gateway.client.base_url).rstrip('/')}/v1"
|
|
|
|
|
|
def _register(
|
|
gateway: Gateway,
|
|
cleanups: Callable[[Callable[[], None]], object],
|
|
card_url: str,
|
|
static_headers: dict[str, JsonValue] | None = None,
|
|
**params: JsonValue,
|
|
) -> str:
|
|
name: Final = f"dbx-agent-{uuid.uuid4().hex[:12]}"
|
|
created: Final = gateway.request(
|
|
"POST",
|
|
"/v1/agents",
|
|
{
|
|
"agent_name": name,
|
|
"agent_card_params": {
|
|
"protocolVersion": "0.3",
|
|
"name": name,
|
|
"description": "Databricks Agent agent",
|
|
"url": card_url,
|
|
"version": "1.0.0",
|
|
"defaultInputModes": ["text"],
|
|
"defaultOutputModes": ["text"],
|
|
"capabilities": {"streaming": True},
|
|
"skills": [],
|
|
},
|
|
"litellm_params": {"custom_llm_provider": "databricks_agent", **params},
|
|
**({"static_headers": static_headers} if static_headers else {}),
|
|
},
|
|
)
|
|
assert created.status_code == 200, created.text
|
|
agent_id: Final = created.json()["agent_id"]
|
|
|
|
def delete() -> None:
|
|
deleted: Final = gateway.request("DELETE", f"/v1/agents/{agent_id}")
|
|
assert deleted.status_code == 200, deleted.text
|
|
|
|
cleanups(delete)
|
|
return name
|
|
|
|
|
|
@contextmanager
|
|
def _agent(
|
|
gateway: Gateway,
|
|
databricks: _Databricks,
|
|
*,
|
|
url_path: str = "",
|
|
static_headers: dict[str, JsonValue] | None = None,
|
|
**params: JsonValue,
|
|
) -> Iterator[tuple[Wire, str]]:
|
|
with wire_server(databricks) as wire, gateway.scenario() as scenario:
|
|
params_with_base: Final = {"api_base": wire.url + url_path, **params}
|
|
yield (
|
|
wire,
|
|
_register(gateway, scenario.cleanups.callback, wire.url + url_path, static_headers, **params_with_base),
|
|
)
|
|
|
|
|
|
def _chat(gateway: Gateway, name: str, **extra: JsonValue) -> httpx.Response:
|
|
return gateway.request(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
{"model": f"a2a/{name}", "messages": [{"role": "user", "content": _PROMPT}], **extra},
|
|
)
|
|
|
|
|
|
def _a2a(
|
|
gateway: Gateway, name: str, method: str, marker: str, metadata: dict[str, JsonValue] | None = None
|
|
) -> httpx.Response:
|
|
message: Final[dict[str, JsonValue]] = {
|
|
"role": "user",
|
|
"messageId": marker,
|
|
"parts": [{"kind": "text", "text": _PROMPT}],
|
|
**({"metadata": metadata} if metadata is not None else {}),
|
|
}
|
|
return gateway.request(
|
|
"POST",
|
|
f"/a2a/{name}",
|
|
{"jsonrpc": "2.0", "id": marker, "method": method, "params": {"message": message}},
|
|
)
|
|
|
|
|
|
def _sse(text: str) -> tuple[dict[str, JsonValue], ...]:
|
|
return tuple(
|
|
_JSON_OBJECT.validate_json(line.removeprefix("data: "))
|
|
for line in text.splitlines()
|
|
if line.startswith("data: ") and line != "data: [DONE]"
|
|
)
|
|
|
|
|
|
def _spend_status(request_id: str) -> str:
|
|
rows: Final = eventually(
|
|
lambda: read_rows('SELECT status FROM "LiteLLM_SpendLogs" WHERE request_id = %s', (request_id,)),
|
|
lambda found: len(found) == 1,
|
|
seconds=70,
|
|
)
|
|
return str(rows[0]["status"])
|
|
|
|
|
|
def _assert_invocation(request: Request, bearer: str, *, stream: bool = False) -> None:
|
|
assert request.method == "POST" and request.target == _INVOCATIONS
|
|
assert request.headers["authorization"] == f"Bearer {bearer}"
|
|
assert _body(request) == {
|
|
"input": [{"role": "user", "content": _PROMPT}],
|
|
**({"stream": True} if stream else {}),
|
|
}
|
|
|
|
|
|
def _assert_token_request(request: Request) -> None:
|
|
assert request.method == "POST"
|
|
assert request.headers["authorization"] == _basic(_CLIENT_ID, _CLIENT_SECRET)
|
|
assert parse_qs(request.body.decode()) == {"grant_type": ["client_credentials"], "scope": ["all-apis"]}
|
|
|
|
|
|
def test_chat_on_a_pat_agent_reaches_the_serving_endpoint_and_bills_the_agent_usage(gateway: Gateway) -> None:
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
response: Final = _chat(gateway, name)
|
|
assert response.status_code == 200, response.text
|
|
payload: Final = response.json()
|
|
assert payload["choices"][0]["message"] == {"content": _REPLY, "role": "assistant"}
|
|
assert payload["choices"][0]["finish_reason"] == "stop"
|
|
assert payload["usage"]["prompt_tokens"] == 11 and payload["usage"]["completion_tokens"] == 3
|
|
received: Final = wire.drain()
|
|
assert len(received) == 1, received
|
|
_assert_invocation(received[0], _PAT)
|
|
assert _spend_status(payload["id"]) == "success"
|
|
|
|
|
|
def test_async_chat_stream_on_a_pat_agent_sends_the_reply_once(gateway: Gateway) -> None:
|
|
async def stream(name: str) -> str:
|
|
client: Final = openai.AsyncOpenAI(base_url=_sdk_base_url(gateway), api_key=gateway.key, max_retries=0)
|
|
chunks: Final = await client.chat.completions.create(
|
|
model=f"a2a/{name}", messages=[{"role": "user", "content": _PROMPT}], stream=True
|
|
)
|
|
return "".join([chunk.choices[0].delta.content or "" async for chunk in chunks if chunk.choices])
|
|
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
assert asyncio.run(stream(name)) == _REPLY
|
|
received: Final = wire.drain()
|
|
assert len(received) == 1, received
|
|
_assert_invocation(received[0], _PAT, stream=True)
|
|
|
|
|
|
def test_sync_sdk_chat_on_a_pat_agent_returns_the_agent_reply(gateway: Gateway) -> None:
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
client: Final = openai.OpenAI(base_url=_sdk_base_url(gateway), api_key=gateway.key, max_retries=0)
|
|
completion: Final = client.chat.completions.create(
|
|
model=f"a2a/{name}", messages=[{"role": "user", "content": _PROMPT}]
|
|
)
|
|
assert completion.choices[0].message.content == _REPLY
|
|
assert len(_calls_to(wire.drain(), _INVOCATIONS)) == 1
|
|
|
|
|
|
def test_responses_on_a_pat_agent_returns_the_reply_in_output(gateway: Gateway) -> None:
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
response: Final = gateway.request("POST", "/v1/responses", {"model": f"a2a/{name}", "input": _PROMPT})
|
|
assert response.status_code == 200, response.text
|
|
texts: Final = tuple(
|
|
part["text"]
|
|
for item in response.json()["output"]
|
|
if item["type"] == "message"
|
|
for part in item["content"]
|
|
if part["type"] == "output_text"
|
|
)
|
|
assert texts == (_REPLY,)
|
|
received: Final = wire.drain()
|
|
assert len(received) == 1, received
|
|
_assert_invocation(received[0], _PAT)
|
|
|
|
|
|
@pytest.mark.parametrize("drop_params", (False, True), ids=("strict", "drop-params"))
|
|
def test_messages_on_a_pat_agent_sends_max_tokens_as_max_output_tokens(gateway: Gateway, drop_params: bool) -> None:
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/v1/messages",
|
|
{
|
|
"model": f"a2a/{name}",
|
|
"max_tokens": 64,
|
|
"messages": [{"role": "user", "content": _PROMPT}],
|
|
**({"drop_params": True} if drop_params else {}),
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json()["content"] == [{"type": "text", "text": _REPLY}]
|
|
invocations: Final = _calls_to(wire.drain(), _INVOCATIONS)
|
|
assert len(invocations) == 1, invocations
|
|
assert _body(invocations[0]) == {"input": [{"role": "user", "content": _PROMPT}], "max_output_tokens": 64}
|
|
|
|
|
|
def test_chat_sampling_params_reach_the_agent_in_its_own_shape(gateway: Gateway) -> None:
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
response: Final = _chat(gateway, name, max_completion_tokens=32, temperature=0.2, top_p=0.9, user="u-1")
|
|
assert response.status_code == 200, response.text
|
|
invocations: Final = _calls_to(wire.drain(), _INVOCATIONS)
|
|
assert len(invocations) == 1, invocations
|
|
assert _body(invocations[0]) == {
|
|
"input": [{"role": "user", "content": _PROMPT}],
|
|
"max_output_tokens": 32,
|
|
"temperature": 0.2,
|
|
"top_p": 0.9,
|
|
"user": "u-1",
|
|
}
|
|
|
|
|
|
def test_a_reply_with_several_message_items_reads_the_same_streamed_or_not(gateway: Gateway) -> None:
|
|
first, second = "Let me look at the export queue.", "The export job is waiting on a lock."
|
|
|
|
def message(item_id: str, text: str) -> dict[str, JsonValue]:
|
|
return {"type": "message", "id": item_id, "content": [{"type": "output_text", "text": text}]}
|
|
|
|
def reply(request: Request) -> Reply:
|
|
if _body(request).get("stream") is not True:
|
|
output: Final = (message("msg-1", first), {"type": "function_call", "id": "fc-1"}, message("msg-2", second))
|
|
return Reply(body=json.dumps({"output": output, "usage": _USAGE}).encode())
|
|
events: Final = (
|
|
{"type": "response.output_text.delta", "item_id": "msg-1", "delta": first},
|
|
{"type": "response.output_item.done", "item": {"type": "function_call", "id": "fc-1"}},
|
|
{"type": "response.output_text.delta", "item_id": "msg-2", "delta": second},
|
|
{"type": "response.output_item.done", "item": message("msg-2", second)},
|
|
)
|
|
return Reply(
|
|
body="".join(f"data: {json.dumps(event)}\n\n" for event in events).encode(),
|
|
content_type="text/event-stream",
|
|
)
|
|
|
|
with _agent(gateway, _Databricks(endpoint_reply=reply), model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
client: Final = openai.OpenAI(base_url=_sdk_base_url(gateway), api_key=gateway.key, max_retries=0)
|
|
messages: Final = [{"role": "user", "content": _PROMPT}]
|
|
whole: Final = client.chat.completions.create(model=f"a2a/{name}", messages=messages)
|
|
streamed: Final = client.chat.completions.create(model=f"a2a/{name}", messages=messages, stream=True)
|
|
expected: Final = f"{first}\n\n{second}"
|
|
assert whole.choices[0].message.content == expected
|
|
assert "".join(chunk.choices[0].delta.content or "" for chunk in streamed if chunk.choices) == expected
|
|
assert len(_calls_to(wire.drain(), _INVOCATIONS)) == 2
|
|
|
|
|
|
def test_identical_chat_requests_on_an_agent_each_reach_the_agent(gateway: Gateway) -> None:
|
|
replies: Final = iter(("first answer", "second answer"))
|
|
databricks: Final = _Databricks(endpoint_reply=lambda _: Reply(body=_agent_reply(next(replies))))
|
|
with _agent(gateway, databricks, model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
answers: Final = tuple(_chat(gateway, name) for _ in range(2))
|
|
assert [answer.json()["choices"][0]["message"]["content"] for answer in answers] == [
|
|
"first answer",
|
|
"second answer",
|
|
], [answer.text for answer in answers]
|
|
assert len(_calls_to(wire.drain(), _INVOCATIONS)) == 2
|
|
|
|
|
|
def test_static_headers_sit_between_client_and_agent_extra_headers_on_chat(gateway: Gateway) -> None:
|
|
with _agent(
|
|
gateway,
|
|
_Databricks(),
|
|
static_headers={"X-Static": "static", "X-Shared": "from-static", "X-Both": "from-static"},
|
|
model=_ENDPOINT,
|
|
api_key=_PAT,
|
|
extra_headers={"x-both": "from-agent"},
|
|
) as (wire, name):
|
|
response: Final = _chat(gateway, name, extra_headers={"x-shared": "from-client", "X-Trace": "trace-1"})
|
|
assert response.status_code == 200, response.text
|
|
invocations: Final = _calls_to(wire.drain(), _INVOCATIONS)
|
|
assert len(invocations) == 1, invocations
|
|
headers: Final = invocations[0].headers
|
|
assert (headers["x-static"], headers["x-shared"], headers["x-both"]) == ("static", "from-static", "from-agent")
|
|
assert headers["x-trace"] == "trace-1"
|
|
_assert_invocation(invocations[0], _PAT)
|
|
|
|
|
|
def test_chat_never_forwards_client_litellm_identity_headers_to_the_agent(gateway: Gateway) -> None:
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
response: Final = _chat(
|
|
gateway,
|
|
name,
|
|
extra_headers={"X-LiteLLM-User-Id": "spoofed-user", "x-litellm-team-id": "spoofed-team", "X-Trace": "t-1"},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
invocations: Final = _calls_to(wire.drain(), _INVOCATIONS)
|
|
assert len(invocations) == 1, invocations
|
|
headers: Final = invocations[0].headers
|
|
assert "x-litellm-user-id" not in headers and "x-litellm-team-id" not in headers, headers
|
|
assert headers["x-trace"] == "t-1"
|
|
_assert_invocation(invocations[0], _PAT)
|
|
|
|
|
|
def test_the_agent_custom_inputs_and_extra_body_keys_win_over_the_callers_on_chat(gateway: Gateway) -> None:
|
|
with _agent(
|
|
gateway,
|
|
_Databricks(),
|
|
model=_ENDPOINT,
|
|
api_key=_PAT,
|
|
custom_inputs={"tenant": "owner"},
|
|
extra_body={"metadata": {"tenant": "owner"}},
|
|
) as (wire, name):
|
|
response: Final = _chat(
|
|
gateway,
|
|
name,
|
|
custom_inputs={"tenant": "caller", "session": "s-1"},
|
|
extra_body={"metadata": {"tenant": "caller", "turn": "2"}},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
invocations: Final = _calls_to(wire.drain(), _INVOCATIONS)
|
|
assert len(invocations) == 1, invocations
|
|
assert _body(invocations[0]) == {
|
|
"input": [{"role": "user", "content": _PROMPT}],
|
|
"custom_inputs": {"tenant": "owner", "session": "s-1"},
|
|
"metadata": {"tenant": "owner", "turn": "2"},
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("drop_params", (False, True), ids=("strict", "drop-params"))
|
|
def test_a2a_message_metadata_reaches_the_agent_only_as_string_values(gateway: Gateway, drop_params: bool) -> None:
|
|
params: Final[dict[str, JsonValue]] = {"drop_params": True} if drop_params else {}
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT, **params) as (wire, name):
|
|
strings: Final = _a2a(gateway, name, "message/send", f"meta-{uuid.uuid4().hex}", {"session": "s-1"})
|
|
assert strings.status_code == 200 and "error" not in strings.json(), strings.text
|
|
assert _body(_calls_to(wire.drain(), _INVOCATIONS)[0])["metadata"] == {"session": "s-1"}
|
|
mixed: Final = _a2a(gateway, name, "message/send", f"meta-{uuid.uuid4().hex}", {"session": "s-1", "turn": 1})
|
|
sent: Final = _calls_to(wire.drain(), _INVOCATIONS)
|
|
if drop_params:
|
|
assert mixed.status_code == 200 and "error" not in mixed.json(), mixed.text
|
|
assert [_body(request)["metadata"] for request in sent] == [{"session": "s-1"}]
|
|
else:
|
|
assert "string values" in mixed.text, mixed.text
|
|
assert sent == ()
|
|
|
|
|
|
def test_a2a_message_send_on_a_pat_agent_answers_with_the_agent_text(gateway: Gateway) -> None:
|
|
marker: Final = f"send-{uuid.uuid4().hex}"
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
response: Final = _a2a(gateway, name, "message/send", marker)
|
|
assert response.status_code == 200, response.text
|
|
body: Final = response.json()
|
|
assert body["id"] == marker and "error" not in body, body
|
|
assert body["result"]["parts"] == [{"kind": "text", "text": _REPLY}]
|
|
received: Final = wire.drain()
|
|
assert len(received) == 1, received
|
|
_assert_invocation(received[0], _PAT)
|
|
|
|
|
|
def test_a2a_message_stream_on_a_pat_agent_streams_the_text_and_completes(gateway: Gateway) -> None:
|
|
marker: Final = f"stream-{uuid.uuid4().hex}"
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
response: Final = _a2a(gateway, name, "message/stream", marker)
|
|
assert response.status_code == 200, response.text
|
|
events: Final = tuple(event["result"] for event in _sse(response.text))
|
|
artifacts: Final = tuple(
|
|
part["text"]
|
|
for event in events
|
|
if event["kind"] == "artifact-update"
|
|
for part in event["artifact"]["parts"]
|
|
)
|
|
assert artifacts == (_REPLY,)
|
|
assert events[-1]["kind"] == "status-update" and events[-1]["final"] is True
|
|
assert events[-1]["status"]["state"] == "completed"
|
|
received: Final = wire.drain()
|
|
assert len(received) == 1, received
|
|
_assert_invocation(received[0], _PAT, stream=True)
|
|
|
|
|
|
def test_the_unified_responses_url_names_the_endpoint_in_the_body(gateway: Gateway) -> None:
|
|
with _agent(gateway, _Databricks(), url_path=_UNIFIED, model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
response: Final = _chat(gateway, name)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json()["choices"][0]["message"]["content"] == _REPLY
|
|
received: Final = wire.drain()
|
|
assert [(request.method, request.target) for request in received] == [("POST", _UNIFIED)]
|
|
assert received[0].headers["authorization"] == f"Bearer {_PAT}"
|
|
assert _body(received[0]) == {"model": _ENDPOINT, "input": [{"role": "user", "content": _PROMPT}]}
|
|
|
|
|
|
def test_an_app_agent_mints_its_token_at_the_workspace_and_calls_the_app(gateway: Gateway) -> None:
|
|
with (
|
|
wire_server(_Databricks()) as workspace,
|
|
_agent(
|
|
gateway,
|
|
_Databricks(),
|
|
url_path=_APP,
|
|
client_id=_CLIENT_ID,
|
|
client_secret=_CLIENT_SECRET,
|
|
workspace_url=workspace.url,
|
|
) as (app, name),
|
|
):
|
|
response: Final = _chat(gateway, name)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json()["choices"][0]["message"]["content"] == _REPLY
|
|
minted: Final = workspace.drain()
|
|
assert [request.target for request in minted] == [_TOKEN]
|
|
_assert_token_request(minted[0])
|
|
called: Final = app.drain()
|
|
assert [(request.method, request.target) for request in called] == [("POST", _APP)]
|
|
assert called[0].headers["authorization"] == f"Bearer {_MINTED}"
|
|
assert _body(called[0]) == {"input": [{"role": "user", "content": _PROMPT}]}
|
|
|
|
|
|
def test_a_nested_oauth_block_mints_once_for_chat_and_a2a_calls_on_one_agent(gateway: Gateway) -> None:
|
|
def oauth(url: str) -> dict[str, JsonValue]:
|
|
return {"client_id": _CLIENT_ID, "client_secret": _CLIENT_SECRET, "workspace_url": url}
|
|
|
|
with wire_server(_Databricks()) as wire, gateway.scenario() as scenario:
|
|
name: Final = _register(
|
|
gateway,
|
|
scenario.cleanups.callback,
|
|
wire.url,
|
|
model=_ENDPOINT,
|
|
api_base=wire.url,
|
|
databricks_oauth=oauth(wire.url),
|
|
)
|
|
for _ in range(3):
|
|
response = _chat(gateway, name)
|
|
assert response.status_code == 200, response.text
|
|
sent: Final = _a2a(gateway, name, "message/send", f"send-{uuid.uuid4().hex}")
|
|
assert sent.status_code == 200 and "error" not in sent.json(), sent.text
|
|
received: Final = wire.drain()
|
|
tokens: Final = _calls_to(received, _TOKEN)
|
|
assert len(tokens) <= 2, [request.target for request in received]
|
|
_assert_token_request(tokens[0])
|
|
invocations: Final = _calls_to(received, _INVOCATIONS)
|
|
assert len(invocations) == 4
|
|
for invocation in invocations:
|
|
_assert_invocation(invocation, _MINTED)
|
|
|
|
|
|
def test_the_minted_token_outranks_the_agent_pat_and_a_client_authorization(gateway: Gateway) -> None:
|
|
with _agent(
|
|
gateway,
|
|
_Databricks(),
|
|
model=_ENDPOINT,
|
|
api_key=_PAT,
|
|
client_id=_CLIENT_ID,
|
|
client_secret=_CLIENT_SECRET,
|
|
) as (wire, name):
|
|
response: Final = _chat(gateway, name, extra_headers={"Authorization": "Bearer client-supplied"})
|
|
assert response.status_code == 200, response.text
|
|
received: Final = wire.drain()
|
|
assert [request.target for request in received] == [_TOKEN, _INVOCATIONS]
|
|
_assert_invocation(received[1], _MINTED)
|
|
|
|
|
|
def test_the_agent_pat_outranks_a_client_authorization(gateway: Gateway) -> None:
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
response: Final = _chat(gateway, name, extra_headers={"Authorization": "Bearer client-supplied"})
|
|
assert response.status_code == 200, response.text
|
|
received: Final = wire.drain()
|
|
assert len(received) == 1, received
|
|
_assert_invocation(received[0], _PAT)
|
|
|
|
|
|
def test_empty_oauth_fields_beside_a_pat_keep_the_pat_path(gateway: Gateway) -> None:
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT, client_id="", client_secret="") as (wire, name):
|
|
response: Final = _chat(gateway, name)
|
|
assert response.status_code == 200, response.text
|
|
received: Final = wire.drain()
|
|
assert [request.target for request in received] == [_INVOCATIONS]
|
|
_assert_invocation(received[0], _PAT)
|
|
|
|
|
|
def test_custom_inputs_reach_the_agent_beside_the_input(gateway: Gateway) -> None:
|
|
custom_inputs: Final[dict[str, JsonValue]] = {"session_id": "s-1", "tier": "gold"}
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
response: Final = _chat(gateway, name, custom_inputs=custom_inputs)
|
|
assert response.status_code == 200, response.text
|
|
received: Final = wire.drain()
|
|
assert len(received) == 1, received
|
|
assert _body(received[0]) == {"input": [{"role": "user", "content": _PROMPT}], "custom_inputs": custom_inputs}
|
|
|
|
|
|
def test_two_agents_on_one_endpoint_name_never_answer_from_each_other(gateway: Gateway) -> None:
|
|
def answering(text: str) -> _Databricks:
|
|
return _Databricks(endpoint_reply=lambda _: Reply(body=_agent_reply(text)))
|
|
|
|
with (
|
|
_agent(gateway, answering("from workspace one"), model=_ENDPOINT, api_key=_PAT) as (first_wire, first),
|
|
_agent(gateway, answering("from workspace two"), model=_ENDPOINT, api_key=_PAT) as (second_wire, second),
|
|
):
|
|
responses: Final = tuple(_chat(gateway, name) for name in (first, second, first))
|
|
assert all(response.status_code == 200 for response in responses), [r.text for r in responses]
|
|
answers: Final = tuple(response.json()["choices"][0]["message"]["content"] for response in responses)
|
|
assert answers == ("from workspace one", "from workspace two", "from workspace one")
|
|
assert len(_calls_to(first_wire.drain(), _INVOCATIONS)) == 2
|
|
assert len(_calls_to(second_wire.drain(), _INVOCATIONS)) == 1
|
|
|
|
|
|
def test_a_workspace_agent_with_no_endpoint_is_refused_before_any_call(gateway: Gateway) -> None:
|
|
with _agent(gateway, _Databricks(), api_key=_PAT) as (wire, name):
|
|
response: Final = _chat(gateway, name)
|
|
assert response.status_code == 400, response.text
|
|
assert "model must name the Databricks serving endpoint" in response.text
|
|
assert wire.drain() == ()
|
|
|
|
|
|
def test_an_app_agent_without_a_workspace_url_is_refused_before_its_secret_leaves(gateway: Gateway) -> None:
|
|
with _agent(gateway, _Databricks(), url_path=_APP, client_id=_CLIENT_ID, client_secret=_CLIENT_SECRET) as (
|
|
wire,
|
|
name,
|
|
):
|
|
response: Final = _chat(gateway, name)
|
|
assert response.status_code == 400, response.text
|
|
assert "workspace_url" in response.text
|
|
assert wire.drain() == ()
|
|
|
|
|
|
@pytest.mark.parametrize(("drop_params", "status"), ((False, 400), (True, 200)), ids=("strict", "drop-params"))
|
|
def test_malformed_client_extra_headers_follow_drop_params(gateway: Gateway, drop_params: bool, status: int) -> None:
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
response: Final = _chat(
|
|
gateway, name, extra_headers=["Authorization", "x"], **({"drop_params": True} if drop_params else {})
|
|
)
|
|
assert response.status_code == status, response.text
|
|
if drop_params:
|
|
received: Final = wire.drain()
|
|
assert len(received) == 1, received
|
|
_assert_invocation(received[0], _PAT)
|
|
else:
|
|
assert "extra_headers must be an object" in response.text
|
|
assert wire.drain() == ()
|
|
|
|
|
|
@pytest.mark.parametrize(("drop_params", "status"), ((False, 400), (True, 200)), ids=("strict", "drop-params"))
|
|
def test_an_image_part_follows_drop_params(gateway: Gateway, drop_params: bool, status: int) -> None:
|
|
content: Final[list[JsonValue]] = [
|
|
{"type": "text", "text": _PROMPT},
|
|
{"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0KGgo="}},
|
|
]
|
|
with _agent(gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
{
|
|
"model": f"a2a/{name}",
|
|
"messages": [{"role": "user", "content": content}],
|
|
**({"drop_params": True} if drop_params else {}),
|
|
},
|
|
)
|
|
assert response.status_code == status, response.text
|
|
if drop_params:
|
|
received: Final = wire.drain()
|
|
assert len(received) == 1, received
|
|
_assert_invocation(received[0], _PAT)
|
|
else:
|
|
assert "image_url" in response.text
|
|
assert wire.drain() == ()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("reply", "status"),
|
|
(
|
|
(Reply(status=401, body=b'{"error_code":"UNAUTHENTICATED","message":"Invalid token"}'), 401),
|
|
(Reply(status=500, body=b'{"error_code":"INTERNAL_ERROR","message":"agent crashed"}'), 500),
|
|
(Reply(body=b"{}"), 502),
|
|
),
|
|
ids=("upstream-401", "upstream-500", "not-a-responses-agent"),
|
|
)
|
|
def test_a_failing_agent_keeps_its_status_and_unrelated_models_keep_serving(
|
|
gateway: Gateway, reply: Reply, status: int
|
|
) -> None:
|
|
with (
|
|
_agent(gateway, _Databricks(endpoint_reply=lambda _: reply), model=_ENDPOINT, api_key=_PAT) as (wire, name),
|
|
gateway.scenario() as scenario,
|
|
):
|
|
response: Final = _chat(gateway, name)
|
|
assert response.status_code == status, response.text
|
|
assert len(_calls_to(wire.drain(), _INVOCATIONS)) >= 1
|
|
unrelated: Final = gateway.chat(scenario.model())
|
|
assert unrelated["choices"], unrelated
|
|
|
|
|
|
def test_a_refused_token_request_answers_an_error_without_calling_the_agent(gateway: Gateway) -> None:
|
|
with _agent(
|
|
gateway,
|
|
_Databricks(token_status=lambda: 400),
|
|
model=_ENDPOINT,
|
|
client_id=_CLIENT_ID,
|
|
client_secret=_CLIENT_SECRET,
|
|
) as (wire, name):
|
|
response: Final = _chat(gateway, name)
|
|
assert response.status_code >= 400, response.text
|
|
received: Final = wire.drain()
|
|
assert [request.target for request in received] == [_TOKEN]
|
|
|
|
|
|
def test_reading_an_agent_back_redacts_its_pat_and_client_secret(gateway: Gateway) -> None:
|
|
with _agent(
|
|
gateway, _Databricks(), model=_ENDPOINT, api_key=_PAT, client_id=_CLIENT_ID, client_secret=_CLIENT_SECRET
|
|
) as (_, name):
|
|
listed: Final = gateway.request("GET", "/v1/agents")
|
|
assert listed.status_code == 200, listed.text
|
|
text: Final = listed.text
|
|
assert name in text
|
|
assert _PAT not in text and _CLIENT_SECRET not in text
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"oauth_fields",
|
|
(None, (), ("client_id",), ("client_id", "client_secret", "workspace_url")),
|
|
ids=(
|
|
"plain",
|
|
"with-empty-databricks-oauth",
|
|
"with-partial-databricks-oauth",
|
|
"with-complete-databricks-oauth",
|
|
),
|
|
)
|
|
def test_a_url_agent_keeps_its_a2a_call_to_the_card_url(gateway: Gateway, oauth_fields: tuple[str, ...] | None) -> None:
|
|
marker: Final = f"url-agent-{uuid.uuid4().hex}"
|
|
|
|
def peer(request: Request) -> Reply:
|
|
if request.method == "GET":
|
|
return Reply(status=404)
|
|
outbound: Final = _body(request)
|
|
return Reply(
|
|
body=json.dumps(
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": outbound["id"],
|
|
"result": {
|
|
"kind": "message",
|
|
"role": "agent",
|
|
"messageId": marker,
|
|
"parts": [{"kind": "text", "text": _REPLY}],
|
|
},
|
|
}
|
|
).encode()
|
|
)
|
|
|
|
with wire_server(peer) as wire, gateway.scenario() as scenario:
|
|
complete_block: Final = {"client_id": _CLIENT_ID, "client_secret": _CLIENT_SECRET, "workspace_url": wire.url}
|
|
oauth_block: Final = None if oauth_fields is None else {field: complete_block[field] for field in oauth_fields}
|
|
created: Final = gateway.request(
|
|
"POST",
|
|
"/v1/agents",
|
|
{
|
|
"agent_name": marker,
|
|
"agent_card_params": {
|
|
"protocolVersion": "0.3",
|
|
"name": marker,
|
|
"description": "URL agent",
|
|
"url": wire.url + "/",
|
|
"version": "1.0.0",
|
|
"defaultInputModes": ["text"],
|
|
"defaultOutputModes": ["text"],
|
|
"capabilities": {"streaming": False},
|
|
"skills": [],
|
|
},
|
|
**({"litellm_params": {"databricks_oauth": oauth_block}} if oauth_block is not None else {}),
|
|
},
|
|
)
|
|
assert created.status_code == 200, created.text
|
|
agent_id: Final = created.json()["agent_id"]
|
|
scenario.cleanups.callback(lambda: gateway.request("DELETE", f"/v1/agents/{agent_id}"))
|
|
response: Final = _chat(gateway, marker, extra_headers={"Authorization": "Bearer client-token"})
|
|
assert response.status_code == 200, response.text
|
|
assert response.json()["choices"][0]["message"]["content"] == _REPLY
|
|
posts: Final = tuple(request for request in wire.drain() if request.method == "POST")
|
|
assert [request.target for request in posts] == ["/"]
|
|
assert _body(posts[0])["method"] == "message/send"
|
|
assert posts[0].headers["authorization"] == "Bearer client-token"
|
|
|
|
|
|
def test_a_url_agent_with_an_incomplete_databricks_oauth_answers_a2a_with_an_internal_error(gateway: Gateway) -> None:
|
|
marker: Final = f"url-agent-{uuid.uuid4().hex}"
|
|
with wire_server(lambda _: Reply(status=404)) as wire, gateway.scenario() as scenario:
|
|
created: Final = gateway.request(
|
|
"POST",
|
|
"/v1/agents",
|
|
{
|
|
"agent_name": marker,
|
|
"agent_card_params": {
|
|
"protocolVersion": "0.3",
|
|
"name": marker,
|
|
"description": "URL agent",
|
|
"url": wire.url + "/",
|
|
"version": "1.0.0",
|
|
"defaultInputModes": ["text"],
|
|
"defaultOutputModes": ["text"],
|
|
"capabilities": {"streaming": False},
|
|
"skills": [],
|
|
},
|
|
"litellm_params": {"databricks_oauth": {"client_id": _CLIENT_ID}},
|
|
},
|
|
)
|
|
assert created.status_code == 200, created.text
|
|
agent_id: Final = created.json()["agent_id"]
|
|
scenario.cleanups.callback(lambda: gateway.request("DELETE", f"/v1/agents/{agent_id}"))
|
|
response: Final = _a2a(gateway, marker, "message/send", marker)
|
|
assert response.status_code == 500, response.text
|
|
assert response.json()["error"]["code"] == -32603
|
|
assert "missing required field" in response.json()["error"]["message"]
|
|
assert tuple(request for request in wire.drain() if request.method == "POST") == ()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"pinned",
|
|
({"model": "another-endpoint"}, {"input": [{"role": "user", "content": "injected"}]}),
|
|
ids=("model", "input"),
|
|
)
|
|
@pytest.mark.parametrize(("drop_params", "status"), ((False, 400), (True, 200)), ids=("strict", "drop-params"))
|
|
def test_extra_body_never_replaces_the_agent_endpoint_or_input(
|
|
gateway: Gateway, pinned: dict[str, JsonValue], drop_params: bool, status: int
|
|
) -> None:
|
|
custom_inputs: Final[dict[str, JsonValue]] = {"tenant": "t-1"}
|
|
with _agent(gateway, _Databricks(), url_path=_UNIFIED, model=_ENDPOINT, api_key=_PAT) as (wire, name):
|
|
response: Final = _chat(
|
|
gateway,
|
|
name,
|
|
extra_body={**pinned, "custom_inputs": custom_inputs},
|
|
**({"drop_params": True} if drop_params else {}),
|
|
)
|
|
assert response.status_code == status, response.text
|
|
received: Final = wire.drain()
|
|
if drop_params:
|
|
assert [(request.method, request.target) for request in received] == [("POST", _UNIFIED)]
|
|
assert _body(received[0]) == {
|
|
"model": _ENDPOINT,
|
|
"input": [{"role": "user", "content": _PROMPT}],
|
|
"custom_inputs": custom_inputs,
|
|
}
|
|
else:
|
|
assert f"cannot set {next(iter(pinned))}" in response.text
|
|
assert received == ()
|
|
|
|
|
|
def test_a_token_outage_mid_burst_fails_its_calls_and_one_mint_serves_the_recovery(gateway: Gateway) -> None:
|
|
outage: Final = threading.Event()
|
|
outage.set()
|
|
|
|
def token_status() -> int:
|
|
return 503 if outage.is_set() else 200
|
|
|
|
async def burst(name: str, size: int) -> tuple[int, ...]:
|
|
async with httpx.AsyncClient(base_url=str(gateway.client.base_url), timeout=60, trust_env=False) as client:
|
|
responses: Final = await asyncio.gather(
|
|
*(
|
|
client.post(
|
|
"/v1/chat/completions",
|
|
json={"model": f"a2a/{name}", "messages": [{"role": "user", "content": _PROMPT}]},
|
|
headers={"Authorization": f"Bearer {gateway.key}"},
|
|
)
|
|
for _ in range(size)
|
|
)
|
|
)
|
|
return tuple(response.status_code for response in responses)
|
|
|
|
with _agent(
|
|
gateway,
|
|
_Databricks(token_status=token_status),
|
|
model=_ENDPOINT,
|
|
client_id=_CLIENT_ID,
|
|
client_secret=_CLIENT_SECRET,
|
|
) as (wire, name):
|
|
failed: Final = asyncio.run(burst(name, 12))
|
|
assert all(status >= 400 for status in failed), failed
|
|
assert _calls_to(wire.drain(), _INVOCATIONS) == ()
|
|
outage.clear()
|
|
recovered: Final = asyncio.run(burst(name, 12))
|
|
assert recovered == (200,) * 12, recovered
|
|
received: Final = wire.drain()
|
|
assert len(_calls_to(received, _TOKEN)) <= 2
|
|
for invocation in _calls_to(received, _INVOCATIONS):
|
|
_assert_invocation(invocation, _MINTED)
|
|
assert len(_calls_to(received, _INVOCATIONS)) == 12
|
|
|
|
|
|
def _owned_config(
|
|
directory: Path, wire: Wire, api_key: str | None = _PAT, *, allow_client_side_credentials: bool = False
|
|
) -> Path:
|
|
base: Final = _JSON_OBJECT.validate_python(yaml.safe_load(Path("tests/integration/proxy_config.yaml").read_text()))
|
|
general_settings: Final = _JSON_OBJECT.validate_python(base.get("general_settings") or {})
|
|
name: Final = "dbx-owned-agent"
|
|
config: Final[dict[str, JsonValue]] = {
|
|
**base,
|
|
"general_settings": {
|
|
**general_settings,
|
|
**({"allow_client_side_credentials": True} if allow_client_side_credentials else {}),
|
|
},
|
|
"agents": [
|
|
{
|
|
"agent_name": name,
|
|
"agent_card_params": {
|
|
"protocolVersion": "0.3",
|
|
"name": name,
|
|
"description": "Databricks Agent agent",
|
|
"url": wire.url,
|
|
"version": "1.0.0",
|
|
"defaultInputModes": ["text"],
|
|
"defaultOutputModes": ["text"],
|
|
"capabilities": {"streaming": True},
|
|
"skills": [],
|
|
},
|
|
"litellm_params": {
|
|
"custom_llm_provider": "databricks_agent",
|
|
"model": _ENDPOINT,
|
|
"api_base": wire.url,
|
|
**({"api_key": api_key} if api_key is not None else {}),
|
|
},
|
|
}
|
|
],
|
|
}
|
|
path: Final = directory / "databricks-agent-owned.yaml"
|
|
path.write_text(yaml.safe_dump(config))
|
|
return path
|
|
|
|
|
|
@pytest.mark.timeout(_OWNED_PROXY_BUDGET)
|
|
def test_a_client_api_base_or_base_url_never_takes_the_agent_pat_off_its_card_url(
|
|
gateway: Gateway, tmp_path: Path
|
|
) -> None:
|
|
with wire_server(_Databricks()) as wire, wire_server(lambda _: Reply(body=_agent_reply())) as collector:
|
|
config: Final = _owned_config(tmp_path, wire, allow_client_side_credentials=True)
|
|
with owned_proxy_process(gateway, tmp_path, {}, config=config, workers=2) as owned:
|
|
rejected: Final = _chat(owned.gateway, "dbx-owned-agent", api_base=collector.url)
|
|
served: Final = _chat(owned.gateway, "dbx-owned-agent", base_url=collector.url)
|
|
assert rejected.status_code == 400, rejected.text
|
|
assert served.status_code == 200, served.text
|
|
assert collector.drain() == ()
|
|
invocations: Final = _calls_to(wire.drain(), _INVOCATIONS)
|
|
assert len(invocations) == 1
|
|
_assert_invocation(invocations[0], _PAT)
|
|
|
|
|
|
@pytest.mark.timeout(_OWNED_PROXY_BUDGET)
|
|
def test_a_killed_worker_leaves_the_sibling_serving_the_databricks_agent(gateway: Gateway, tmp_path: Path) -> None:
|
|
with wire_server(_Databricks()) as wire:
|
|
with owned_proxy_process(gateway, tmp_path, {}, config=_owned_config(tmp_path, wire), workers=2) as owned:
|
|
candidate: Final = owned.gateway
|
|
workers: Final = eventually(
|
|
lambda: tuple(int(pid) for pid in _STARTED_WORKER.findall(owned.log.read_text())),
|
|
lambda pids: len(pids) == 2,
|
|
seconds=graceful_stop_seconds() + 60,
|
|
)
|
|
first: Final = _chat(candidate, "dbx-owned-agent")
|
|
assert first.status_code == 200, first.text
|
|
replacement: Final = owned.kill_worker(workers[0])
|
|
assert replacement not in workers
|
|
served: Final = tuple(_chat(candidate, "dbx-owned-agent") for _ in range(6))
|
|
assert all(response.status_code == 200 for response in served), [r.text for r in served]
|
|
assert {response.json()["choices"][0]["message"]["content"] for response in served} == {_REPLY}
|
|
invocations: Final = _calls_to(wire.drain(), _INVOCATIONS)
|
|
assert len(invocations) == 7
|
|
for invocation in invocations:
|
|
_assert_invocation(invocation, _PAT)
|
|
|
|
|
|
@pytest.mark.timeout(_OWNED_PROXY_BUDGET)
|
|
def test_the_env_service_principal_mints_once_over_an_env_pat_and_env_references_resolve_on_an_api_created_agent(
|
|
gateway: Gateway, tmp_path: Path
|
|
) -> None:
|
|
with wire_server(_Databricks()) as wire:
|
|
environment: Final = {
|
|
"DATABRICKS_CLIENT_ID": _CLIENT_ID,
|
|
"DATABRICKS_CLIENT_SECRET": _CLIENT_SECRET,
|
|
"DATABRICKS_HOST": wire.url,
|
|
"DATABRICKS_API_KEY": "env-pat",
|
|
"DBX_AGENT_HOST": wire.url,
|
|
"DBX_AGENT_PAT": _PAT,
|
|
}
|
|
with owned_proxy_process(gateway, tmp_path, environment, config=_owned_config(tmp_path, wire, None)) as owned:
|
|
candidate: Final = owned.gateway
|
|
served: Final = tuple(_chat(candidate, "dbx-owned-agent") for _ in range(3))
|
|
assert all(response.status_code == 200 for response in served), [r.text for r in served]
|
|
received: Final = wire.drain()
|
|
assert len(_calls_to(received, _TOKEN)) == 1, received
|
|
_assert_token_request(_calls_to(received, _TOKEN)[0])
|
|
for invocation in _calls_to(received, _INVOCATIONS):
|
|
_assert_invocation(invocation, _MINTED)
|
|
assert len(_calls_to(received, _INVOCATIONS)) == 3
|
|
|
|
with candidate.scenario() as scenario:
|
|
name: Final = _register(
|
|
candidate,
|
|
scenario.cleanups.callback,
|
|
wire.url,
|
|
model=_ENDPOINT,
|
|
api_base="os.environ/DBX_AGENT_HOST",
|
|
api_key="os.environ/DBX_AGENT_PAT",
|
|
)
|
|
response: Final = _chat(candidate, name)
|
|
assert response.status_code == 200, response.text
|
|
invocations: Final = _calls_to(wire.drain(), _INVOCATIONS)
|
|
assert len(invocations) == 1, invocations
|
|
_assert_invocation(invocations[0], _PAT)
|