Skip to content

feat(agent-model-api): agent turn execution slice (tsk-gkh4mi) - #2175

Closed
jaylfc wants to merge 1 commit into
masterfrom
tsk-gkh4mi-agent-turn-slice
Closed

feat(agent-model-api): agent turn execution slice (tsk-gkh4mi)#2175
jaylfc wants to merge 1 commit into
masterfrom
tsk-gkh4mi-agent-turn-slice

Conversation

@jaylfc

@jaylfc jaylfc commented Jul 27, 2026

Copy link
Copy Markdown
Owner

What

Implements the agent-turn execution slice for POST /v1/chat/completions (tsk-gkh4mi). Replaces the 501 stub with a real one-shot agent turn driven through the opencode host-server seam:

consent key -> agent -> agent's opencode server + LiteLLM virtual key -> one non-streaming turn -> OpenAI ChatCompletion shape.

How

  • Reuses ensure_taos_opencode_server(app_state, model) and drive_turn(...) from the existing taOS agent chat path (taos_agent.py) — no new transport.
  • model param = agent_id (per the consent contract); the host resolves it the same way the chat endpoint does.
  • Collects the final reply from the drive_turn sink; returns OpenAI chat.completion envelope.
  • stream:true returns a single SSE completion chunk so standard clients still work (streaming not in the locked seam for this slice).
  • Transport failure degrades to 502 (_TurnError); internals never leak as a 500 trace.
  • agent_ids scope check (404) is unchanged and doubles as scope enforcement.

Live round-trip

The code compiles and the path mirrors the working chat endpoint. The one remaining step for the required live round-trip is minting a consent key via POST /api/agent-model-keys with agent_ids: [PA-agent-id] — that needs an owner (human) session, which the agent cannot do with its registry JWT. Once a key is minted (Jay or @taOS-dev), I will POST a real /v1/chat/completions call and paste the round-trip into this PR.

Acceptance

  • Non-streaming turn returns assistant content in OpenAI shape.
  • 404 when model not in the key's agent_ids.
  • 502 on opencode/LLM transport failure.
  • No new tables/models; reuses existing runtime.

Tests + doc sweep + CHANGELOG to follow once the live round-trip is pasted.

Replace the 501 stub in POST /v1/chat/completions with a real one-shot
agent turn via the opencode host-server seam (consent key -> agent ->
opencode server + LiteLLM virtual key -> one non-streaming turn ->
OpenAI ChatCompletion shape). Reuses ensure_taos_opencode_server and
drive_turn from the existing taOS agent chat path. Returns 502 on
transport failure; never leaks internals.

LIVE ROUND-TRIP: pending consent-key mint (owner session required) — see PR.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@jaylfc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 012fc076-fa9d-4330-990c-0bd7225c5e47

📥 Commits

Reviewing files that changed from the base of the PR and between d7b4e14 and 3c94d98.

📒 Files selected for processing (1)
  • tinyagentos/routes/agent_model_api.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tsk-gkh4mi-agent-turn-slice

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

