Skip to content

Add a Portkey key type and explain invalid providers.json saves - #131

Merged
wch merged 6 commits into
mainfrom
portkey-key-type-and-config-errors
Oct 7, 2026
Merged

wch merged 6 commits into
mainfrom
portkey-key-type-and-config-errors

Conversation

@wch

@wch wch commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

This PR makes two independent improvements:

  1. A new keyType setting for the built-in Portkey provider says whether the API key is a Portkey API key ("portkey") or the upstream provider's key ("upstream"), so that Portkey works through a corporate proxy in front of hosted Portkey and through Portkey hybrid or enterprise gateways.
  2. When providers.json is invalid, or a change would make it invalid, updating the file now fails with a typed error that says why, so that hosts can show the user what to fix instead of a generic failure.

Today the base URL decides what the Portkey key means: https://api.portkey.ai means a Portkey key, and any other URL means an upstream provider's key. A user whose company routes hosted Portkey through its own proxy has a Portkey key on a non-Portkey URL. The provider treated that key as an upstream key: it added /v1 to the URL, sent the key in the upstream's auth header, and injected a routing header, so every request failed.

With key type "portkey" on any URL other than hosted Portkey, the provider now matches the request a user would send with curl:

  • It uses the URL exactly as entered (a trailing / is trimmed). Users include /v1 if their gateway needs it.
  • It sends the key only as x-portkey-api-key, with no Authorization or x-api-key header.
  • It injects no x-portkey-provider routing header.
  • It accepts both bare model IDs (claude-sonnet-4-6) and catalog IDs (@anthropic/claude-sonnet-4-6).
  • It discovers models from <base URL>/models. If that request fails, the existing cache and declared-models fallback applies.

When keyType is unset, the key type is inferred from the URL exactly as before, so existing configs behave the same. The key type can also come from the PORTKEY_KEY_TYPE environment variable or an administrator's config, and the resolved catalog reports where it came from so forms can show it read-only.

One shared check owns the rules that combine the base URL, key type, and key presence. The provider runs it on the effective connection before each request, and it's exported so that hosts can run it at save time. It reports a missing base URL, the api.portkey.ai host on anything other than exactly https://api.portkey.ai, key type "upstream" with the hosted URL, and key type "portkey" with no key. It takes only whether a key is present, never the key itself. These rules deliberately live outside the strict schema, so a bad combination can never make providers.json invalid and block every later save.

The invalid-file error distinguishes a file that is already invalid from a change that would make it invalid. For a syntax error it carries the line and column; for a schema error it carries each problem's path and message. Messages never include the values found in the file, so a secret in a custom header can't leak into an error shown to the user.

Changes

  • providers.json accepts providers.portkey.keyType ("portkey" or "upstream"), and the published JSON schema describes it.
  • The resolved Portkey connection includes the key type and its source (user file, environment, or administrator), including the key type a lower layer provides when the user clears theirs.
  • Runtime API-key credentials can carry the Portkey key type. The credential store never persists it; it always comes from config.
  • Exported helpers return the key type a URL implies and check a whole Portkey connection.
  • Updating providers.json throws a typed, secret-safe error when the file is invalid before or after the change. Previously, the caller received an untyped error with no structured detail.
  • Type-mismatch schema messages name the expected type only. Previously, they could include the received value.

Not included

  • Custom type: "portkey" entries don't get keyType; they keep the URL-based behavior. That needs changes to the custom schema, forms, and discovery policy, and is a follow-up.
  • Discovery at <base URL>/models and header-only auth through the customer's proxy haven't been confirmed against the real proxy yet. If they don't hold, the gateway mode needs rework before release.

Breaking changes

Older clients share ~/.posit/ai/providers.json, and their strict schema skips a portkey block that sets keyType. The monorepo's writers therefore write keyType only when it differs from the key type the URL implies, so hosted users keep a config older clients can read.

wch added 6 commits October 6, 2026 18:01
mutateProvidersConfig now rejects with ProvidersConfigInvalidError when the
existing file has a JSONC syntax error or fails schema validation, or when the
mutation result would be invalid. The error carries the file path, a phase
(existing-file or proposed-result), and either the syntax position or Zod
issue paths and messages, so hosts can tell users why a save failed without
forwarding raw exceptions that might contain credentials.
Hosts carry the diagnostic across browser boundaries, so the detail, phase,
and issue shapes belong in the browser-safe entry rather than only beside
the node-only error class.
providers.portkey.keyType ("portkey" | "upstream", env PORTKEY_KEY_TYPE)
says what the stored API key is, instead of inferring it from whether the
URL is api.portkey.ai. Absent keeps today's URL inference.

- The schema declares only the enum. Cross-field rules live in the pure
  checkPortkeyConnection({ baseUrl, keyType, apiKeyPresent }), so a bad
  combination never makes providers.json invalid.
- inferredPortkeyKeyType(baseUrl) lets writers omit a keyType equal to what
  the URL implies, so older strict readers keep the block.
- The resolved connection carries keyType, with per-field provenance for
  keyType and baseUrl so forms can pin env- and admin-owned fields.
- ApiKeyCredentials gains a runtime-only portkey.keyType, never persisted;
  the shape guard pins its vocabulary to ai-config's.
With keyType "portkey" on a URL other than api.portkey.ai, the Portkey
provider now treats the gateway as one that takes Portkey keys (a corporate
proxy in front of hosted Portkey, or a Portkey hybrid gateway):

- sends only x-portkey-api-key; delegates get an empty native key so
  Authorization and x-api-key are stripped
- uses the base URL verbatim, without appending /v1
- injects no x-portkey-provider routing header
- accepts both bare and @slug/model ids
- discovers models from <url>/models with Portkey auth

Validation now goes through ai-config's checkPortkeyConnection, and the
chat same-gateway check uses a mode-aware samePortkeyGateway so the raw
provider URL the catalog passes per request still matches. Configs without
a key type behave exactly as before.
Zod 4's invalid_type message names the received value's type, and for an
object without Object.prototype (jsonc parses into those) that is the
file-controlled constructor.name. A secret placed there could reach the
UI and logs through the typed invalid-config error.

schemaIssues now rebuilds invalid_type messages from the expected type
alone. Other built-in messages name only schema values or key names.
connectionProvenance.portkey.keyTypeAfterUserClear is the key type the
source stack yields with the user layer removed (an admin default or a
pin). Writers need it to tell whether omitting the user's keyType keeps
its meaning, and the winning source alone hides it once the user file
sets a value.

Also:
- export isPinnedConnectionFieldSource, the one "environment or
  enforced" predicate for pinned connection fields
- collapse the Portkey gateway URL helpers into one mode-aware function
  and stop exporting samePortkeyGateway
- test that gateway mode refuses a per-model base URL for another host,
  including the same URL with /v1 appended
- name the invalid PORTKEY_KEY_TYPE test for what it does: the whole
  env source is dropped and reported
- update comments and the hosted-mode model id error that still said the
  URL alone decides the key type
@wch
wch merged commit 73c69d0 into main Oct 7, 2026
4 checks passed
@wch
wch deleted the portkey-key-type-and-config-errors branch October 7, 2026 19:19
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