Skip to content

feat: multi-protocol model provider (Anthropic + OpenAI-compat) — support LiteLLM and RITS from main - #159

Merged
pdettori merged 4 commits into
mainfrom
feat/multi-protocol-model-provider-157
Aug 24, 2026
Merged

feat: multi-protocol model provider (Anthropic + OpenAI-compat) — support LiteLLM and RITS from main#159
pdettori merged 4 commits into
mainfrom
feat/multi-protocol-model-provider-157

Conversation

@cwiklik

@cwiklik cwiklik commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Generalize the harness model wiring so a leaf/turn can be driven against any of the common LLM wire protocols — Anthropic Messages, OpenAI Chat Completions, (OpenAI Responses, deferred) — selected per model via SH_MODEL_API, delegating to Pi's existing providers. Unblocks OpenAI-compatible endpoints (IBM RITS, vLLM, OpenAI, Azure, most OSS gateways) from main; the Anthropic + LiteLLM paths are unchanged.

Closes #157. Design: docs/specs/2026-08-20-multi-protocol-model-provider-design.md.

What changed

  • requireModel dispatches on SH_MODEL_API (anthropic default | openai-completions | openai-responses stub). New synthesizeOpenAICompletionsModel builds a Model<"openai-completions"> with baseUrl + headers, delegating to Pi's openai-completions provider.
  • Config: SH_MODEL_BASE_URL (falls back to ANTHROPIC_BASE_URL/OPENAI_BASE_URL), SH_MODEL_HEADERS (JSON; string values support ${VAR} interpolation so a secret flows in via a secretKeyRef env, no inline literal), SH_MODEL_AUTH (bearer | custom-header | none).
  • applyModelGateway no-ops for non-Anthropic models (they carry their own baseUrl/headers/auth).
  • toolChoiceExtension is protocol-aware: Anthropic object {type:"auto"} vs OpenAI string "auto" (vLLM/RITS reject the object).
  • Docs: OpenAI-compatible how-to added to README-k8s/kind/ocp.

Backward compatible: default (registry, or SH_MODEL_CUSTOM=1 without SH_MODEL_API) is byte-for-byte the Anthropic behavior shipped today.

Verification

  • Unit: 24 tests green (run-turn-model + new tool-choice-extension) — OpenAI synth, base-URL fallback, ${VAR} interpolation, custom-header Bearer strip, validation errors, Anthropic-default regression, object-vs-string tool_choice. tsc --noEmit clean.
  • Live gate (epoc002, RITS-hosted Granite ibm-granite/granite-4.1-8b):
    • SH_MODEL_API=openai-completions + SH_MODEL_HEADERS={"RITS_API_KEY":"${RITS_API_KEY}"} + SH_MODEL_AUTH=custom-header → auth succeeded (secret via secretKeyRef).
    • Log confirms tool_choice="auto" (string form) sent, no vLLM rejection.
    • Tool-requiring /turn ran hostname and returned the real pod name (unguessable) → structured tool call executed, stopReason:stop.

The pi-fork submodule has a pre-existing non-hermetic build issue (models.generated.ts regenerated from a live API drifts against a pinned type); the live image was built as a thin overlay of the two changed source files over the working base, which the harness loads via tsx. Not caused by this PR.

Follow-ups (not in this PR)

  • Retire the /tmp OpenAI wiring fork (superseded by this).
  • Slot a milestone-registry id in docs/specs/README.md (maintainer call).
  • openai-responses synthesizer (deferred; all current targets are openai-completions).

Assisted-By: Claude Code

