Skip to content

docs(agent-workflows): plan for custom providers + model auth in Pi#5013

Draft
mmabrouk wants to merge 2 commits into
mainfrom
custom-providers-in-pi-plan
Draft

docs(agent-workflows): plan for custom providers + model auth in Pi#5013
mmabrouk wants to merge 2 commits into
mainfrom
custom-providers-in-pi-plan

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Jul 1, 2026

Copy link
Copy Markdown
Member

Goal

Let a user run an OpenAI-compatible model through a Pi agent. The user stores a connection (a base URL, a key, and one or more model ids), picks that connection and a model in the playground, and the run reaches the model. The connection's slug is its identity end to end: the vault record, the resolver, the wire, and the Pi config the runner writes all agree on the same slug, so two connections that speak the same protocol stay distinct.

This is a docs-only plan-feature workspace at docs/design/agent-workflows/projects/custom-providers-in-pi/. No code changes.

What already landed since the plan was written

The plan was written on 2026-07-02. Three of its original slices have since landed in the codebase, so this revision marks them done and re-sequences the rest:

  • The three provider-to-env maps collapsed into one canonical PROVIDER_ENV_VARS (capabilities.py:111-125), with together_ai -> TOGETHER_API_KEY and minimax. There is a parity regression test.
  • A custom connection whose kind is a known provider family resolves to deployment="direct" (connections.py:330-335) and passes Pi's ["direct"] gate.
  • The runner fails loud on an unsettable model: allowedModels reads c.value ?? c.id (model.ts:72), applyModel raises a typed ModelNotSettableError, and AGENTA_AGENT_MODEL_STRICT is wired for every harness and defaults to true (sandbox_agent.ts:374). This shipped strict-by-default directly, so the plan's staged "flip later" step is moot.

What remains, and the revised design

A connection whose kind is not a known family (an Ollama gateway, an in-house proxy, any named OpenAI-compatible endpoint) still fails before it runs, and the runner writes no Pi models.json. Six review comments on the design reshaped how the plan closes that path. All six were accepted:

  • Keep deployment="custom" for a record whose kind is not a known family, default a provider-less record to the OpenAI-compatible family after the trusted vault record resolves, and let Pi allow the (custom, openai-compatible) pair. The known-family direct normalization that landed stays.
  • Key models.json by the connection slug, not resolved_connection.provider, because two custom connections can resolve to the same family and a provider-less one has no provider id.
  • Speak openai-completions only in v1. Do not infer anthropic-messages from a provider label. A pure builder exposes a protocol discriminator for later; v1 rejects unsupported protocols loudly.
  • Build an isolated managed Pi directory for a managed run, carrying only non-credential settings plus models.json with a "$ENV" apiKey reference. The raw key stays in the daemon or sandbox environment. This also fixes the existing local path, which copies the operator auth.json today while Daytona already refuses.
  • Keep the bare model id on the wire; the runner derives the exact <slug>/<model> id and sets it directly, bypassing the suffix fallback that can pick the wrong provider.
  • Cut the picker expansion (it moves to the model-config sibling, Part 3). The UI change here is one rename: "Custom provider" to "OpenAI-compatible endpoint".

One new wire field

The first plan's non-goal "no new /run wire field" is dropped. Keying models.json by slug needs the slug on the wire. It is replaced by: exactly one new field, slug on ResolvedConnection.to_wire(). Everything else the runner still derives from resolved_connection and secrets.

The revised slices

  1. Service: add slug to ResolvedConnection and its wire form; a provider-less or unknown-kind custom connection resolves to the OpenAI-compatible family with deployment="custom"; Pi's capability table allows custom.
  2. Runner: a model-config builder writes models.json keyed by providers[<slug>] (dialect openai-completions, "$ENV" apiKey, the one selected model) and returns the exact <slug>/<model> id; the runner creates an isolated managed Pi dir on the local and Daytona paths and sets that exact id.
  3. Frontend: rename the type label at three locations.

Each slice is independently shippable and carries tests (resolver unit tests and a wire golden for slice 1; builder, managed-dir isolation, Daytona parity, and a live acceptance run for slice 2; a render check for slice 3). The Pi startup-banner leak stays a separate lane.

Files

Six files in the plan workspace: README.md, context.md, research.md, design.md, plan.md, status.md. research.md gains a dated 2026-07-14 re-verification section. design.md keeps the design-interfaces role tables as its spine.

