Skip to content

feat(evals): route TypeSafe decision-model connections through OpenRouter or Vercel AI Gateway - #17798

Open
hassiebp wants to merge 1 commit into
mainfrom
hassiebbot/typesafe-gateway-upstreams-58f0
Open

hassiebp wants to merge 1 commit into
mainfrom
hassiebbot/typesafe-gateway-upstreams-58f0

Conversation

@hassiebp

@hassiebp hassiebp commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

🟡 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:

  • TypeSafe (https://api.typesafe.ai/v1, models jev-1.13.0, jev-latest)
  • Vercel AI Gateway (https://ai-gateway.vercel.sh/typesafe/v1, models typesafe-ai/jev, typesafe-ai/jev-latest)
  • OpenRouter (https://openrouter.ai/api/v1, models jev-1.13, jev-latest)

Both gateways ship a TypeSafe-compatible POST …/v1/systemone endpoint that accepts TypeSafe's request shape (model, state, questions with noul/choice/score) and returns TypeSafe's response shape (model, answers, usage), so the existing @ai-sdk/typesafe-ai adapter only needs its baseURL swapped. See OpenRouter: TypeSafe SDK and Vercel: TypeSafe API with AI Gateway.

typesafe_connection_upstream_select_openrouter_vercel.mp4

Add LLM connection dialog with the TypeSafe adapter, Upstream set to OpenRouter, default models jev-1.13 and jev-latest, and an OpenRouter API key field

Changes

  • packages/shared: TypeSafeConfigSchema ({ upstream }) joins the connection config union; TYPESAFE_UPSTREAM_DEFINITIONS holds label, base URL, default model IDs, and docs link per upstream; resolveTypeSafeUpstream / getDecisionModelDefaultModels read them. createTypeSafeDecisionModelClient takes an upstream and passes the matching baseURL to 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 in config, 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 pricing matchPattern additionally 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 (the typesafe adapter is behind the decisionModelEvaluators flag: Langfuse admins or LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=true):

  1. + Add LLM Connection → adapter typesafe (experimental).
  2. A new Upstream select appears; pick OpenRouter — the key field label becomes "OpenRouter API key" and the default models switch to jev-1.13, jev-latest.
  3. Paste an OpenRouter key and create; the connection test runs one noul question against https://openrouter.ai/api/v1/systemone.
  4. Edit the connection, switch upstream to Vercel AI Gateway without entering a key → blocked with "Enter the API key for the new upstream."
  5. Under Evaluators → decision model, the model list for that connection shows the upstream's IDs.

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

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update (the LLM connections docs should mention the upstream presets; follow-up in langfuse-docs)

Mandatory Tasks

  • Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.

Verification

  • pnpm --filter @langfuse/shared run test src/server/llm/typesafe/typeSafeDecisionModelClient.test.tsTests 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.tsTests 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.tsTests 3 passed (3)
  • pnpm exec turbo run typecheck lint --force --filter=web --filter=@langfuse/sharedTasks: 7 successful, 7 total, Cached: 0 cached, 7 total
  • pnpm --filter worker run typecheck and pnpm --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 0
  • node .agents/skills/add-model-price/scripts/test-match-pattern.mjs --model jev … — 15 accept/reject samples pass; validate-pricing-file.mjs validates 172 entries
  • Unauthenticated POST probes of all three /v1/systemone endpoints return 401 with a JSON error the adapter parses (paths exist)
  • Browser check on the rebuilt local Cloud stack (video above): upstream select, per-upstream default models, docs link and key label, and the empty-key validation error
Open in Web Open in Cursor 

RetriggerConfidence Score: 4/5

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.

  • Adds TypeSafe, Vercel AI Gateway, and OpenRouter endpoint/model definitions.
  • Stores and validates the selected upstream while preserving legacy TypeSafe behavior.
  • Routes test and production evaluations through the selected endpoint.
  • Expands pricing matching for gateway response aliases.
  • One upstream-switching edge case remains for connections carrying custom model IDs.
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 --> Price
Loading

Reviews (1) · Last reviewed commit: "feat(evals): route TypeSafe decision-mod..."

…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>
@github-actions

Copy link
Copy Markdown
Contributor

@claude review

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-22T17:23:24.220014Z 604cb48 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

🟢 AWS preview image pushed

Argo CD is rolling it out — the environment is usually ready within a
few minutes of this comment.

URL: pr-17798 app preview (signs you in automatically; opt out with /auth/sign-in?autoSignIn=false)
Login: demo@langfuse.com / password
API keys: pk-lf-1234567890 / sk-lf-1234567890
Commit: 604cb48

URL not loading / 404? Full debug guide — deploy allowlist,
sleeping preview, pod status, ClickHouse:
https://github.com/langfuse/langfuse/blob/main/.agents/skills/langfuse-previews/SKILL.md#debug-a-preview

Logs (needs preview-cluster kubectl access):

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:8025

Add --previous for a crashed container, --tail=200 to limit, or
kubectl -n langfuse-pr-17798 get pods to inspect status.

Synthetic preview data only. Never add production data to public accounts.

label:
TYPESAFE_UPSTREAM_DEFINITIONS[upstream].label,
}))}
onValueChange={field.onChange}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +161 to +164
if (
resolveTypeSafeUpstream(params.inputConfig) !==
resolveTypeSafeUpstream(params.existingConfig)
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment on lines 620 to 632
});
}

assertDecisionModelUpstreamChangeHasSecretKey({
adapter: existingKey.adapter,
existingConfig: existingKey.config,
inputConfig: input.config,
hasNewSecretKey,
});

if (input.baseURL && isBaseURLChanged) {
await validateLlmConnectionBaseURL(input.baseURL);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 (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)…

@cursor

cursor Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Proof of work

typesafe_connection_upstream_select_openrouter_vercel.mp4

What to doubt in review

  • The default model IDs per upstream come from docs, not from an authenticated call. jev-latest is documented for TypeSafe and OpenRouter, typesafe-ai/jev for Vercel. typesafe-ai/jev-latest appears only in the AI SDK docs, and jev-1.13 (OpenRouter's slug) may or may not resolve through the TypeSafe-compatible path. One real key per gateway settles this in a minute.
  • The upstream lives in config.upstream, and the base URL is derived at call time. Existing TypeSafe connections have config: null and fall back to TypeSafe. If someone prefers the URL persisted in baseURL (what every other adapter does), that is a small swap.
  • Changing the upstream on update requires a new key. That is enforced both in the form and in update / testUpdate, but I did not exercise the update flow in the browser because creating a connection needs a real key for the connection test.
  • The Jev pricing regex now also matches typesafe/jev-1.13-20260917 (OpenRouter's response model). Check that this does not accidentally cover a future non-Jev slug.
  • Self-hosted deployments with an LLM base-URL allowlist will need openrouter.ai / ai-gateway.vercel.sh added; nothing in this PR changes the allowlist.

This branch was successfully deployed

1 active deployment
PR Preview 604cb48d Deployed Sep 22, 2026 by github-actions[bot]
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