👋 Thanks for the PR! This one targets master, which is our
stable branch (it's what live installs track). Please retarget it to
dev — click Edit next to the PR title and change the base
branch dropdown from master to dev. Your commits and any review
carry over, nothing is lost.

See CONTRIBUTING.md for the branch model.

@gitar-bot

gitar-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Implement agent turn execution for POST /v1/chat/completions

✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Replace the /v1/chat/completions 501 stub with a real one-shot agent turn execution.
• Reuse the existing opencode host-server seam to drive a single non-streaming turn.
• Return OpenAI-shaped chat completion responses, with 502 on transport failures.
Diagram

graph TD
  C["OpenAI client"] --> A["POST /v1/chat/completions"] --> K[("AgentModelKeyStore") ] --> S["agent_ids scope check"] --> T["Turn runner (drive_turn)"] --> O["opencode host server"] --> R["OpenAI completion (JSON or SSE)"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Implement true incremental SSE streaming via drive_turn sink
  • ➕ Lower latency (tokens/chunks delivered as they arrive)
  • ➕ Closer compatibility with OpenAI streaming clients and tooling
  • ➕ Avoids buffering the entire final response before sending anything
  • ➖ Requires mapping opencode reply events to OpenAI delta semantics
  • ➖ More edge cases (partial failures, finish_reason timing, [DONE] emission)
  • ➖ Increases surface area for client compatibility bugs
2. Pass full OpenAI messages context instead of only last user message
  • ➕ Closer to OpenAI semantics for multi-turn prompts (system + conversation history)
  • ➕ Reduces surprising behavior when clients rely on prior messages
  • ➖ Depends on how the opencode/agent harness persists or accepts per-turn context
  • ➖ May require new adapter API or explicit session/message injection
  • ➖ Harder to define correct precedence between harness persona/system and request system prompts

Recommendation: The PR’s approach is reasonable for a “one-shot turn slice”: it reuses the existing opencode server lifecycle and turn driver, keeps the consent/scope contract intact, and degrades transport failures to 502 without leaking internals. Follow up with (1) proper streaming (multi-chunk SSE) and (2) a clear policy for how OpenAI messages history maps into the agent/opencode session, since the current implementation only uses the last user message.

Files changed (1) +112 / -6

Enhancement (1) +112 / -6
agent_model_api.pyReplace /v1/chat/completions stub with one-shot agent turn execution +112/-6

Replace /v1/chat/completions stub with one-shot agent turn execution

• Implements the turn execution path for POST /v1/chat/completions by resolving the consent binding, enforcing agent_id scope via the existing 404 behavior, then driving a single opencode-backed agent turn and returning an OpenAI ChatCompletion-shaped response. Adds defensive error handling to return 502 on transport/turn failures and supports 'stream:true' by emitting a single SSE completion chunk followed by [DONE].

tinyagentos/routes/agent_model_api.py

@jaylfc

jaylfc commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

Reviewed. The seam is right, and that is the hard part. You reused ensure_taos_opencode_server + drive_turn rather than reaching for direct LiteLLM, so the agent keeps its memory, tools and identity. That was the locked decision and you honoured it. The _TurnError to 502 degradation and not leaking internals as a 500 are both good calls.

But I am not merging this yet, and the reason is the one thing the card singled out.

The card said, in capitals, that a LIVE ROUND-TRIP pasted in the PR is required and that a unit test alone is not accepted as evidence. This PR has no live round-trip, no tests at all, and no docs or CHANGELOG. The justification offered is:

The code compiles and the path mirrors the working chat endpoint.

That is precisely the claim the card was written to refuse. "Compiles and mirrors a working path" is exactly what every one of tonight's five blocked PRs could have said about itself, and I have spent the evening finding features that were merged in that state and had never once executed. #2048 is the cautionary case: it looked complete, passed 17 checks, and when I actually drove its happy path the minted invite could not be redeemed by anyone. Nobody had ever run it.

I have removed your stated blocker, so there is now nothing between you and the evidence. You said the live round-trip needs a consent key minted through an owner session, which your registry JWT cannot do. Correct, and that was mine to solve. I minted one with Jay's session:

key_id    : 1
agent_ids : ["naira", "mary"]      <- both, so you are not blocked picking the wrong one
scopes    : ["chat"]
rate_cap  : 200

The token is on the Pi at /home/jay/.hermes/agent-model-consent.key, mode 0600. I minted it on the Pi and never printed it, so it has not touched the bus, this PR, or any transcript. Read it from that file. Do not paste the key itself into the PR when you post the round-trip; redact it.

Note the endpoint deployed on the Pi is still the 501 stub, since this branch is not merged. You will need to run your branch to exercise it. You have the live stack, which is why this card is yours.

To merge I need, in this PR:

  1. A live round-trip: the request, and the response showing real assistant content in OpenAI shape. It must be visible that the AGENT answered (memory/tools/identity), not a bare model.
  2. Tests for the three acceptance cases you listed: non-streaming turn returns OpenAI shape, 404 when model is not in the key's agent_ids, 502 on transport failure. Please prove the 404 path actually refuses rather than only testing the happy path.
  3. Doc sweep + CHANGELOG. The module docstring at lines 9-11 still says the turn "is the next slice, pending" and is now false, which is the first thing to fix since it is what sent you looking in the first place.

One code question while you are in there: _run_agent_turn takes only the last user message and discards earlier turns and the system message, with a comment that the harness carries context per-turn. Please confirm that is actually true of the opencode path rather than assumed. If a caller sends a multi-turn messages array, an OpenAI-compatible endpoint silently dropping all but the last one is a correctness bug, and it is the kind that looks fine in a one-shot test.

Ping me when it is up. I am awake and merging tonight.

# agent's opencode server + LiteLLM virtual key -> one turn -> OpenAI shape).
# The requested `model` is the agent_id; the host runs the taOS agent's
# opencode server, so we resolve it the same way the chat endpoint does.
stream = bool(body.get("stream", False))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: Stream flag accepts non-boolean truthy values

bool(body.get("stream", False)) treats any truthy value as streaming, including the JSON string "false". A client sending "stream": "false" would unexpectedly receive an SSE response instead of a JSON completion.

Suggested change
stream = bool(body.get("stream", False))
stream = body.get("stream") is True

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

user_text = ""
for m in reversed(messages):
if isinstance(m, dict) and m.get("role") == "user":
user_text = m.get("content", "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: Empty user message content is rejected as "no message found"

If the last user message has content: "" or an empty content-parts list, not user_text is True and the endpoint raises _TurnError("no user message found in request"). An empty user message is still a user message; the error message is also misleading in that case.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


def _sink(reply: dict) -> None:
if reply.get("kind") == "final":
collected["final"] = reply.get("content", "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: None content in a final reply is treated as no reply

reply.get("content", "") returns None when the final reply explicitly has content: None. The subsequent collected["final"] is None check then raises "agent returned no reply" even though a final reply was received.

Suggested change
collected["final"] = reply.get("content", "")
collected["final"] = reply.get("content") or ""

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (1)

Context used
✅ Compliance rules (platform): 35 rules

Grey Divider


Action required

1. Stale 501 test expectation 🐞 Bug ☼ Reliability
Description
The PR removes the 501 not_implemented stub and now executes a turn, but an existing endpoint test
still asserts that contract-valid calls return 501. Unless the test is updated to mock the new
runtime and assert the new completion behavior, CI will fail.
Code

tinyagentos/routes/agent_model_api.py[R134-155]

+    # --- Turn execution slice (tsk-gkh4mi) ---------------------------------
+    # Consent + scope contract satisfied. Drive ONE non-streaming turn through
+    # the agent's opencode host-server (seam: consent key -> agent -> that
+    # agent's opencode server + LiteLLM virtual key -> one turn -> OpenAI shape).
+    # The requested `model` is the agent_id; the host runs the taOS agent's
+    # opencode server, so we resolve it the same way the chat endpoint does.
+    stream = bool(body.get("stream", False))
+    try:
+        reply_text = await _run_agent_turn(request.app.state, model, messages)
+    except _TurnError as e:
+        return _openai_error(str(e), type="server_error", code="agent_error", status=502)
+    except Exception as e:  # defensive: never leak internals as a 500 trace
+        logger.exception("agent_model_api: turn failed")
+        return _openai_error(
+            "agent turn failed", type="server_error", code="agent_error", status=502
+        )
+
+    if stream:
+        # Streaming not in the locked seam for this slice; return the completed
+        # turn as a single SSE chunk so standard clients still work.
+        return _chat_completion_stream(reply_text, model)
+    return _chat_completion(reply_text, model)
Relevance

⭐⭐⭐ High

Team frequently updates/fixes tests to match behavior and unblock CI (e.g., PR #446 test fixes).

PR-#446

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code path no longer returns 501 for contract-valid requests, but the test suite contains an
explicit assertion for 501/not_implemented on that exact scenario.

tests/test_routes_agent_model_api.py[101-112]
tinyagentos/routes/agent_model_api.py[134-155]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`tests/test_routes_agent_model_api.py` still asserts a 501 not_implemented response for contract-valid requests, but the implementation now runs a turn and returns a completion (or 502 on runtime failure).

## Issue Context
The unit test should not depend on a real opencode binary/process; it should mock `_run_agent_turn()` (or underlying `ensure_taos_opencode_server`/`drive_turn`) and assert the OpenAI envelope returned.

## Fix Focus Areas
- tinyagentos/routes/agent_model_api.py[134-155]
- tests/test_routes_agent_model_api.py[101-112]

Suggested updates:
- Replace the 501 assertion with a 200 assertion and validate `object == "chat.completion"` and the assistant message content.
- Add a test that forces `_run_agent_turn` to raise `_TurnError` and asserts HTTP 502.
- Optionally add a streaming=true test asserting SSE framing and `[DONE]`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Agent id used as model 🐞 Bug ≡ Correctness
Description
_run_agent_turn() passes agent_id into ensure_taos_opencode_server() and
drive_turn(model_id=agent_id), but opencode uses model_id as the provider model identifier in
prompt_async. If agent_id differs from the agent’s configured LLM model (common), opencode will
request a non-existent model and the route will return 502 for otherwise valid requests.
Code

tinyagentos/routes/agent_model_api.py[R186-202]

+    server = await ensure_taos_opencode_server(app_state, agent_id)
+    collected: dict = {"final": None}
+
+    def _sink(reply: dict) -> None:
+        if reply.get("kind") == "final":
+            collected["final"] = reply.get("content", "")
+        elif reply.get("kind") == "error" and collected["final"] is None:
+            collected["_error"] = reply.get("error", "agent turn failed")
+
+    await drive_turn(
+        user_text,
+        trace_id=None,
+        sink=_sink,
+        base_url=server.base_url,
+        model_id=agent_id,
+        model_provider_id="litellm",
+        server_password=getattr(app_state, "taos_opencode_password", None),
Relevance

⭐⭐ Medium

No clear historical decision on agent_id vs model_id for opencode; only general model-id correctness
fixes (PR #297).

PR-#297

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The current implementation forwards agent_id as model_id to opencode, but opencode’s adapter
builds prompt_async with modelID=self._cfg.model_id and drive_turn explicitly documents
model_id as the opencode model id (e.g. an LLM model like gpt-4o). The repo also shows agents have
a separate configured LLM model field (agent_dict['model']).

tinyagentos/routes/agent_model_api.py[162-203]
tinyagentos/opencode_runtime.py[339-372]
tinyagentos/adapters/opencode_adapter.py[314-340]
tinyagentos/routes/openclaw.py[109-131]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_run_agent_turn()` currently treats the OpenAI `model` parameter (agent id per consent contract) as the opencode/LiteLLM `model_id`. In opencode, `model_id` is the *LLM model identifier* placed into the `prompt_async` body, so passing an agent id will make prompts fail.

## Issue Context
Agents have a configured LLM model (e.g. `agent_dict["model"]`) that should be used for opencode’s `modelID`.

## Fix Focus Areas
- Resolve the agent dict from `app_state.config.agents` using `agent_id` and extract its configured LLM model id.
- Call `ensure_taos_opencode_server(app_state, llm_model_id)` and `drive_turn(..., model_id=llm_model_id, ...)`.
- If the agent cannot be resolved or has no configured model, return an OpenAI-shaped 400 (invalid_request_error) rather than attempting the turn.

### References
- tinyagentos/routes/agent_model_api.py[162-203]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Client error becomes 502 🐞 Bug ≡ Correctness
Description
When no usable user message text is found, _run_agent_turn() raises _TurnError("no user message
found in request"), and chat_completions maps all _TurnError to HTTP 502 server_error. This
misclassifies a client request validation failure as an upstream/transport failure.
Code

tinyagentos/routes/agent_model_api.py[R140-144]

+    stream = bool(body.get("stream", False))
+    try:
+        reply_text = await _run_agent_turn(request.app.state, model, messages)
+    except _TurnError as e:
+        return _openai_error(str(e), type="server_error", code="agent_error", status=502)
Relevance

⭐⭐⭐ High

Repo tends to treat request-shape/validation issues as 400-level, not 5xx (accepted validation
guards in PR #260).

PR-#260

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The handler maps _TurnError to 502, while _run_agent_turn raises _TurnError for a purely
request-shape problem (“no user message found”), which is not a transport failure.

tinyagentos/routes/agent_model_api.py[140-149]
tinyagentos/routes/agent_model_api.py[172-185]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Missing/empty user input is currently surfaced as a 502 server_error because `_TurnError` is always mapped to 502.

## Issue Context
The route already has `_bad_request(...)` for OpenAI-shaped 400s, but the “no user message” case is only detected inside `_run_agent_turn()`.

## Fix Focus Areas
- tinyagentos/routes/agent_model_api.py[140-149]
- tinyagentos/routes/agent_model_api.py[172-185]

Suggested approach:
- Validate that there is at least one `{"role":"user"}` message with non-empty text before calling `_run_agent_turn()` and return `_bad_request(...)`.
- Alternatively, introduce a separate exception type (e.g. `_TurnBadRequest`) mapped to 400, keeping `_TurnError` mapped to 502.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. _chat_completion returns raw dict 📜 Skill insight ✧ Quality
Description
The new ChatCompletion response builders return untyped dict payloads via JSONResponse/manual SSE
JSON instead of using declared Pydantic request/response models, reducing validation and schema
guarantees. This violates the requirement that route request/response payloads use Pydantic models.
Code

tinyagentos/routes/agent_model_api.py[R211-235]

+def _chat_completion(content: str, model: str) -> JSONResponse:
+    """OpenAI ChatCompletion (non-streaming) envelope."""
+    return JSONResponse({
+        "id": f"chatcmpl-{_short_id()}",
+        "object": "chat.completion",
+        "created": int(time.time()),
+        "model": model,
+        "choices": [{
+            "index": 0,
+            "message": {"role": "assistant", "content": content},
+            "finish_reason": "stop",
+        }],
+        "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
+    })
+
+
+def _chat_completion_stream(content: str, model: str):
+    """OpenAI SSE stream envelope (single completion chunk)."""
+    from fastapi.responses import StreamingResponse
+
+    def _gen():
+        yield f"data: {json.dumps({'id': f'chatcmpl-{_short_id()}', 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': model, 'choices': [{'index': 0, 'delta': {'role': 'assistant', 'content': content}, 'finish_reason': 'stop'}]})}\n\n"
+        yield "data: [DONE]\n\n"
+
+    return StreamingResponse(_gen(), media_type="text/event-stream")
Relevance

⭐ Low

Similar “use Pydantic response_model instead of raw dict” suggestion was rejected by maintainers in
PR #2122.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185155 requires request/response payloads to use Pydantic models rather than raw
dicts. The added _chat_completion() and _chat_completion_stream() functions build OpenAI
response payloads directly from Python dicts/inline json.dumps(...) without any Pydantic model
usage.

tinyagentos/routes/agent_model_api.py[211-235]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`/v1/chat/completions` response construction is done with raw dicts (and streaming builds JSON inline), rather than using Pydantic models / `response_model`, which violates the route payload modeling requirement.

## Issue Context
Even if the handler keeps manual JSON parsing to preserve OpenAI-shaped errors, the parsed `body` can still be validated by a Pydantic model (catching shape issues deterministically), and responses can be emitted from Pydantic models (or `response_model`) to ensure schema consistency.

## Fix Focus Areas
- tinyagentos/routes/agent_model_api.py[134-155]
- tinyagentos/routes/agent_model_api.py[211-235]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +186 to +202
server = await ensure_taos_opencode_server(app_state, agent_id)
collected: dict = {"final": None}

def _sink(reply: dict) -> None:
if reply.get("kind") == "final":
collected["final"] = reply.get("content", "")
elif reply.get("kind") == "error" and collected["final"] is None:
collected["_error"] = reply.get("error", "agent turn failed")

await drive_turn(
user_text,
trace_id=None,
sink=_sink,
base_url=server.base_url,
model_id=agent_id,
model_provider_id="litellm",
server_password=getattr(app_state, "taos_opencode_password", None),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Agent id used as model 🐞 Bug ≡ Correctness

_run_agent_turn() passes agent_id into ensure_taos_opencode_server() and
drive_turn(model_id=agent_id), but opencode uses model_id as the provider model identifier in
prompt_async. If agent_id differs from the agent’s configured LLM model (common), opencode will
request a non-existent model and the route will return 502 for otherwise valid requests.
Agent Prompt
## Issue description
`_run_agent_turn()` currently treats the OpenAI `model` parameter (agent id per consent contract) as the opencode/LiteLLM `model_id`. In opencode, `model_id` is the *LLM model identifier* placed into the `prompt_async` body, so passing an agent id will make prompts fail.

## Issue Context
Agents have a configured LLM model (e.g. `agent_dict["model"]`) that should be used for opencode’s `modelID`.

## Fix Focus Areas
- Resolve the agent dict from `app_state.config.agents` using `agent_id` and extract its configured LLM model id.
- Call `ensure_taos_opencode_server(app_state, llm_model_id)` and `drive_turn(..., model_id=llm_model_id, ...)`.
- If the agent cannot be resolved or has no configured model, return an OpenAI-shaped 400 (invalid_request_error) rather than attempting the turn.

### References
- tinyagentos/routes/agent_model_api.py[162-203]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +140 to +144
stream = bool(body.get("stream", False))
try:
reply_text = await _run_agent_turn(request.app.state, model, messages)
except _TurnError as e:
return _openai_error(str(e), type="server_error", code="agent_error", status=502)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Client error becomes 502 🐞 Bug ≡ Correctness

When no usable user message text is found, _run_agent_turn() raises _TurnError("no user message
found in request"), and chat_completions maps all _TurnError to HTTP 502 server_error. This
misclassifies a client request validation failure as an upstream/transport failure.
Agent Prompt
## Issue description
Missing/empty user input is currently surfaced as a 502 server_error because `_TurnError` is always mapped to 502.

## Issue Context
The route already has `_bad_request(...)` for OpenAI-shaped 400s, but the “no user message” case is only detected inside `_run_agent_turn()`.

## Fix Focus Areas
- tinyagentos/routes/agent_model_api.py[140-149]
- tinyagentos/routes/agent_model_api.py[172-185]

Suggested approach:
- Validate that there is at least one `{"role":"user"}` message with non-empty text before calling `_run_agent_turn()` and return `_bad_request(...)`.
- Alternatively, introduce a separate exception type (e.g. `_TurnBadRequest`) mapped to 400, keeping `_TurnError` mapped to 502.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +134 to +155
# --- Turn execution slice (tsk-gkh4mi) ---------------------------------
# Consent + scope contract satisfied. Drive ONE non-streaming turn through
# the agent's opencode host-server (seam: consent key -> agent -> that
# agent's opencode server + LiteLLM virtual key -> one turn -> OpenAI shape).
# The requested `model` is the agent_id; the host runs the taOS agent's
# opencode server, so we resolve it the same way the chat endpoint does.
stream = bool(body.get("stream", False))
try:
reply_text = await _run_agent_turn(request.app.state, model, messages)
except _TurnError as e:
return _openai_error(str(e), type="server_error", code="agent_error", status=502)
except Exception as e: # defensive: never leak internals as a 500 trace
logger.exception("agent_model_api: turn failed")
return _openai_error(
"agent turn failed", type="server_error", code="agent_error", status=502
)

if stream:
# Streaming not in the locked seam for this slice; return the completed
# turn as a single SSE chunk so standard clients still work.
return _chat_completion_stream(reply_text, model)
return _chat_completion(reply_text, model)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Stale 501 test expectation 🐞 Bug ☼ Reliability

The PR removes the 501 not_implemented stub and now executes a turn, but an existing endpoint test
still asserts that contract-valid calls return 501. Unless the test is updated to mock the new
runtime and assert the new completion behavior, CI will fail.
Agent Prompt
## Issue description
`tests/test_routes_agent_model_api.py` still asserts a 501 not_implemented response for contract-valid requests, but the implementation now runs a turn and returns a completion (or 502 on runtime failure).

## Issue Context
The unit test should not depend on a real opencode binary/process; it should mock `_run_agent_turn()` (or underlying `ensure_taos_opencode_server`/`drive_turn`) and assert the OpenAI envelope returned.

## Fix Focus Areas
- tinyagentos/routes/agent_model_api.py[134-155]
- tests/test_routes_agent_model_api.py[101-112]

Suggested updates:
- Replace the 501 assertion with a 200 assertion and validate `object == "chat.completion"` and the assistant message content.
- Add a test that forces `_run_agent_turn` to raise `_TurnError` and asserts HTTP 502.
- Optionally add a streaming=true test asserting SSE framing and `[DONE]`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@kilo-code-bot

kilo-code-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
tinyagentos/routes/agent_model_api.py 140 Stream flag accepts non-boolean truthy values (bool(body.get("stream", False)) treats the JSON string "false" as True)
tinyagentos/routes/agent_model_api.py 177 Empty user message content is rejected as "no message found"
tinyagentos/routes/agent_model_api.py 191 None content in a final reply is treated as no reply
Files Reviewed (1 file)
  • tinyagentos/routes/agent_model_api.py - 3 issues

Note: The module docstring (lines 9–15) and function docstring (lines 89–91) still state that the endpoint returns 501 and that turn execution is "the next slice, pending." Those lines are outside the current diff hunks so they could not receive inline comments, but they are now stale and should be updated.

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 127.1K · Output: 35.5K · Cached: 770.2K

@jaylfc

jaylfc commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

rebasing onto dev (active dev branch); reopening against dev

@jaylfc jaylfc closed this Jul 27, 2026
@jaylfc
jaylfc deleted the tsk-gkh4mi-agent-turn-slice branch July 27, 2026 23:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant