feat(evals): route TypeSafe decision-model connections through OpenRouter or Vercel AI Gateway - #17798
feat(evals): route TypeSafe decision-model connections through OpenRouter or Vercel AI Gateway#17798hassiebp wants to merge 1 commit into
Conversation
…uter or Vercel AI Gateway TypeSafe connections now pick an upstream (TypeSafe, Vercel AI Gateway, or OpenRouter) instead of a free-form base URL. Both gateways expose a TypeSafe-compatible /v1/systemone endpoint, so the existing @ai-sdk/typesafe-ai adapter only needs its base URL swapped. The upstream is stored in the connection config, decides the default Jev model IDs, and requires a fresh key when changed. The Jev pricing pattern also accepts OpenRouter's dated response model slugs. Co-authored-by: Hassieb Pakzad <hassiebp@users.noreply.github.com>
|
@claude review |
There was a problem hiding this comment.
Claude Code Review
No review was started: this request came from a bot account. Manual reviews can only be requested by someone with write access to this repository. Ask a maintainer to comment @claude review, or have your automation post the comment from a user account with write access.
Tip: disable this comment in your organization's Code Review settings.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
🟢 AWS preview image pushedArgo CD is rolling it out — the environment is usually ready within a URL: pr-17798 app preview (signs you in automatically; opt out with URL not loading / 404? Full debug guide — deploy allowlist, Logs (needs preview-cluster kubectl -n langfuse-pr-17798 logs -f deploy/langfuse-pr-17798-web # web
kubectl -n langfuse-pr-17798 logs -f deploy/langfuse-pr-17798-worker # worker
kubectl -n langfuse-pr-17798 port-forward svc/preview-mailpit 8025:8025 # captured email UI → http://localhost:8025Add Synthetic preview data only. Never add production data to public accounts. |
| label: | ||
| TYPESAFE_UPSTREAM_DEFINITIONS[upstream].label, | ||
| }))} | ||
| onValueChange={field.onChange} |
There was a problem hiding this comment.
Changing a TypeSafe connection's upstream only updates typeSafeUpstream, so existing custom model IDs remain unchanged. Since the upstreams use different model IDs and connection testing prefers customModels[0] over the selected upstream's defaults, a connection with a TypeSafe-specific custom model such as jev-1.13.0 cannot be switched to OpenRouter: the hidden stale model is tested against OpenRouter and the update fails. Please clear custom models when the upstream changes, expose them for editing, or otherwise reconcile them with the new upstream.
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/features/public-api/components/CreateLLMApiKeyForm.tsx
Line: 859
Comment:
**Stale models block switching**
Changing a TypeSafe connection's upstream only updates `typeSafeUpstream`, so existing custom model IDs remain unchanged. Since the upstreams use different model IDs and connection testing prefers `customModels[0]` over the selected upstream's defaults, a connection with a TypeSafe-specific custom model such as `jev-1.13.0` cannot be switched to OpenRouter: the hidden stale model is tested against OpenRouter and the update fails. Please clear custom models when the upstream changes, expose them for editing, or otherwise reconcile them with the new upstream.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 604cb48dbe
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if ( | ||
| resolveTypeSafeUpstream(params.inputConfig) !== | ||
| resolveTypeSafeUpstream(params.existingConfig) | ||
| ) { |
There was a problem hiding this comment.
Revalidate evaluator models before switching upstreams
When a TypeSafe connection with active decision-model evaluators is moved between upstreams, this check only requires a replacement secret. Evaluator definitions persist their provider and model, but the upstream defaults are incompatible (jev-1.13.0, jev-1.13, and typesafe-ai/jev); testUpdate probes the new upstream's first default while the worker later forwards each evaluator's old model unchanged. The update can therefore succeed and immediately cause scheduled evaluations to fail, so dependent evaluators must be migrated, revalidated, or blocked before accepting the switch.
Useful? React with 👍 / 👎.
| }); | ||
| } | ||
|
|
||
| assertDecisionModelUpstreamChangeHasSecretKey({ | ||
| adapter: existingKey.adapter, | ||
| existingConfig: existingKey.config, | ||
| inputConfig: input.config, | ||
| hasNewSecretKey, | ||
| }); | ||
|
|
||
| if (input.baseURL && isBaseURLChanged) { | ||
| await validateLlmConnectionBaseURL(input.baseURL); | ||
| } |
There was a problem hiding this comment.
🟡 (optional) testUpdate lets a caller preview a TypeSafe connection edit with a malformed config and silently tests the wrong upstream instead of failing clearly. create, update and test all call assertDecisionModelConnectionInput (router.ts:326, 562, 709) to reject a config that isn't {upstream}, but testUpdate never does, so resolveTypeSafeUpstream just falls back to the 'typesafe' default. A caller previewing a switch to openrouter or vercel-ai-gateway with a bad config (wrong key name, leftover config from another adapter) gets tested against api.typesafe.ai/v1/systemone with the new secret key, then hears pass/fail for the wrong target instead of 'Decision-model connections only accept an upstream in their config.' …
Why this was flagged
…Fix: call assertDecisionModelConnectionInput(input) inside testUpdate too, matching create/update/test.
web/src/features/llm-api-key/server/router.ts:642 builds config = input.config ?? existingKey.config inside testUpdate and passes it straight to testLLMConnection without ever calling assertDecisionModelConnectionInput (used at router.ts:326, 562, 709). When input.adapter/existingKey.adapter is TypeSafe and input.config doesn't match TypeSafeConfigSchema, resolveTypeSafeUpstream (packages/shared/src/server/llm/types.ts:518) safely falls back to DEFAULT_TYPESAFE_UPSTREAM='typesafe' rather than raising. testDecisionModelConnection then runs against https://api.typesafe.ai/v1/systemone using the caller's new secretKey, not the upstream the caller meant to test. Before this PR there was no config schema for TypeSafe at all, so this silent misroute couldn't happen; now the config carries routing meaning that testUpdate quietly ignores when malformed.
Verification: nit. Real but low-impact. testUpdate (web/src/features/llm-api-key/server/router.ts:586-669) never calls assertDecisionModelConnectionInput, unlike its siblings create (326), test (562) and update (709). At line 642 it builds const config = input.config ?? existingKey.config and passes it to testLLMConnection at lines 654-662. The tRPC input schema (web/src/features/llm-api-key/types.ts)…
|
Proof of work typesafe_connection_upstream_select_openrouter_vercel.mp4 What to doubt in review
|
🟡 Live preview: pr-17798 app preview
What does this PR do?
Lets a TypeSafe decision-model connection pick where Jev is served from instead of exposing a free-form base URL. The connection form gains an Upstream select with three presets:
https://api.typesafe.ai/v1, modelsjev-1.13.0,jev-latest)https://ai-gateway.vercel.sh/typesafe/v1, modelstypesafe-ai/jev,typesafe-ai/jev-latest)https://openrouter.ai/api/v1, modelsjev-1.13,jev-latest)Both gateways ship a TypeSafe-compatible
POST …/v1/systemoneendpoint that accepts TypeSafe's request shape (model,state,questionswithnoul/choice/score) and returns TypeSafe's response shape (model,answers,usage), so the existing@ai-sdk/typesafe-aiadapter only needs itsbaseURLswapped. See OpenRouter: TypeSafe SDK and Vercel: TypeSafe API with AI Gateway.Changes
packages/shared:TypeSafeConfigSchema({ upstream }) joins the connectionconfigunion;TYPESAFE_UPSTREAM_DEFINITIONSholds label, base URL, default model IDs, and docs link per upstream;resolveTypeSafeUpstream/getDecisionModelDefaultModelsread them.createTypeSafeDecisionModelClienttakes anupstreamand passes the matchingbaseURLto the AI SDK provider. Connections created before this change have no config and keep talking to TypeSafe directly.web: the LLM connection form renders the upstream select for the TypeSafe adapter and labels the key field per upstream. The router stores the upstream inconfig, still rejects a custom base URL and extra headers for decision models, rejects non-TypeSafe configs, and requires a new secret key when the upstream changes (a TypeSafe key does not work at OpenRouter). The connection test and the evaluator test call use the stored upstream. The decision-model picker lists per-upstream default model IDs.worker: the eval execution path passes the stored upstream to the client. The Jev pricingmatchPatternadditionally accepts OpenRouter's dated response slugs (typesafe/jev-1.13-20260917) and the~typesafe/alias so gateway-routed evaluations still get a cost.Test this
https://pr-17798.preview.langfuse.com/project/<id>/settings/llm-connections(thetypesafeadapter is behind thedecisionModelEvaluatorsflag: Langfuse admins orLANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=true):jev-1.13, jev-latest.noulquestion againsthttps://openrouter.ai/api/v1/systemone.Data: no seed needed; the settings page is enough. No gateway keys were available in the agent environment, so the authenticated round-trip against OpenRouter / Vercel (step 3) still needs a human with a key.
Type of change
langfuse-docs)Mandatory Tasks
Verification
pnpm --filter @langfuse/shared run test src/server/llm/typesafe/typeSafeDecisionModelClient.test.ts—Tests 6 passed (6)(new: per-upstream URL routing, tolerance of OpenRouter's extra response fields)pnpm --filter web run test src/__tests__/server/llm-api-key.servertest.ts—Tests 35 passed (35)(new: upstream persisted in config, base URL / foreign config rejected, upstream change requires a new key, legacy connection without config resolves to TypeSafe)pnpm --filter worker run test src/features/evaluation/decisionModel/runDecisionModelEvaluation.test.ts—Tests 3 passed (3)pnpm exec turbo run typecheck lint --force --filter=web --filter=@langfuse/shared—Tasks: 7 successful, 7 total,Cached: 0 cached, 7 totalpnpm --filter worker run typecheckandpnpm --filter worker run lint— both exit 0 (run directly; the turbo path needs the Rust toolchain for@langfuse/native, which is not installed here)pnpm exec knip— exit 0node .agents/skills/add-model-price/scripts/test-match-pattern.mjs --model jev …— 15 accept/reject samples pass;validate-pricing-file.mjsvalidates 172 entriesPOSTprobes of all three/v1/systemoneendpoints return401with a JSON error the adapter parses (paths exist)The PR should not merge until upstream changes handle existing custom model IDs, because stale identifiers can prevent otherwise valid gateway migrations.
Summary
This PR adds preset TypeSafe decision-model upstreams and propagates the selected upstream through connection management, evaluator testing, production worker execution, model selection, and Jev pricing.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR Form[LLM connection form] -->|stores config.upstream| DB[(LLM connection)] DB --> Selector[Decision model selector] DB --> WebTest[Evaluator test] DB --> Worker[Evaluation worker] Selector -->|upstream-specific model IDs| Eval[Evaluator definition] WebTest --> Resolve[Resolve TypeSafe upstream] Worker --> Resolve Resolve --> TS[TypeSafe API] Resolve --> Vercel[Vercel AI Gateway] Resolve --> OR[OpenRouter] TS --> Price[Jev pricing match] Vercel --> Price OR --> PriceReviews (1) · Last reviewed commit: "feat(evals): route TypeSafe decision-mod..."