Files
majordomo/docs/adr/0010-tools-structured-output-mapping.md
T
steve 043249e0e1 feat: OpenAI, Anthropic, and native-Ollama providers + media pipeline
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>
2026-06-10 12:58:08 +02:00

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_delta fragments 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.
  • SchemaName feeds providers that need a name (OpenAI; default "response"); others ignore it.
  • Tool handlers never panic the loop: Toolbox.Execute/ExecuteTool recover 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.