Skip to content

[FEATURE] Let an agent carry its own model provider credential #5190

Open

Description

@dberesford

Problem Statement

A multi-agent project runs every agent and delegate in one veryfront serve process, and all of them call the model with the same credential. The openai/ slot resolves OPENAI_API_KEY from the process environment when a model string is resolved (src/provider/model-registry.ts, registerShared("openai", ...)), so two agents that name openai/<model> cannot present different keys.

The per-agent hook cannot change that. resolveModelTransport can return headers, but the OpenAI request builder merges them first and then sets authorization from the slot's key (src/provider/runtime-loader/provider-request-init.ts, createRequestHeaders: new Headers(extraHeaders) followed by headers.set("authorization", ...)). We confirmed on the wire that a hook returning Authorization: Bearer <agent key> still arrives at the endpoint with the process key, while another header from the same hook arrives.

registerModelProvider(name, factory) is not a route either: the factory must return a complete ModelRuntime, and the built-in OpenAI runtime is not exported, so a project cannot ask for "the same provider, a different credential" without writing its own client.

Why it matters: when an OpenAI-compatible gateway sits in front of the model, the bearer is how the gateway identifies the caller for policy decisions, attribution and per-caller token budgets. With one key per process, the gateway sees one caller for every agent in the application, and per-agent controls are not possible without one process per agent.

Proposed Solution

Either of these would do; the first is the smaller change.

  1. Let ResolvedModelTransport carry a credential for the call, for example credential?: string, threaded into LLMProviderConfig.credential for the provider created for that call. An agent would then set resolveModelTransport: () => ({ credential: process.env.GATEWAY_KEY_POLICIES }). Alternatively, treat an authorization header that the hook sets explicitly as authoritative instead of overwriting it.
  2. Export a documented way to create a model runtime from a built-in provider with an explicit credential and baseURL, for example createProviderModel("openai", modelId, { credential, baseURL }) from veryfront/provider, so registerModelProvider can register one named slot per agent.

In both cases the credential should stay out of prompts, tool metadata and logs, as the existing provider credential does.

Component

Agents

Alternatives Considered

  • One process per agent through the agent service runtime. It works but turns a three-agent project into three deployments, and delegation then crosses the network.
  • The mistral/ slot shares the OpenAI-compatible code path with its own MISTRAL_API_KEY and MISTRAL_BASE_URL, so a second agent can be pointed at the same gateway with a second key. That gives at most two identities per process by repurposing a slot named for another vendor, and providerName: "mistral" changes response handling.
  • A header set by the hook. It is overwritten, see above.

Example Use Case

As a platform team running Veryfront applications behind an OpenAI-compatible gateway (Envoy AI Gateway with OPA deciding each call by bearer), I want each agent in a delegates tree to present its own credential, so that the gateway can approve models, attribute usage and enforce token budgets per agent rather than per application, without patching Veryfront or splitting the application into one process per agent.

Verification

Activity

  1. kwakayama commented on Oct 16, 2026

    @kwakayama
    Contributor

    This was generated by AI during triage.

    Agent Brief

    Category: enhancement

    Summary:
    Let an agent's model transport hook supply the provider credential for a call, so agents in one process can present different credentials to the same built-in provider.

    Current behavior:

    • The built-in openai slot in src/provider/model-registry.ts reads its key and base URL from the process environment when a model string resolves, then calls provider.createModel(id, { credential, baseURL, ... }).
    • The public ResolvedModelTransport (src/agent/types.ts) has model, headers, providerOptions and reasoning. It has no credential field.
    • createRequestHeaders in src/provider/runtime-loader/provider-request-init.ts builds headers from extraHeaders and then calls headers.set(apiKeyHeaderName, apiKey). A hook-supplied authorization header is therefore overwritten by the slot key. This matches the report. I did not reproduce it on the wire.
    • The Anthropic header builder already treats its credential as authoritative and deletes the conflicting header, so the behavior is not uniform across providers.
    • The hook can return a model runtime, but the built-in provider runtimes are not exported, so a project cannot build "the same provider with a different credential" without its own client.
    • Support first-class secret-manager integration for model provider credential #3870 is about secret-manager loading, not per-agent credentials. I found no duplicate.

    Desired behavior:

    • A hook can pass a per-call credential, optionally with a base URL, that is used instead of the environment credential for that call only.
    • The credential must not leak into prompts, tool metadata, traces, logs or error messages, and must not persist across calls or agents.
    • An empty or invalid credential fails with a typed VeryfrontError, using the existing credential validation.
    • Behavior with no hook credential is unchanged.
    • Maintainers decide the final shape. The issue offers (1) credential?: string on ResolvedModelTransport threaded into LLMProviderConfig.credential, or (2) an exported createProviderModel(provider, modelId, { credential, baseURL }). Option 1 is the smaller change.

    Key interfaces:

    • ResolvedModelTransport and ModelTransportResolver (agent resolveModelTransport hook)
    • LLMProviderConfig.credential and the built-in provider registry
    • Request header construction for OpenAI-compatible providers
    • Egress-bound outbound fetch used for provider calls

    Acceptance criteria:

    • Two agents using openai/<model> in one process send different bearer credentials, verified with a mocked fetch (withMockFetch)
    • The hook credential is used for that call only, and the environment credential is used when the hook sets none
    • The credential does not appear in logs, traces, errors or tool metadata (add a redaction test)
    • Invalid credential input fails with a typed error
    • Docs and the generated public API reference are updated for the new field
    • Relevant formatting, lint, typecheck, and CI checks pass
    • Existing behavior outside this issue remains unchanged

    Out of scope:

    Verification plan:

    • deno task test:file src/agent/runtime/model-transport.test.ts
    • deno task test:file src/agent/runtime/provider-transport.test.ts
    • A new colocated test for the provider request headers
    • deno fmt --check, deno task lint:ci, deno task typecheck
    • Run deno task docs on the pinned Deno version if the public types change
    • CI jobs in .github/workflows/cicd.yml covering unit tests, lint, typecheck and docs

    Handoff: ready for agent

    Thanks @dberesford. I reviewed the report against the current Veryfront Code implementation and reduced it to the Agent Brief above. The remaining work is bounded by the acceptance criteria and verification plan.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestmediumMedium priority

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions