mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-01 15:36:08 +00:00
620 lines
23 KiB
Python
620 lines
23 KiB
Python
"""P0 gate tests — provider layer. SDK-free (inject a fake OpenAI client)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from types import SimpleNamespace
|
|
|
|
from coworker.providers import (
|
|
AssistantTurn,
|
|
ModelCapabilities,
|
|
OpenAIProvider,
|
|
ToolCall,
|
|
capabilities_for,
|
|
)
|
|
|
|
|
|
class _FakeCompletions:
|
|
def __init__(self, response):
|
|
self._response = response
|
|
self.calls: list[dict] = []
|
|
|
|
def create(self, **kwargs):
|
|
self.calls.append(kwargs)
|
|
return self._response
|
|
|
|
|
|
class _FakeClient:
|
|
def __init__(self, response):
|
|
self.chat = SimpleNamespace(completions=_FakeCompletions(response))
|
|
|
|
|
|
def _response(content=None, tool_calls=None, finish_reason="stop"):
|
|
message = SimpleNamespace(content=content, tool_calls=tool_calls)
|
|
choice = SimpleNamespace(message=message, finish_reason=finish_reason)
|
|
return SimpleNamespace(choices=[choice])
|
|
|
|
|
|
def test_complete_returns_text():
|
|
client = _FakeClient(_response(content="hello there"))
|
|
provider = OpenAIProvider(client=client)
|
|
|
|
turn = provider.complete(
|
|
model="gpt-5.5", messages=[{"role": "user", "content": "hi"}]
|
|
)
|
|
|
|
assert isinstance(turn, AssistantTurn)
|
|
assert turn.text == "hello there"
|
|
assert turn.tool_calls == []
|
|
assert turn.has_tool_calls is False
|
|
assert turn.finish_reason == "stop"
|
|
|
|
|
|
def test_complete_parses_tool_calls():
|
|
tc = SimpleNamespace(
|
|
id="call_1",
|
|
function=SimpleNamespace(
|
|
name="read_file", arguments=json.dumps({"path": "a.py"})
|
|
),
|
|
)
|
|
client = _FakeClient(_response(tool_calls=[tc], finish_reason="tool_calls"))
|
|
provider = OpenAIProvider(client=client)
|
|
|
|
turn = provider.complete(
|
|
model="gpt-5.5",
|
|
messages=[],
|
|
tools=[{"type": "function", "function": {"name": "read_file"}}],
|
|
)
|
|
|
|
assert turn.has_tool_calls
|
|
assert turn.tool_calls[0] == ToolCall(
|
|
id="call_1", name="read_file", arguments={"path": "a.py"}
|
|
)
|
|
# tools forwarded to the API
|
|
assert "tools" in client.chat.completions.calls[0]
|
|
|
|
|
|
def test_complete_tolerates_bad_tool_args():
|
|
tc = SimpleNamespace(
|
|
id="call_2", function=SimpleNamespace(name="x", arguments="{not json")
|
|
)
|
|
client = _FakeClient(_response(tool_calls=[tc]))
|
|
provider = OpenAIProvider(client=client)
|
|
|
|
turn = provider.complete(model="gpt-5.5", messages=[])
|
|
|
|
assert turn.tool_calls[0].arguments == {"_raw": "{not json"}
|
|
|
|
|
|
def test_tools_omitted_when_none():
|
|
client = _FakeClient(_response(content="x"))
|
|
provider = OpenAIProvider(client=client)
|
|
|
|
provider.complete(model="gpt-5.5", messages=[])
|
|
|
|
assert "tools" not in client.chat.completions.calls[0]
|
|
|
|
|
|
def test_settings_forwarded():
|
|
client = _FakeClient(_response(content="x"))
|
|
provider = OpenAIProvider(client=client)
|
|
|
|
provider.complete(model="gpt-5.5", messages=[], temperature=0.2)
|
|
|
|
assert client.chat.completions.calls[0]["temperature"] == 0.2
|
|
|
|
|
|
def test_capabilities_known_models():
|
|
assert capabilities_for("gpt-5.5").tools is True
|
|
assert capabilities_for("openai:gpt-5.5").vision is True # provider prefix stripped
|
|
assert capabilities_for("o3-mini").parallel_tool_calls is False
|
|
assert capabilities_for("deepseek-chat").tools is True
|
|
|
|
|
|
def test_capabilities_via_provider():
|
|
provider = OpenAIProvider(client=_FakeClient(_response()))
|
|
caps = provider.capabilities("gpt-5.5")
|
|
assert isinstance(caps, ModelCapabilities)
|
|
assert caps.tools is True
|
|
|
|
|
|
# -- GPT-5.6 tools + reasoning_effort on chat/completions (owner repro 2026-07-14) ----
|
|
# The API defaults these models to effort "medium" and then rejects function tools:
|
|
# "Function tools with reasoning_effort are not supported for gpt-5.6-sol in
|
|
# /v1/chat/completions. To use function tools, use /v1/responses or set
|
|
# reasoning_effort to 'none'." Until we speak the Responses API, we pin effort none.
|
|
|
|
_TOOLS = [{"type": "function", "function": {"name": "read_file"}}]
|
|
_EFFORT_400 = (
|
|
"Error code: 400 - {'error': {'message': \"Function tools with reasoning_effort "
|
|
"are not supported for %s in /v1/chat/completions. To use function tools, use "
|
|
"/v1/responses or set reasoning_effort to 'none'.\", 'type': "
|
|
"'invalid_request_error', 'param': 'reasoning_effort', 'code': None}}"
|
|
)
|
|
|
|
|
|
def test_gpt56_tools_pin_reasoning_effort_none():
|
|
client = _FakeClient(_response(content="x"))
|
|
provider = OpenAIProvider(client=client)
|
|
calls = client.chat.completions.calls
|
|
|
|
for model in ("gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"):
|
|
provider.complete(model=model, messages=[], tools=_TOOLS)
|
|
assert [c["reasoning_effort"] for c in calls] == ["none"] * 3
|
|
|
|
# an explicit caller choice is respected on the first attempt
|
|
provider.complete(
|
|
model="gpt-5.6-sol", messages=[], tools=_TOOLS, reasoning_effort="low"
|
|
)
|
|
assert calls[3]["reasoning_effort"] == "low"
|
|
|
|
# no tools, or another model → the request is untouched
|
|
provider.complete(model="gpt-5.6-sol", messages=[])
|
|
provider.complete(model="gpt-5.5", messages=[], tools=_TOOLS)
|
|
assert "reasoning_effort" not in calls[4] and "reasoning_effort" not in calls[5]
|
|
|
|
|
|
class _EffortRejectingCompletions:
|
|
"""Behaves like the live API: tools + any effort other than 'none' → the 400."""
|
|
|
|
def __init__(self, response):
|
|
self._response = response
|
|
self.calls: list[dict] = []
|
|
|
|
def create(self, **kwargs):
|
|
self.calls.append(kwargs)
|
|
if kwargs.get("tools") and kwargs.get("reasoning_effort") != "none":
|
|
raise RuntimeError(_EFFORT_400 % kwargs["model"])
|
|
if kwargs.get("stream"):
|
|
return iter([_chunk(content="ok"), _chunk(finish="stop")])
|
|
return self._response
|
|
|
|
|
|
def test_effort_400_from_an_unpinned_model_retries_once_at_none():
|
|
# a hypothetical next generation we haven't listed yet — proactive pin misses it
|
|
client = _FakeClient(_response(content="x"))
|
|
client.chat.completions = _EffortRejectingCompletions(_response(content="x"))
|
|
provider = OpenAIProvider(client=client)
|
|
|
|
turn = provider.complete(model="gpt-5.7-sol", messages=[], tools=_TOOLS)
|
|
calls = client.chat.completions.calls
|
|
assert turn.text == "x" and len(calls) == 2
|
|
assert "reasoning_effort" not in calls[0] and calls[1]["reasoning_effort"] == "none"
|
|
|
|
# streaming path retries the same way
|
|
out = list(provider.stream(model="gpt-5.7-sol", messages=[], tools=_TOOLS))
|
|
assert out[-1].turn.text == "ok" and len(client.chat.completions.calls) == 4
|
|
|
|
|
|
def test_max_tokens_rejection_retries_as_max_completion_tokens():
|
|
"""Reasoning-routed models 400 on max_tokens (want max_completion_tokens); compat
|
|
servers know only max_tokens — so the swap happens on rejection, never up front.
|
|
(Owner-hit 2026-07-20: the auto-title call silently no-oped on gpt-5.6-sol.)"""
|
|
|
|
class _MaxTokensRejecting:
|
|
def __init__(self, response):
|
|
self._response = response
|
|
self.calls: list[dict] = []
|
|
|
|
def create(self, **kwargs):
|
|
self.calls.append(kwargs)
|
|
if "max_tokens" in kwargs:
|
|
raise RuntimeError(
|
|
"Error code: 400 - Unsupported parameter: 'max_tokens' is not "
|
|
"supported with this model. Use 'max_completion_tokens' instead."
|
|
)
|
|
return self._response
|
|
|
|
client = _FakeClient(_response(content="Jira vs Linear"))
|
|
client.chat.completions = _MaxTokensRejecting(_response(content="Jira vs Linear"))
|
|
provider = OpenAIProvider(client=client)
|
|
|
|
turn = provider.complete(model="gpt-5.6-sol", messages=[], max_tokens=64)
|
|
calls = client.chat.completions.calls
|
|
assert turn.text == "Jira vs Linear" and len(calls) == 2
|
|
assert calls[0]["max_tokens"] == 64
|
|
assert "max_tokens" not in calls[1] and calls[1]["max_completion_tokens"] == 64
|
|
|
|
|
|
def test_unrelated_400s_are_not_retried():
|
|
class _AlwaysRejects:
|
|
calls: list = []
|
|
|
|
def create(self, **kwargs):
|
|
self.calls.append(kwargs)
|
|
raise RuntimeError("Error code: 400 - context_length_exceeded")
|
|
|
|
client = _FakeClient(_response(content="x"))
|
|
client.chat.completions = _AlwaysRejects()
|
|
provider = OpenAIProvider(client=client)
|
|
try:
|
|
provider.complete(model="gpt-5.5", messages=[], tools=_TOOLS)
|
|
raise AssertionError("should have raised")
|
|
except RuntimeError:
|
|
pass
|
|
assert len(client.chat.completions.calls) == 1 # no blind second attempt
|
|
|
|
|
|
# -- streaming ------------------------------------------------------------------
|
|
|
|
|
|
def _chunk(content=None, tool_call=None, finish=None):
|
|
delta = SimpleNamespace(
|
|
content=content, tool_calls=[tool_call] if tool_call else None
|
|
)
|
|
return SimpleNamespace(choices=[SimpleNamespace(delta=delta, finish_reason=finish)])
|
|
|
|
|
|
class _StreamClient:
|
|
def __init__(self, chunks):
|
|
self.chat = SimpleNamespace(
|
|
completions=SimpleNamespace(create=lambda **kwargs: iter(chunks))
|
|
)
|
|
|
|
|
|
def test_stream_text_deltas():
|
|
chunks = [_chunk(content="Hel"), _chunk(content="lo"), _chunk(finish="stop")]
|
|
provider = OpenAIProvider(client=_StreamClient(chunks))
|
|
out = list(provider.stream(model="gpt-5.5", messages=[]))
|
|
assert [c.text_delta for c in out if c.text_delta] == ["Hel", "lo"]
|
|
assert out[-1].turn.text == "Hello"
|
|
assert out[-1].turn.finish_reason == "stop"
|
|
|
|
|
|
def test_stream_accumulates_tool_calls():
|
|
tc1 = SimpleNamespace(
|
|
index=0,
|
|
id="call_1",
|
|
function=SimpleNamespace(name="read_file", arguments='{"pa'),
|
|
)
|
|
tc2 = SimpleNamespace(
|
|
index=0, id=None, function=SimpleNamespace(name=None, arguments='th": "a.py"}')
|
|
)
|
|
chunks = [_chunk(tool_call=tc1), _chunk(tool_call=tc2), _chunk(finish="tool_calls")]
|
|
provider = OpenAIProvider(client=_StreamClient(chunks))
|
|
turn = list(provider.stream(model="gpt-5.5", messages=[]))[-1].turn
|
|
assert turn.tool_calls[0] == ToolCall(
|
|
id="call_1", name="read_file", arguments={"path": "a.py"}
|
|
)
|
|
|
|
|
|
# -- OpenAI-compatible vendor providers (Z AI, DeepSeek, Kimi, MiniMax, Qwen, xAI, Mistral) ------
|
|
|
|
COMPAT_VENDORS = {
|
|
"zai": "https://api.z.ai/api/paas/v4",
|
|
"deepseek": "https://api.deepseek.com",
|
|
"kimi": "https://api.moonshot.ai/v1",
|
|
"minimax": "https://api.minimax.io/v1",
|
|
"qwen": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
|
"xai": "https://api.x.ai/v1",
|
|
"mistral": "https://api.mistral.ai/v1",
|
|
}
|
|
|
|
|
|
def test_compat_vendor_descriptors_ship_prefilled_endpoints():
|
|
from coworker.providers.registry import get_descriptor
|
|
|
|
for name, endpoint in COMPAT_VENDORS.items():
|
|
d = get_descriptor(name)
|
|
assert d is not None and d.needs_key, name
|
|
base = next(f for f in d.fields if f.key == "base_url")
|
|
assert base.default == endpoint # prefilled, editable
|
|
assert not base.required # blank falls back to the default in the builder
|
|
assert "OpenAI-compatible" in d.blurb
|
|
assert d.env_key and d.recommended_model
|
|
|
|
|
|
def test_compat_builder_defaults_and_profile_override(monkeypatch):
|
|
from coworker.providers.registry import build_provider_client
|
|
|
|
p = build_provider_client("zai", {"api_key": "zk"}, None)
|
|
assert p._base_url == COMPAT_VENDORS["zai"]
|
|
assert p._api_key == "zk"
|
|
|
|
override = "https://open.bigmodel.cn/api/paas/v4"
|
|
p2 = build_provider_client("zai", {"api_key": "zk", "base_url": override}, None)
|
|
assert p2._base_url == override
|
|
|
|
|
|
def test_compat_builder_env_key_fallback(monkeypatch):
|
|
from coworker.providers.registry import build_provider_client
|
|
|
|
monkeypatch.setenv("DEEPSEEK_API_KEY", "ds-key")
|
|
p = build_provider_client("deepseek", {}, None)
|
|
assert p._api_key == "ds-key"
|
|
assert p._base_url == COMPAT_VENDORS["deepseek"]
|
|
|
|
|
|
def test_compat_builder_never_leaks_the_openai_key(monkeypatch):
|
|
"""A configured OPENAI_API_KEY must never be sent to a different vendor's endpoint —
|
|
a missing vendor key fails fast with a vendor-named error instead."""
|
|
import pytest
|
|
|
|
from coworker.providers.registry import build_provider_client
|
|
|
|
monkeypatch.setenv("OPENAI_API_KEY", "sk-openai-real")
|
|
monkeypatch.delenv("MOONSHOT_API_KEY", raising=False)
|
|
with pytest.raises(RuntimeError, match="Kimi"):
|
|
build_provider_client("kimi", {}, None)
|
|
|
|
|
|
ARK_RESPONSES_VENDORS = {
|
|
"ark": {
|
|
"base_url": "https://ark.ap-southeast.bytepluses.com/api/v3",
|
|
"env_key": "ARK_API_KEY",
|
|
"recommended_model": "dola-seed-evolving-latest-version",
|
|
"reasoning_summary": False,
|
|
},
|
|
"ark-agent-plan-cn": {
|
|
"base_url": "https://ark.cn-beijing.volces.com/api/plan/v3",
|
|
"env_key": "ARK_AGENT_PLAN_CN_API_KEY",
|
|
"recommended_model": "doubao-seed-evolving",
|
|
"reasoning_summary": True,
|
|
},
|
|
}
|
|
|
|
|
|
def test_ark_responses_descriptors_are_separate():
|
|
from coworker.providers.registry import get_descriptor
|
|
|
|
for name, expected in ARK_RESPONSES_VENDORS.items():
|
|
d = get_descriptor(name)
|
|
assert d is not None and d.needs_key, name
|
|
assert d.env_key == expected["env_key"]
|
|
assert d.recommended_model == expected["recommended_model"]
|
|
assert "Responses API" in d.blurb
|
|
base = next(f for f in d.fields if f.key == "base_url")
|
|
assert base.default == expected["base_url"]
|
|
assert not base.required
|
|
|
|
|
|
def test_ark_responses_builder_capabilities_PathsUnchanged(monkeypatch):
|
|
from coworker.providers.openai_responses import OpenAIResponsesProvider
|
|
from coworker.providers.registry import build_provider_client
|
|
|
|
monkeypatch.setenv("ARK_AGENT_PLAN_CN_API_KEY", "plan-key")
|
|
bp = build_provider_client("ark", {"api_key": "bp-key"}, None)
|
|
plan = build_provider_client("ark-agent-plan-cn", {}, None)
|
|
|
|
assert isinstance(bp, OpenAIResponsesProvider)
|
|
assert (bp._api_key, bp._base_url) == (
|
|
"bp-key",
|
|
ARK_RESPONSES_VENDORS["ark"]["base_url"],
|
|
)
|
|
assert isinstance(plan, OpenAIResponsesProvider)
|
|
assert (plan._api_key, plan._base_url) == (
|
|
"plan-key",
|
|
ARK_RESPONSES_VENDORS["ark-agent-plan-cn"]["base_url"],
|
|
)
|
|
assert bp._reasoning_summary is ARK_RESPONSES_VENDORS["ark"]["reasoning_summary"]
|
|
assert plan._reasoning_summary is ARK_RESPONSES_VENDORS["ark-agent-plan-cn"][
|
|
"reasoning_summary"
|
|
]
|
|
|
|
|
|
def test_ark_responses_never_leak_the_openai_key(monkeypatch):
|
|
import pytest
|
|
|
|
from coworker.providers.registry import build_provider_client
|
|
|
|
monkeypatch.setenv("OPENAI_API_KEY", "sk-openai-real")
|
|
monkeypatch.delenv("ARK_API_KEY", raising=False)
|
|
with pytest.raises(RuntimeError, match="BytePlus Ark"):
|
|
build_provider_client("ark", {}, None)
|
|
|
|
|
|
def test_existing_chat_compat_paths_unchanged():
|
|
"""Lockdown: adding Responses vendors must not migrate existing compat providers."""
|
|
from coworker.providers.registry import build_provider_client
|
|
|
|
provider = build_provider_client("deepseek", {"api_key": "ds-key"}, None)
|
|
assert isinstance(provider, OpenAIProvider)
|
|
assert provider._base_url == COMPAT_VENDORS["deepseek"]
|
|
|
|
|
|
def test_ark_curated_models_are_strict_allowlists():
|
|
from coworker.providers.matrix import models_for_provider
|
|
|
|
assert models_for_provider("ark") == [
|
|
"dola-seed-evolving-latest-version",
|
|
"dola-seed-2-1-turbo-260628",
|
|
]
|
|
assert models_for_provider("ark-agent-plan-cn") == [
|
|
"doubao-seed-evolving",
|
|
"doubao-seed-2.1-turbo",
|
|
]
|
|
|
|
|
|
def test_ark_models_route_and_get_verified_agent_capabilities():
|
|
from coworker.providers.router import ProviderRouter
|
|
|
|
models = (
|
|
"ark:dola-seed-evolving-latest-version",
|
|
"ark:dola-seed-2-1-turbo-260628",
|
|
"ark-agent-plan-cn:doubao-seed-evolving",
|
|
"ark-agent-plan-cn:doubao-seed-2.1-turbo",
|
|
)
|
|
router = ProviderRouter.__new__(ProviderRouter)
|
|
for model in models:
|
|
prefix, bare = model.split(":", 1)
|
|
assert router._provider_name(model) == prefix
|
|
assert ProviderRouter._bare(model) == bare
|
|
caps = capabilities_for(model)
|
|
assert caps.tools and caps.parallel_tool_calls and caps.streaming
|
|
assert not caps.vision
|
|
|
|
|
|
def test_ark_recommended_models_are_curated():
|
|
from coworker.providers.matrix import models_for_provider
|
|
from coworker.providers.registry import get_descriptor
|
|
|
|
for name in ARK_RESPONSES_VENDORS:
|
|
d = get_descriptor(name)
|
|
assert d.recommended_model in models_for_provider(name)
|
|
|
|
|
|
def test_compat_models_route_and_get_tool_capabilities():
|
|
from coworker.providers.router import ProviderRouter
|
|
|
|
router = ProviderRouter.__new__(
|
|
ProviderRouter
|
|
) # only using _provider_name (stateless)
|
|
for model in (
|
|
"zai:glm-5.2",
|
|
"deepseek:deepseek-v4-flash",
|
|
"kimi:kimi-k2.6",
|
|
"minimax:MiniMax-M2.5",
|
|
"qwen:qwen3-max",
|
|
"xai:grok-4.3",
|
|
"mistral:mistral-large-latest",
|
|
):
|
|
prefix = model.split(":", 1)[0]
|
|
assert router._provider_name(model) == prefix
|
|
assert ProviderRouter._bare(model) == model.split(":", 1)[1]
|
|
caps = capabilities_for(model)
|
|
assert caps.tools and caps.streaming
|
|
|
|
|
|
def test_compat_recommended_models_are_in_the_suggested_lists():
|
|
"""set_provider only auto-adds the recommended model if it's in _suggested_models —
|
|
keep the registry and the manager's COMPAT_MODELS table in lockstep."""
|
|
from coworker.providers.registry import get_descriptor
|
|
from coworker.server.manager import SessionManager
|
|
|
|
for name in COMPAT_VENDORS:
|
|
d = get_descriptor(name)
|
|
assert d.recommended_model in SessionManager.COMPAT_MODELS[name], name
|
|
|
|
|
|
# -- curated model matrix (labels + capabilities by full routed id) -----------------
|
|
|
|
|
|
def test_matrix_answers_capabilities_for_reseller_ids():
|
|
"""Reseller ids ('together:zai-org/GLM-5.2') defeat the name-prefix heuristics — the
|
|
matrix must answer them exactly, with tool calling on."""
|
|
for mid in (
|
|
"together:zai-org/GLM-5.2",
|
|
"together:meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
|
|
"fireworks:accounts/fireworks/models/kimi-k2p6",
|
|
"openrouter:z-ai/glm-5.2",
|
|
"openrouter:meta-llama/llama-4-maverick",
|
|
):
|
|
caps = capabilities_for(mid)
|
|
assert caps.tools and caps.parallel_tool_calls and caps.streaming
|
|
|
|
|
|
def test_matrix_labels_and_custom_model_fallback():
|
|
from coworker.providers.matrix import MATRIX, model_labels
|
|
|
|
labels = model_labels()
|
|
assert labels["together:zai-org/GLM-5.2"] == "GLM-5.2 · via Together"
|
|
assert labels["zai:glm-5.2"] == "GLM-5.2 · Z AI"
|
|
# Deliberately small: agent-capable current models only (owner call, 2026-07-04).
|
|
# 60→65 (2026-08-24): the stealth ox-alpha preview slug tipped it; reclaim slack by
|
|
# pruning retired entries before raising this again.
|
|
assert len(MATRIX) < 65
|
|
assert all(e.caps.tools for e in MATRIX.values())
|
|
# A custom (unlisted) reseller model falls back to the conservative default — usable,
|
|
# but at the user's own risk (no parallel tool calls assumed).
|
|
caps = capabilities_for("together:some-org/Brand-New-Model")
|
|
assert caps.tools and not caps.parallel_tool_calls
|
|
|
|
|
|
def test_reseller_descriptors_and_matrix_stay_in_lockstep():
|
|
"""Reseller suggested models derive from the matrix, and each descriptor's
|
|
recommended model must be one of them (set_provider's auto-add depends on it)."""
|
|
from coworker.providers.matrix import models_for_provider
|
|
from coworker.providers.registry import get_descriptor
|
|
|
|
for name in ("together", "fireworks", "openrouter"):
|
|
d = get_descriptor(name)
|
|
assert d is not None and d.needs_key
|
|
curated = models_for_provider(name)
|
|
assert curated and d.recommended_model in curated
|
|
# full ids in the matrix must round-trip: prefix + bare == matrix key
|
|
base = next(f for f in d.fields if f.key == "base_url")
|
|
assert base.default.startswith("https://")
|
|
|
|
|
|
def test_foreign_sidecars_stripped_from_outbound_messages():
|
|
"""Provider-private sidecars (`_gemini` thought signatures et al) must never reach the
|
|
OpenAI wire — it and its compat servers reject unknown message fields."""
|
|
client = _FakeClient(_response(content="ok"))
|
|
provider = OpenAIProvider(client=client)
|
|
provider.complete(
|
|
model="gpt-5.5",
|
|
messages=[
|
|
{"role": "user", "content": "hi"},
|
|
{"role": "assistant", "content": "prev", "_gemini": {"call_sigs": ["x"]}},
|
|
],
|
|
)
|
|
sent = client.chat.completions.calls[0]["messages"]
|
|
assert sent[1] == {"role": "assistant", "content": "prev"}
|
|
|
|
|
|
def test_stream_reasoning_content_deltas():
|
|
"""DeepSeek-style thinking: reasoning_content deltas surface as reasoning chunks and
|
|
land on the final turn — never mixed into the answer text."""
|
|
def rchunk(text):
|
|
delta = SimpleNamespace(content=None, tool_calls=None, reasoning_content=text)
|
|
return SimpleNamespace(choices=[SimpleNamespace(delta=delta, finish_reason=None)])
|
|
|
|
chunks = [rchunk("hmm "), rchunk("okay."), _chunk(content="Answer"), _chunk(finish="stop")]
|
|
provider = OpenAIProvider(client=_StreamClient(chunks))
|
|
out = list(provider.stream(model="deepseek-v4-pro", messages=[]))
|
|
assert [c.reasoning_delta for c in out if c.reasoning_delta] == ["hmm ", "okay."]
|
|
final = out[-1].turn
|
|
assert final.text == "Answer" and final.reasoning == "hmm okay."
|
|
|
|
|
|
def test_complete_picks_up_reasoning_content():
|
|
message = SimpleNamespace(content="Answer", tool_calls=None, reasoning_content="deep thought")
|
|
choice = SimpleNamespace(message=message, finish_reason="stop")
|
|
provider = OpenAIProvider(client=_FakeClient(SimpleNamespace(choices=[choice])))
|
|
turn = provider.complete(model="deepseek-v4-pro", messages=[{"role": "user", "content": "x"}])
|
|
assert turn.text == "Answer" and turn.reasoning == "deep thought"
|
|
|
|
|
|
def test_default_max_tokens_injected_and_caller_setting_wins():
|
|
"""Compat servers left to their OWN defaults cap completions absurdly low
|
|
(owner-hit 2026-08-15: Together defaulted Kimi K3 to ~2k tokens, so every report
|
|
write truncated mid-arguments). The request always names a ceiling now."""
|
|
from coworker.providers.openai_provider import DEFAULT_MAX_TOKENS
|
|
|
|
client = _FakeClient(_response(content="ok"))
|
|
provider = OpenAIProvider(client=client)
|
|
provider.complete(model="kimi-k3", messages=[])
|
|
assert client.chat.completions.calls[0]["max_tokens"] == DEFAULT_MAX_TOKENS
|
|
|
|
client2 = _FakeClient(_response(content="ok"))
|
|
provider2 = OpenAIProvider(client=client2)
|
|
provider2.complete(model="kimi-k3", messages=[], max_tokens=512)
|
|
assert client2.chat.completions.calls[0]["max_tokens"] == 512
|
|
|
|
|
|
def test_over_limit_max_tokens_is_dropped_and_retried():
|
|
"""A model whose completion limit sits below our default must not surface the 400:
|
|
drop the param, retry on the server's own default (yesterday's behavior, at worst)."""
|
|
|
|
class _LimitRejecting:
|
|
def __init__(self, response):
|
|
self._response = response
|
|
self.calls: list[dict] = []
|
|
|
|
def create(self, **kwargs):
|
|
self.calls.append(kwargs)
|
|
if "max_tokens" in kwargs:
|
|
raise RuntimeError(
|
|
"Error code: 400 - max_tokens must be at most 8193 for this model"
|
|
)
|
|
return self._response
|
|
|
|
client = _FakeClient(_response(content="ok"))
|
|
client.chat.completions = _LimitRejecting(_response(content="ok"))
|
|
provider = OpenAIProvider(client=client)
|
|
|
|
turn = provider.complete(model="tiny-model", messages=[])
|
|
calls = client.chat.completions.calls
|
|
assert turn.text == "ok" and len(calls) == 2
|
|
assert "max_tokens" not in calls[1]
|