https://claude.ai/code/session_01HCMtsTWnCdh8fPEzGrda6C

@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jul 14, 2026 7:35pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ecba93e1-e080-437b-aee1-1e5d5cbddcb3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch custom-providers-in-pi-plan

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

New plan-feature workspace for making provider + model auth work end to end on
the Pi harness, custom providers included. Builds on provider-model-auth (BUILT)
and model-config (DESIGNED). Diagnoses five gaps and sequences five slices.

Claude-Session: https://claude.ai/code/session_01HCMtsTWnCdh8fPEzGrda6C
Normalize `deployment` to the closed set of real access surfaces, and let `endpoint` carry the
"customness":

- A `custom_provider` whose kind is in `_PROVIDER_ENV_VARS` (a known single-key direct provider:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 The AI agent says:

This still leaves the original pre-resolve failure in place for an arbitrary custom slug such as ollama or my-gateway: it only works when kind is already present in _PROVIDER_ENV_VARS. The requirement is a named OpenAI-compatible connection, not a known built-in provider disguised as custom. Keep custom as the resolved deployment, defer the provider decision until after the trusted vault record is resolved, and allow the Pi (custom, OpenAI-compatible) pair explicitly. A provider-less custom record can default to the OpenAI-compatible family in the service without changing the stored schema.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Accepted. The revised design.md Section 1 keeps deployment="custom" for a record whose kind is not a known family, defaults a provider-less record to the OpenAI-compatible family after the trusted vault record resolves (no stored-schema change), and adds the (custom, openai-compatible) pair to Pi's capability row so a named endpoint reaches the runner instead of dying at the pre-resolve or post-resolve gate. The known-family normalization to direct already landed (connections.py:330-335) and stays.


| Field | Role | Owner / lifecycle | Source in our contract |
| --- | --- | --- | --- |
| `providers` | config, extensible map (plural, correct) | service, per run | keyed by `resolved_connection.provider` |

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 The AI agent says:

Key this map by the selected connection slug, not resolved_connection.provider. Two custom connections can both resolve to the OpenAI family, and a provider-less custom connection may not have a provider at all; using the family either collides or cannot produce a Pi provider id. The stable Pi identity should be providers[connection.slug], with the selected model registered under that entry.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Accepted. models.json is now keyed by providers[], not resolved_connection.provider, and the selected model registers under that entry. This adds one field: slug on ResolvedConnection and its to_wire(), classified in design.md Section 3 as identity/metadata owned by the vault record and changing per connection edit. This drops the first plan's "no new wire field" non-goal, replaced by exactly one new wire field.

| --- | --- | --- | --- |
| `providers` | config, extensible map (plural, correct) | service, per run | keyed by `resolved_connection.provider` |
| `providers.<p>.baseUrl` | routing (the endpoint being contacted, nested under the provider) | service, per run | `resolved_connection.endpoint.baseUrl` |
| `providers.<p>.api` | config (the wire dialect) | service, per run | derived from provider (default `openai-completions`; Anthropic to `anthropic-messages`) |

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 The AI agent says:

For this first version, keep the dialect strictly openai-completions. Anthropic Messages has materially different request/response semantics and should not be inferred from a provider label here. A small pure builder can expose a protocol discriminator/union so anthropic-messages can be added later, while v1 rejects unsupported protocols instead of half-supporting them.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Accepted. v1 speaks strictly openai-completions and does not infer anthropic-messages from a provider label. The model-config builder exposes a protocol discriminator so another dialect can be added later, and v1 rejects any unsupported protocol loudly. See design.md Section 2.

daemon env. The real key rides `resolved_connection.env` and the runner injects it into the daemon
env (the same channel `auth.json` uses). So `models.json` on disk carries no secret value. `auth.json`
follows the same rule: `{ "openrouter": { "type": "api_key", "key": "$OPENROUTER_API_KEY" } }`,
merged with the login's copied `auth.json` so OAuth still works.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 The AI agent says:

Do not merge the operator's copied auth.json into a vault-managed custom-provider run. The existing local helper copies personal login state; reusing it unchanged would let a managed run silently authenticate with an operator subscription or the wrong provider. Create an isolated managed Pi directory that carries only non-credential settings/assets plus models.json with an env reference. The raw key remains solely in the daemon/sandbox environment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Accepted. A managed run no longer reuses the operator's copied auth.json. The runner builds an isolated managed Pi directory carrying only non-credential settings plus models.json with a "$ENV" apiKey reference, and the raw key stays solely in the daemon or sandbox environment. This also fixes the existing local path, which copies auth.json today (pi-assets.ts:475-490) while Daytona already refuses (daytona.ts:168-171). See design.md Section 2.


- The base URL rides `resolved_connection.endpoint.baseUrl`. Already on the wire
(`wire_resolved_connection()`, `provider-model-auth` Slice 4).
- The selected model id rides `resolved_connection.model`. Already on the wire.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 The AI agent says:

The wire can keep the bare selected model id, but the runner must derive and set the fully qualified Pi id ${connection.slug}/${model}. Pi may advertise multiple providers with the same model suffix, and the current suffix-resolution fallback can select the first wrong provider. Have the model-config builder return both the file content and exact runtime model id, then pass that exact id to setModel.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Accepted. The wire keeps the bare selected model id, and the model-config builder returns both the file content and the exact runtime id /. The runner passes that exact id to setModel, bypassing the suffix fallback in pickModel (model.ts:49-59) that can select the wrong provider. See design.md Section 2.

grouped options. A vault custom-provider's models appear only if that provider (and its
now-`direct` deployment) is reachable by the selected harness, the same policy filter
`namedConnectionOptions` already applies to the Connection dropdown.
- `VaultConnectionEntry` (`connectionUtils.ts:302-312`) gains `models?: string[]`, so the array

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 The AI agent says:

