Skip to content

fix(harness-pi): resolve compound provider/id model ids under their scoped provider - #18334

Merged
felixarntz merged 4 commits into
vercel:mainfrom
fdarian:fix-harness-pi-compound-model-ids
Aug 10, 2026
Merged

felixarntz merged 4 commits into
vercel:mainfrom
fdarian:fix-harness-pi-compound-model-ids

Conversation

@fdarian

@fdarian fdarian commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

This PR is fully authored by Opus 5 and Sonnet 5 using Claude Code harness.

Here's a message from Opus regarding this change

Root cause. createPiModelResolver matched catalog entries with flat equality only (packages/harness-pi/src/pi-model-resolver.ts:42-43 before this change): m.id === effectiveId || m.name === effectiveId. That never inspects the separate provider field, so a compound id only ever matched a provider that happens to store the whole compound string as its own flat id, which is what the routing proxies do.

The id is not dropped, it resolves to the wrong provider. ModelRegistry.getAll() returns the full static catalog for every builtin provider with no credential filtering (model-registry.js:18-20 into model-runtime.js:192-194; the auth-filtered list is the separate getAvailable(), which this resolver never calls). vercel-ai-gateway registers its whole model table unconditionally, and that table contains an entry whose flat id is literally "xai/grok-4.3". So for a user with only xai credentials: useGateway is false, the gateway-preference clause short-circuits, and the unrestricted models.find(matches) returns the gateway entry, which builtinProviders() registers ahead of xai. Pi then dispatches through a provider the user never configured and fails with "No API key found for vercel-ai-gateway".

The fix is one new tier, and its position is the whole subtlety. Resolution order is now:

  1. when gateway credentials are present, the vercel-ai-gateway flat match (pre-existing)
  2. the new provider-scoped match: split on the first /, then m.provider === prefix && (m.id === bareId || m.name === bareId)
  3. the unrestricted flat match (pre-existing)

The scoped tier deliberately sits after the gateway preference. Gateway and OpenRouter entries legitimately carry another provider's id verbatim as their own flat id (vercel-ai-gateway has an entry whose id is literally "xai/grok-4.3", alongside the native xai entry with bare id: "grok-4.3"), and when the user has gateway credentials, routing through the gateway is what they asked for. I originally placed the scoped tier first, which regressed exactly that case; the automated review on an earlier commit caught it, and the added tests now pin both directions.

Why ordered tiers rather than one predicate. Folding the scoped check into the existing matches predicate would make a single find() return whichever entry sits earlier in the catalog array, so a proxy entry could beat the real one by array position. Separate tiers make the outcome deterministic. Two tests assert this in both catalog orderings.

Why not delegate to Pi's own resolveCliModel. It is a public export from @earendil-works/pi-coding-agent and does the same slash-split, but adopting it would also replace the harness-specific vercel-ai-gateway preference tier, and it returns a richer result shape (warning/error/thinkingLevel) that the adapter does not consume. This keeps the change inside the existing tier structure.

Scope. Matching logic only. #18155 makes an unresolved model id throw instead of being silently dropped; that is a different failure mode from this one (here the id resolves, just to the wrong provider), and it is left to that PR. #18147 and #18153 share the symptom but have a different root cause again (customEnv registering credentials without corresponding catalog entries).

No reproduction example was added under examples/: the bug is pure catalog-matching logic covered by the unit tests, and reproducing it end to end would require live xai plus gateway credentials just to surface the symptom.

Background

I have pi authenticated with xai on my machine. Passing xai/grok-4.3 to HarnessAgent apparently routed to vercel-ai-gateway, meanwhile xai/grok-4.3 is a valid pi's modelId (<provider-id>/<provider-model-id>). For example pi --model "xai/grok-4.3" -p "hello" is a valid expected syntax for grok-4.3 from xai provider.

Summary

Compound <provider>/<model-id> ids were only matched literally, against a provider's flat id. That happens to work for routing proxies like vercel-ai-gateway, which store the compound string as their own id, but never for a native provider like xai, whose entry is provider: "xai" + id: "grok-4.3". So xai/grok-4.3 silently landed on the gateway entry instead. Added provider-scoped matching so these resolve to the provider the id actually names.

End-to-End Verification

Added unit tests in packages/harness-pi/src/pi-model-resolver.test.ts for compound provider/id and provider/name resolution, the gateway and scoped tiers against each other in both catalog orderings, and fallthrough to flat matching.

  • pnpm test:node (packages/harness-pi): 126 tests passed.
  • pnpm type-check:full (root): passed.
  • oxfmt and oxlint: clean on both changed files.

