043249e0e1
Phase 3: - provider/openai: Chat Completions for OpenAI + compat endpoints (SSE streaming with by-index tool-call assembly, response_format json_schema, legacy max_tokens option, reasoning_effort) - provider/anthropic: Messages API (tool_use/tool_result, GA structured output via output_config.format, full SSE event parser, 529 transient) - provider/ollama: one native /api/chat client behind the ollama, ollama-cloud, and foreman built-ins (presets; NDJSON streaming tolerant of foreman's buffered single-object responses; object tool arguments; format-schema structured output; think mapping) - media/: capability normalization (sniff, downscale, transcode, byte ladder, ErrUnsupported), wired into the chain executor per target with penalty-free advance past incapable elements - registry: real provider + scheme wiring, WithHTTPClient option, required env-foreman TLS chat round-trip test - ADR-0009 multimodal strategy, ADR-0010 tools/structured mapping; README matrix + CLAUDE.md synced Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2.8 KiB
2.8 KiB
ADR-0010: Tools and structured output — one canonical shape, native mappings
Status: Accepted — 2026-06-10
Context
Tool calling and schema-constrained output exist on every target but with different wire shapes (verified against current docs, June 2026; shapes recorded in each provider's package doc). The canonical API must hide all of it.
Decision
Canonical: Tool{Name, Description, Parameters (JSON Schema), Handler};
Response.ToolCalls[]{ID, Name, Arguments json.RawMessage}; results return
as ToolResultsMessage(ToolResult{ID, Name, Content, IsError}). Structured
output via WithSchema(schema, name). Per-provider mapping:
| Concern | OpenAI(+compat) | Anthropic(+compat) | Ollama/foreman | Google (Phase 4) |
|---|---|---|---|---|
| Tool def | tools[].function{name,description,parameters} |
tools[]{name,description,input_schema} |
tools[].function |
FunctionDeclaration.ParametersJsonSchema |
| Call args | JSON string → RawMessage | tool_use.input object |
arguments object |
FunctionCall.Args map |
| Results | one role:tool msg per result (tool_call_id) |
one user msg, tool_result blocks (is_error native) |
role:tool + tool_name |
FunctionResponse parts |
| IsError | "ERROR: " content prefix |
is_error: true |
"ERROR: " prefix |
response payload field |
| Forced choice | tool_choice string / named object |
{"type":"any"/"tool"/"none"} |
none → drop tools; others best-effort ignored | FunctionCallingConfig modes |
| Structured | response_format json_schema (no strict flag) |
output_config.format json_schema (GA mechanism) |
format: <schema> |
ResponseJsonSchema + JSON MIME |
Cross-cutting decisions:
- Missing call ids are synthesized (
call_<n>) — Ollama and some compat servers omit them; the agent loop needs ids to match results. - Streaming buffers tool-call arguments to completion (ADR-0002):
OpenAI fragments accumulate by index, Anthropic
input_json_deltafragments accumulate per block; consumers only ever see parseable calls. - No strict-mode flag is sent to OpenAI: strict mode imposes schema
constraints (every property required, additionalProperties:false) that
caller-supplied schemas may not satisfy. The
Generate[T]reflector (Phase 5) emits strict-compatible schemas anyway. SchemaNamefeeds providers that need a name (OpenAI; default "response"); others ignore it.- Tool handlers never panic the loop:
Toolbox.Execute/ExecuteToolrecover panics and JSON-encode results (ADR to agent loop, Phase 5).
Consequences
- One test matrix per provider asserts the exact wire JSON both directions; drift is caught by httptest fixtures, not in production.
- Ollama's missing tool_choice means "required" cannot be enforced there — documented in the README matrix rather than emulated.