mirror of https://github.com/aliasrobotics/cai.git
177 lines
6.1 KiB
Python
177 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import AsyncIterator
|
|
from typing import Any
|
|
|
|
import httpx
|
|
import pytest
|
|
from openai import NOT_GIVEN
|
|
from openai.types.chat.chat_completion import ChatCompletion, Choice
|
|
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
|
|
from openai.types.chat.chat_completion_message import ChatCompletionMessage
|
|
from openai.types.chat.chat_completion_message_tool_call import (
|
|
ChatCompletionMessageToolCall,
|
|
Function,
|
|
)
|
|
from openai.types.completion_usage import CompletionUsage
|
|
from openai.types.responses import (
|
|
Response,
|
|
ResponseFunctionToolCall,
|
|
ResponseOutputMessage,
|
|
ResponseOutputRefusal,
|
|
ResponseOutputText,
|
|
)
|
|
|
|
from cai.sdk.agents import (
|
|
ModelResponse,
|
|
ModelSettings,
|
|
ModelTracing,
|
|
OpenAIChatCompletionsModel,
|
|
OpenAIProvider,
|
|
generation_span,
|
|
)
|
|
from cai.sdk.agents.models.fake_id import FAKE_RESPONSES_ID
|
|
import os
|
|
cai_model = os.getenv('CAI_MODEL', "qwen2.5:14b")
|
|
|
|
@pytest.mark.allow_call_model_methods
|
|
@pytest.mark.asyncio
|
|
async def test_get_response_with_text_message(monkeypatch) -> None:
|
|
"""
|
|
When the model returns a ChatCompletionMessage with plain text content,
|
|
`get_response` should produce a single `ResponseOutputMessage` containing
|
|
a `ResponseOutputText` with that content, and a `Usage` populated from
|
|
the completion's usage.
|
|
"""
|
|
msg = ChatCompletionMessage(role="assistant", content="Hello")
|
|
choice = Choice(index=0, finish_reason="stop", message=msg)
|
|
chat = ChatCompletion(
|
|
id="resp-id",
|
|
created=0,
|
|
model="fake",
|
|
object="chat.completion",
|
|
choices=[choice],
|
|
usage=CompletionUsage(completion_tokens=5, prompt_tokens=7, total_tokens=12),
|
|
)
|
|
|
|
async def patched_fetch_response(self, *args, **kwargs):
|
|
return chat
|
|
|
|
monkeypatch.setattr(OpenAIChatCompletionsModel, "_fetch_response", patched_fetch_response)
|
|
model = OpenAIProvider(use_responses=False).get_model(cai_model)
|
|
resp: ModelResponse = await model.get_response(
|
|
system_instructions=None,
|
|
input="",
|
|
model_settings=ModelSettings(),
|
|
tools=[],
|
|
output_schema=None,
|
|
handoffs=[],
|
|
tracing=ModelTracing.DISABLED,
|
|
)
|
|
# Should have produced exactly one output message with one text part
|
|
assert isinstance(resp, ModelResponse)
|
|
assert len(resp.output) == 1
|
|
assert isinstance(resp.output[0], ResponseOutputMessage)
|
|
msg_item = resp.output[0]
|
|
assert len(msg_item.content) == 1
|
|
assert isinstance(msg_item.content[0], ResponseOutputText)
|
|
assert msg_item.content[0].text == "Hello"
|
|
# Usage should be preserved from underlying ChatCompletion.usage
|
|
assert resp.usage.input_tokens == 7
|
|
assert resp.usage.output_tokens == 5
|
|
assert resp.usage.total_tokens == 12
|
|
assert resp.referenceable_id is None
|
|
|
|
|
|
@pytest.mark.allow_call_model_methods
|
|
@pytest.mark.asyncio
|
|
async def test_get_response_with_refusal(monkeypatch) -> None:
|
|
"""
|
|
When the model returns a ChatCompletionMessage with a `refusal` instead
|
|
of normal `content`, `get_response` should produce a single
|
|
`ResponseOutputMessage` containing a `ResponseOutputRefusal` part.
|
|
"""
|
|
msg = ChatCompletionMessage(role="assistant", refusal="No thanks")
|
|
choice = Choice(index=0, finish_reason="stop", message=msg)
|
|
chat = ChatCompletion(
|
|
id="resp-id",
|
|
created=0,
|
|
model="fake",
|
|
object="chat.completion",
|
|
choices=[choice],
|
|
usage=None,
|
|
)
|
|
|
|
async def patched_fetch_response(self, *args, **kwargs):
|
|
return chat
|
|
|
|
monkeypatch.setattr(OpenAIChatCompletionsModel, "_fetch_response", patched_fetch_response)
|
|
model = OpenAIProvider(use_responses=False).get_model(cai_model)
|
|
resp: ModelResponse = await model.get_response(
|
|
system_instructions=None,
|
|
input="",
|
|
model_settings=ModelSettings(),
|
|
tools=[],
|
|
output_schema=None,
|
|
handoffs=[],
|
|
tracing=ModelTracing.DISABLED,
|
|
)
|
|
assert len(resp.output) == 1
|
|
assert isinstance(resp.output[0], ResponseOutputMessage)
|
|
refusal_part = resp.output[0].content[0]
|
|
assert isinstance(refusal_part, ResponseOutputRefusal)
|
|
assert refusal_part.refusal == "No thanks"
|
|
# With no usage from the completion, usage defaults to zeros.
|
|
assert resp.usage.requests == 1
|
|
assert resp.usage.input_tokens == 5
|
|
assert resp.usage.output_tokens == 0
|
|
|
|
|
|
@pytest.mark.allow_call_model_methods
|
|
@pytest.mark.asyncio
|
|
async def test_get_response_with_tool_call(monkeypatch) -> None:
|
|
"""
|
|
If the ChatCompletionMessage includes one or more tool_calls, `get_response`
|
|
should append corresponding `ResponseFunctionToolCall` items after the
|
|
assistant message item with matching name/arguments.
|
|
"""
|
|
tool_call = ChatCompletionMessageToolCall(
|
|
id="call-id",
|
|
type="function",
|
|
function=Function(name="do_thing", arguments="{'x':1}"),
|
|
)
|
|
msg = ChatCompletionMessage(role="assistant", content="Hi", tool_calls=[tool_call])
|
|
choice = Choice(index=0, finish_reason="stop", message=msg)
|
|
chat = ChatCompletion(
|
|
id="resp-id",
|
|
created=0,
|
|
model="fake",
|
|
object="chat.completion",
|
|
choices=[choice],
|
|
usage=None,
|
|
)
|
|
|
|
async def patched_fetch_response(self, *args, **kwargs):
|
|
return chat
|
|
|
|
monkeypatch.setattr(OpenAIChatCompletionsModel, "_fetch_response", patched_fetch_response)
|
|
model = OpenAIProvider(use_responses=False).get_model(cai_model)
|
|
resp: ModelResponse = await model.get_response(
|
|
system_instructions=None,
|
|
input="",
|
|
model_settings=ModelSettings(),
|
|
tools=[],
|
|
output_schema=None,
|
|
handoffs=[],
|
|
tracing=ModelTracing.DISABLED,
|
|
)
|
|
# Expect a message item followed by a function tool call item.
|
|
assert len(resp.output) == 2
|
|
assert isinstance(resp.output[0], ResponseOutputMessage)
|
|
fn_call_item = resp.output[1]
|
|
assert isinstance(fn_call_item, ResponseFunctionToolCall)
|
|
assert fn_call_item.call_id == "call-id"
|
|
assert fn_call_item.name == "do_thing"
|
|
assert fn_call_item.arguments == "{'x':1}"
|
|
|