This schema/picker expansion is outside the agreed scope. The existing UI already creates the custom-provider record and sends its selected connection/model; for now the UI change should be limited to renaming the visible type to OpenAI-compatible. Keep the implementation boundary from the service through the runner and remove the VaultConnectionEntry/model-choice contract changes from this plan.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Accepted. The picker and model-choice contract expansion is cut from this plan and parked in the model-config sibling (Part 3); the VaultConnectionEntry symbol the first plan named does not exist in web/. The only UI change here is renaming the visible type "Custom provider" to "OpenAI-compatible endpoint" at the three locations. See design.md Section 5.

@mmabrouk
mmabrouk force-pushed the custom-providers-in-pi-plan branch from c89be77 to 6eb358c Compare July 14, 2026 19:33
@mmabrouk
mmabrouk changed the base branch from big-agents to main July 16, 2026 10:45
@mmabrouk

Copy link
Copy Markdown
Member Author

Correction: no new wire field is needed

This plan proposed threading a custom-provider connection to Pi by adding a dedicated wire field. That turned out to be unnecessary, and this PR's design text on that point is superseded.

What actually ships: the connection {mode, slug} already rides the existing wire via model_ref threading. The gap was that a named custom connection was not threading its model_ref; that is fixed in sdks/python/agenta/sdk/agents/adapters/harnesses.py (Pi and Agenta harnesses). No new /run wire field and no vault schema change were required.

Where the implementation landed (both retargeted to main, since big-agents is merged):

The plan of record is docs/design/agent-workflows/projects/pi-openai-compatible-models/, committed on #5345.

This PR: the corrected copies of the six custom-providers-in-pi/ docs (lane content plus correction banners) currently sit in the working tree awaiting a clean lane apply; a but apply of this lane no-ops while untracked same-path copies occupy those paths, so the correction commit could not be landed on the lane in this pass. This PR's base was retargeted from the retired big-agents to main.

@mmabrouk

Copy link
Copy Markdown
Member Author

Correction: no new wire field was needed; this plan is superseded.

This plan claimed the change needs "exactly one new wire field, the connection slug on ResolvedConnection". That was wrong. No wire change was needed. The connection {mode, slug} already rides the /run request through model_ref threading in sdks/python/agenta/sdk/agents/adapters/harnesses.py. The earlier verification missed that the named custom connection was not threading its model_ref, so it read the wire as needing a new field. The fix threaded the existing model_ref instead.

The feature shipped under the sibling pi-openai-compatible-models project, now implemented and end-to-end verified across the local and Daytona paths:

The corrected doc text (correction banners on README/context/design/plan/status; research unchanged) sits in the working tree awaiting a clean lane apply — the but apply of this lane silently no-op'd because the corrected copies already exist as untracked same-path files, so the correction was not committed to this lane in this pass. The plan is kept for history; read the sibling project for the shipped design and status.

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.

1 participant