… design (#157)

Design for generalizing the harness model-synthesis wrapper to dispatch on a
SH_MODEL_API protocol selector (anthropic | openai-completions | openai-responses),
delegating to Pi's existing providers, with configurable base URL / headers / auth.
Unblocks RITS/vLLM/OpenAI-compatible endpoints from main; Anthropic + LiteLLM paths
unchanged (default). Status: Accepted.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: cwiklik <cwiklikj@gmail.com>
…#157)

Generalize requireModel to select the wire protocol via SH_MODEL_API
(anthropic | openai-completions | openai-responses), delegating to Pi's
existing providers. Adds synthesizeOpenAICompletionsModel (baseUrl +
SH_MODEL_HEADERS + SH_MODEL_AUTH bearer/custom-header/none) so RITS/vLLM/
OpenAI-compatible endpoints work from main; openai-responses is a defined-but-
deferred stub. applyModelGateway now no-ops for non-anthropic models (they
carry their own baseUrl/headers/auth). toolChoiceExtension emits the object
form for Anthropic and the string "auto" for OpenAI.

Default (registry, or SH_MODEL_CUSTOM=1 without SH_MODEL_API) is unchanged;
the anthropic base URL now also accepts SH_MODEL_BASE_URL as a fallback.

Tests: run-turn-model (openai synth, base-url fallback, custom-header strip,
validation errors, anthropic-default regression) + new tool-choice-extension
(object vs string, no-override, inert-without-custom). 23 pass; tsc --noEmit
clean. Full suite: only pre-existing Redis-ECONNREFUSED integration tests fail.

Implements docs/specs/2026-08-20-multi-protocol-model-provider-design.md.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: cwiklik <cwiklikj@gmail.com>
Header string values support ${VAR} env interpolation so a secret header value
(e.g. RITS's RITS_API_KEY) can be supplied via a secretKeyRef env var —
{"RITS_API_KEY":"${RITS_API_KEY}"} — instead of an inline literal in the
manifest. Unset ${VAR} → empty string. Needed for the OpenAI-compat custom-header
auth path (RITS) to consume the cluster secret cleanly.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: cwiklik <cwiklikj@gmail.com>
Add the OpenAI-compatible path (SH_MODEL_API=openai-completions + SH_MODEL_BASE_URL/
OPENAI_BASE_URL + custom-header auth via SH_MODEL_HEADERS ${VAR} indirection) to the
'Choosing the model' how-to in README-k8s/kind/ocp, alongside the existing Anthropic-
compatible instructions. Uses the RITS/Granite example validated on-cluster.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: cwiklik <cwiklikj@gmail.com>

@pdettori pdettori left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Summary

Clean, well-scoped generalization: dispatch on SH_MODEL_API in requireModel, a new synthesizeOpenAICompletionsModel, a protocol-aware toolChoiceExtension (object vs string auto), and a correct applyModelGateway no-op guard for non-Anthropic APIs. The Anthropic default path is preserved byte-for-byte, ${VAR} header interpolation keeps secrets out of manifests, and the 24 unit tests cover synthesis, base-URL fallback, custom-header strip, interpolation, and validation errors. Docs (k8s/kind/ocp READMEs + design spec) match the code.

Areas reviewed: TypeScript (run-turn, tool-choice-extension), tests, docs. Commits: 4, all DCO signed-off. Agent/IDE config: none.

CI status — 2 failing, both pre-existing and unrelated to this PR

  • check dies at Build pi-fork (type declarations) with the same supportsReasoningEffort / OpenAIResponsesCompat type error that also fails on main (run 32408167817). This PR touches neither pi-fork nor the generated types.
  • trivy-scan flags CRITICAL/HIGH vulns in pnpm node_modules; this PR changes no package.json/lockfile.

⚠️ Verification caveat: because check fails at the pi-fork build step, the Typecheck and Test steps never run — so CI never actually executed the 24 tests or tsc --noEmit. The "24 green / tsc clean" evidence is locally-verified only. Worth merging with eyes open until the pre-existing pi-fork build issue is resolved.

Approving on the code's merit — the two inline notes are non-blocking suggestions.

Assisted-By: Claude Code

Comment thread harness/src/run-turn.ts
}
const contextWindow = Number(env.SH_MODEL_CONTEXT_WINDOW) || 131072;
const maxTokens = Number(env.SH_MODEL_MAX_TOKENS) || 8192;
const auth = env.SH_MODEL_AUTH ?? "bearer";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestionSH_MODEL_AUTH isn't validated. An unrecognized value (a typo like custom_header, or Bearer with a capital) silently falls through to bearer behavior — the Authorization strip and placeholder-key seed on lines 147–152 never fire, producing a confusing downstream auth failure rather than a clear error. SH_MODEL_API errors on unknown values (see requireModel); consider validating this enum the same way for symmetry.

Comment thread harness/src/run-turn.ts
// SDK's default Authorization Bearer so an unknown/empty Bearer isn't sent. pi's openai client
// still requires a non-empty api key even when the Bearer is unused, so seed a placeholder.
headers.Authorization = null;
if (!env.OPENAI_API_KEY) process.env.OPENAI_API_KEY = "unused";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion — this reads env.OPENAI_API_KEY but mutates the global process.env.OPENAI_API_KEY. When a caller passes a custom env (not process.env), the check and the write target different objects, so a caller supplying an env without the key pollutes the real process.env with "unused". The tests already work around this by passing OPENAI_API_KEY: "present". It's necessary because pi resolves the key from process.env at request time — worth a one-line comment noting the intentional read-env / write-global split (or seeding onto the passed env where feasible).

@pdettori
pdettori merged commit 41d0d90 into main Aug 24, 2026
8 of 10 checks passed
@pdettori
pdettori deleted the feat/multi-protocol-model-provider-157 branch August 24, 2026 19:41
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.

feat: multi-protocol model provider (Anthropic + OpenAI-compat) — support LiteLLM and RITS from main

2 participants