Tasks: Support connecting to any OpenAI-compatible API
The last spec artifact (requirements → design → testing plan → tasks). A DAG of implementation tasks derived from the design and testing plan. It has no approval gate of its own (issue-281).
Eight tasks, delivered on the work item's one pull request, #22, in this repository. No other repository is touched. Every task names the requirement it satisfies, the testing-plan row that proves it, and the test that goes red first.
Conventions for every task:
- pyright strict, zero errors, no
Any; ruff clean. - Integration tests carry the Gherkin docstring with
Requirement: docs/specs/issue-19/requirements.md#R<n>(the repository's convention). - Tests run with
OPENAI_API_KEYunset unless the case is about a key. - The commit message records the test command and its red→green transition.
Task list
- [x] 1. Config:
base_url,api,context_window_tokens, optional keyOpenAIConfig:api_key: SecretStr | Nonedeclared beforebase_url: HttpUrl | None;api: Literal["responses", "chat_completions"];context_window_tokens: PositiveInt | None.base_urlfield validator: reject user information; rejecthttpto a non-loopback host whenapi_keyis set (loopback =localhostor a literal loopback IP, no DNS).model_validator(mode="after"): neither key norbase_url→ error (programmatic backstop).load_settings:_openai_base_url_set(data); skip the missing-OPENAI_API_KEYerror when it is set.secret_valuesinservice/runtime.py: addopenai.api_keyonly when set.- Depends on: none
- Requirements: R1.4, R2.1, R2.2, R3.1, R4.3; abuse cases 2, 3
- Test: T1 + T8 — new
tests/unit/test_config_openai.py(every R1.4 / R2 / R3.1 / R4.3 case and abuse cases 2–3 from the trace table); T10 — existingtests/unit/test_config.pyunedited (red→green)
- [x] 2. Adapter: client construction, model info, keyless headers
OpenAILLM.__init__gainsbase_url,api,context_window_tokens;api_keymay beNone._client(...)per the design's table: explicitbase_url(defaultDEFAULT_BASE_URL),"no-key"placeholder + per-requestAuthorization: omit,OpenAI-Organization/OpenAI-Projectomitted andfollow_redirects=Falsewhenbase_urlis set.LLMModelInfo.endpointand.api(defaulted);context_window_tokensoverride.- Responses path unchanged apart from passing
extra_headerswhen keyless. - Depends on: none
- Requirements: R1.1, R1.2, R1.3, R1.5, R2.2, R2.3, R4.1, R4.2; abuse case 1
- Test: T1 + T8 —
tests/unit/models/test_openai_endpoint.py(URL reached, headers,OPENAI_BASE_URLignored, org/project suppressed, 307 refused, model info); T10 — existingtests/unit/models/test_openai.pyunedited (red→green)
- [x] 3. Parse-failure wrapping for both APIs
- Move
parse_responseinside the translatingtryininvoke; catchValueError,KeyError,TypeError,AttributeError,IndexErrorfrom parsing ininvokeandstream→ProviderError(status=200, detail="unparseable responses response"). - Depends on: 2
- Requirements: R5.3; abuse case 4
- Test: T1 + T8 — malformed Responses bodies (invalid tool-call JSON, wrong types) on invoke and stream →
ProviderError(red: todayjson.JSONDecodeErrorescapes)
- Move
- [x] 4. Chat Completions module and dispatch
- New
tiny_harness/harness/models/openai_chat.py:build_messages,build_chat_params,parse_chat,ChatStreamAssembler, the shared finish rule. - Recorded fixtures under
tests/fixtures/openai/chat/(text, tool calls, refusal,content_filter,length, no-usage, no-id, malformed, and an SSE stream with fragmented tool-call arguments and a usage chunk). OpenAILLM.invoke/streamdispatch onapi; parse failures wrapped as in task 3 withdetail="unparseable chat_completions response".- The new names stay public in
openai_chatitself; the package re-exports onlyOpenAILLM, as it does for the Responses helpers. - Depends on: 2, 3
- Requirements: R3.2, R3.3, R3.4, R5.1–R5.3
- Test: T1 —
tests/unit/models/test_openai_chat.py(every R3.3 / R3.4 / R5 case in the trace table) (red→green)
- New
- [x] 5. Runtime wiring and observability
build_runtimepasses the four settings toOpenAILLMand logsmodel endpoint <scheme>://<host[:port]> api=<api> model=<model>once at INFO.LLMInvokedPre.model: LLMModelInfo | None = None, set byInProcessOperations; the o11y hook addsserver.address,server.port,tiny_harness.llm.apito the chat span.- Depends on: 1, 2
- Requirements: R1.1, R4.1, NFR observability; abuse case 5
- Test: T1 + T8 — runtime unit test: the log line's shape (no path, query or key); o11y hook unit test: span attributes; key absent from log, span and a translated error (red→green)
- [x] 6. Contract snapshots
- Regenerate the four snapshots; confirm the diff is additive only.
- Depends on: 1, 2, 4, 5
- Requirements: NFR backwards compatibility
- Test: T6 —
uv run pytest tests/contract(red until regenerated)
- [x] 7. Integration scenarios against a scripted endpoint
tests/integration/compat/: astarlette+uvicornfake OpenAI-compatible server on an OS-chosen loopback port serving/v1/responsesand/v1/chat/completionsfrom scripted recorded bodies, recording each request's headers;running_harnesswith embedded Temporal and[openai] base_urlpointing at it, no key.- The three T2 scenarios from the trace table, each with its Gherkin docstring.
- Depends on: 4, 5
- Requirements: R1.1, R2.2, R3.2, R3.3, R5.1, R6.2
- Test: T2 —
uv run pytest tests/integration/compat; T8 offline — the same insideunshare -rn(red→green)
- [x] 8. Ollama demo configuration, e2e test and documentation
examples/demo/config.ollama.tomlper the design;tests/e2e/test_demo_ollama.py(e2emarker; skips with a reason when127.0.0.1:11434does not answer or the model is not pulled;TINY_HARNESS_OLLAMA_MODELoverrides the model) using the existingtests/e2e/demo.pydriver.- README "Run the demo" and
docs/guide/getting-started.md: an "Offline with Ollama" section. Configuration reference /docs/capabilities/configuration.mdandmodels.md: the new keys,OPENAI_BASE_URLignored, the key rule; history rows. docs/decisions/decision-006.md+ its row indecisions.md: the endpoint is configuration-only and a custom endpoint reusesOPENAI_API_KEY.- Depends on: 5
- Requirements: R6.1, R6.2, R6.3
- Test: T4 —
uv run pytest tests/e2e -k ollama -s(skips without Ollama: the skip reason is asserted in a unit test of the guard); T12 —bun run --cwd docs docs:build(red→green)
Dependency graph (DAG)
Tasks 1 and 2 have no dependency on each other; the session runs them in order 1, 2, 3, 4, 5, 6, 7, 8.
Checkpoints
- After every task:
uv run pytest tests/unitanduv run pyright, then commit with the red→green line, tick the box, compact. - After task 6:
uv run pytest tests/contract tests/security. - After task 7:
uv run pytest tests/integration. - After task 8:
uv run pre-commit run --all-files— then the verification node executestesting-plan.md, and the review phases run the self/critic rounds and the security review gate (recorded inevidence/security-review.md).
Review comments
Appended by the-loop's
record-feedbackhook when a human gate approves with comments (issue-109). Append-only and attributed: an approval never silently discards a reviewer's suggestions, and the feedback travels with the document it concerns rather than living in a side-channel tracker.