mirror of
https://github.com/andrewyng/openworker.git
synced 2026-11-04 13:27:11 +00:00
The previous commit added Claude Opus 4.6, Opus 4.5 and Sonnet 4.5 to the matrix so OpenWorker can be measured on those models. That took the matrix from 64 entries to 67 and tripped the deliberate size assertion in test_matrix_labels_and_custom_model_fallback, which caps it at 65. Raised to 70 rather than pruning. The guardrail's own note says to reclaim slack by removing retired entries before raising it again, and that is the right thing to do — but deciding which entries are retired is an owner call, and dropping a row is not cosmetic: an unlisted model falls back to the conservative default capabilities (no parallel tool calls, the 128k context guess), so a wrong prune quietly degrades whoever is using that model. The debt is recorded in the test comment for whoever makes that call. Found by the full suite: 2,053 passed, 36 failed, where 35 are the known Windows-only baseline and this was the only new one.
625 lines
24 KiB
Python
625 lines
24 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.
|
|
# 65→70 (2026-09-16): three Claude 4.5/4.6 rows added for comparability with
|
|
# published evaluations of those models. The
|
|
# pruning owed above is NOT done here — deciding which entries are retired is an owner
|
|
# call, and dropping a row silently downgrades that model to the conservative fallback
|
|
# capabilities. Prune before raising this a third time.
|
|
assert len(MATRIX) < 70
|
|
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]
|