Checklist

  • All commits are signed (PRs with unsigned commits cannot be merged)
  • Tests have been added / updated (for bug fixes / features)
  • Documentation has been added / updated (for bug fixes / features)
  • A patch changeset for relevant packages has been added (for bug fixes / features - run pnpm changeset in the project root)
  • I have reviewed this pull request (self-review)

Related Issues

Related to #18155, #18147, #18153 (see the collapsed summary above for how each relates).

@fdarian
fdarian force-pushed the fix-harness-pi-compound-model-ids branch from f5c989b to bfb278f Compare August 3, 2026 10:35
…coped provider

createPiModelResolver only matched catalog entries by flat id/name, so a
compound id like "xai/grok-4.3" matched nothing and the model was silently
dropped, falling back to Pi's own hardcoded default (which tries
vercel-ai-gateway before xai and fails with "No API key found").

Add a provider-scoped resolution tier that splits on the first slash and
matches against the catalog's provider field, run before the existing flat
tiers. Kept as ordered tiers rather than one predicate because routing
proxies (vercel-ai-gateway, openrouter) can carry another provider's id
verbatim as their own flat id, and an OR'd predicate would let catalog
array order decide the winner.
@fdarian
fdarian force-pushed the fix-harness-pi-compound-model-ids branch from bfb278f to c0de15b Compare August 3, 2026 11:02
@fdarian
fdarian marked this pull request as draft August 3, 2026 11:13
@fdarian
fdarian marked this pull request as ready for review August 3, 2026 11:31
@ai-sdk-factory

Copy link
Copy Markdown
Contributor

Bugfix review

Outcome: changes-required

Fixes issue

Status: fully-addresses

The new scoped tier correctly resolves xai/grok-4.3 to the native xai entry while retaining the existing AI Gateway preference when gateway credentials are present.

Side effects

Risk: medium

The unconditional scoped tier can reroute legitimate proxy model IDs to an unauthenticated native provider.

Concerns:

  • For an OpenRouter-only user selecting openai/gpt-4o, the catalog contains an OpenRouter entry whose literal ID is openai/gpt-4o and a native OpenAI entry with ID gpt-4o. The new scoped tier selects native OpenAI without checking configured authentication, whereas the previous flat match selected OpenRouter. This can produce a missing OpenAI credential failure.
  • Pi's own resolver handles this ambiguity by preferring a uniquely authenticated literal-ID match when the inferred native provider is unauthenticated; the harness resolver preserves that behavior only for Vercel AI Gateway.

Performance

Risk: low

Compound IDs add one additional linear catalog scan, with no additional retained data or unbounded allocation.

Backwards compatibility

Risk: none

The change neither reads nor modifies persisted credentials, model configuration files, session data, or other stored formats.

Breaking changes

Risk: medium

Public types and exports are unchanged, but existing accepted compound model inputs can now select a different provider and fail authentication.

Concerns:

  • Authenticated non-gateway routing proxies such as OpenRouter lose precedence for literal IDs whose prefix is also a known native provider, changing externally observable routing and error behavior.

Architecture

Risk: low

The implementation remains localized within harness-pi and introduces no cross-package source imports or dependency-boundary violations.

Change scope

Status: minimal

The resolver change, regression tests, and patch changeset are directly related to the reported model-resolution bug.

Security

Risk: none

The change adds only in-memory string matching and does not introduce credential exposure, network access, parsing, or injection surfaces.

Testing

Status: needs-more

The tests cover native xAI resolution, catalog ordering, gateway precedence, and flat fallback, but omit authenticated non-gateway proxy collisions.

Concerns:

  • Add a regression test where only OpenRouter is configured and openai/gpt-4o exists both as an OpenRouter literal ID and as a native OpenAI scoped model; the resolver should not choose an unauthenticated provider.

Verification

Reviewed the complete three-file diff and installed Pi 0.80.10 resolver/catalog behavior. The harness-pi suite passed all 126 tests, focused type checking completed successfully, formatting and lint checks were clean, and diff validation found no whitespace errors. Catalog inspection exposed the untested authenticated-OpenRouter routing regression.

@felixarntz felixarntz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@fdarian Thank you for the PR!

Following the above agent feedback, I added a test, and it actually identified a flaw in the current solution: despite a gateway's auth being configured (in the test OpenRouter), the code still chooses the direct OpenAI model. So this needs to be fixed. I'm happy to add this.

https://github.com/vercel/ai/actions/runs/31431154190/job/93595814790?pr=18334

@felixarntz felixarntz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

pushed the fix in c30775b - with that, this is good to go.

Thanks again @fdarian!

@felixarntz
felixarntz merged commit b6642fa into vercel:main Aug 10, 2026
49 of 50 checks passed
@fdarian
fdarian deleted the fix-harness-pi-compound-model-ids branch August 11, 2026 03:25
@felixarntz felixarntz added the harness/pi Issues related to @ai-sdk/harness-pi label Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants