← Documentation indexdocs/superpowers/plans/2026-10-08-v3-issue-228.md

V3 issue #228: structured provider response contract

Issue: https://github.com/EvanProgramming/OpenKyrozen/issues/228 (R001, R002, R004, R007) Related: https://github.com/EvanProgramming/OpenKyrozen/issues/225 Base: current main 25588796b4ed187dbde84b763042512c0e598405 Branch: Evan/v3-228-provider-contracts

Task 1 — SDK-independent contract (RED → GREEN)

Define ToolCall (nonempty id/name, JSON object arguments), ModelResponse (text, ordered calls, usage, finish reason, explicit metadata), ProviderCapabilities and FinishReason in providers/models.py. Normalize final/tool/length/error/cancelled/ blocked/unknown outcomes. Never infer final from text or an absent finish status. Preserve native IDs; synthesize response-scoped IDs only for transports omitting IDs and identify them in metadata. Reject malformed/non-object/non-JSON arguments. Tests must first fail for lost calls and finish reasons before adding code.

Task 2 — adapter and compatibility migration

Add LLMProvider.chat_response() and get_capabilities(); keep chat() and text streaming. Base chat_response bridges legacy-only providers with UNKNOWN status. Native adapters expose full non-stream responses and chat delegates through the shared checked compatibility conversion. That conversion refuses calls, tool requests, length stops, errors, cancellation and blocked output. Preserve ordinary text behavior and existing accounting exactly once, even for rejected responses.

Implement each current transport: OpenAI chat/Azure, OpenAI Responses, Anthropic, Google/Vertex, Bedrock, Ollama, Perplexity. Do not add tool request serialization. Capabilities represent implemented adapter request features, not guessed endpoint or model capabilities: native tools, strict schemas, parallel/streaming tools and reasoning controls remain false until those paths exist; text streaming reflects the actual override. Fallback uses model-mapped conservative intersections and preserves the responding provider metadata; conversion failure is not a reason to replay a successful structured response through another provider.

Task 3 — shared runtime boundary

Add _get_model_response() to the existing providers/calls boundary and bind it on AgentRuntime. Reuse bounded calls, scoped usage, context and token accounting. _get_llm_response retains its string API through checked conversion. Legacy duck providers remain supported, without treating dynamically invented mock attributes as an explicit structured API. Current stream handling remains text-only; native streaming is explicitly unsupported here and owned by #230. Do not invoke tools.

Task 4 — verification and delivery

Cover all transports with text-only, mixed/tool-only, multiple-call and malformed argument fixtures; validate IDs, usage, finish status, metadata and inheritance. Use real installed SDK response types where available to check fixture shape. Test legacy callers/providers, fallback identity/model mapping/capabilities, lossy conversions, runtime structured receipt, no tool execution and cost-once. Run focused tests on supported Python 3.12/3.13, full make test, make check, lint, docs-check and offline agent/subagent acceptance. Obtain fresh independent review and fix material findings. Sign/verify commits with GPG, create/attach one PR, resolve valid review feedback and wait for final-head CI. Do not merge.

Limits

No paid provider calls, new dependencies, raw SDK objects/credentials in metadata, or changes to native request serialization (#229), registry (#232), structured streaming (#230), or AgentEngine execution (#238). Live interoperability remains unverified. Record this limitation in the PR rather than claiming fixture tests are live acceptance.