Skip to content

feat(provider): add TypeSafe Jev provider and judgment model - #2335

Merged
zhangmo8 merged 5 commits into
devfrom
feat/typesafe-jev-judgment-model
Sep 21, 2026
Merged

zhangmo8 merged 5 commits into
devfrom
feat/typesafe-jev-judgment-model

Conversation

@zhangmo8

@zhangmo8 zhangmo8 commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds TypeSafe's Jev as a provider protocol and wires it into an opt-in, per-agent
judgment model used for tool-permission review. Refs #2326.

Why it has this shape

Jev is not a chat model. Its entire documented HTTP surface is one decision endpoint:

POST https://api.typesafe.ai/v1/systemone
{ "state": …, "model": "jev-latest", "questions": { … } }
-> { "model": "jev-1.13.0", "answers": { … }, "usage": { … } }

No chat messages, no streaming, no tool calls, and — by design — typed answers instead of generated
text. It therefore cannot reuse any @ai-sdk/* transport, and src/main has no hand-written
LanguageModelV2/doStream to extend, so this is a dedicated BaseLLMProvider subclass selected by
an explicit instance branch (the same shape ollama already uses).

jev is deliberately not added to PROVIDER_API_TYPE_REGISTRY. That registry maps a protocol to
an AiSdkProviderDefinition and would route Jev to a transport that cannot express it.

What's implemented

Provider / protocol (docs/features/typesafe-jev-provider/spec.md)

  • JevProvider: POST /v1/systemone, Bearer auth, GET /v1/models discovery ({ models: [...] } is
    not the OpenAI { data: [...] } shape, so it is parsed directly).
  • Connection check is the authenticated catalog fetch — it spends no tokens, which matters because
    TypeSafe bills input tokens per request and a generation probe is meaningless here.
  • Every chat-shaped entry point refuses rather than issuing a request: promise members throw a typed
    unsupported-capability error, coreStream yields it as a stream error event.
  • ModelType.Judgment keeps these models out of chat pickers; ModelSelect additionally hides them
    from any picker that does not explicitly request that type.
  • Disabled built-in typesafe profile with a static fallback catalog so the picker is not empty
    before the first refresh.
  • Custom providers (自定义服务商) can select apiType: jev; jev joins the import and deeplink
    allow-lists so an imported config keeps its api type instead of degrading to openai-completions.
  • Provider mark: TypeSafe's official square logo under assets/llm-icons/, registered under both the
    typesafe provider id and the jev api type. Note the repo convention is a static asset plus a
    modelIconRegistry.ts entry — not a websites.icon field, which no code reads.

Agent judgment model (docs/features/agent-judgment-model/spec.md)

  • New per-agent judgmentModel, restricted to judgment models, read only by the permission
    reviewer.
  • With it set, review issues one System One call and composes the typed answers in code. With it
    unset, the existing generative path runs unchanged.
  • Safety floors are enforced in code and are not overridable by the model: critical blocks, high
    asks the user, and failure / timeout / malformed answers ask the user.
  • The generative path's actionHash echo is replaced by code-side binding — Jev cannot echo
    anything — so a verdict stays bound to one exact action and its arguments.
  • rationale is fixed local copy derived from the classification, not model-authored text.
  • Questions, thresholds, and composition live in one file (jevPermissionQuestions.ts) because
    TypeSafe's own guidance is that those are the parts a human must review.

Scope decision worth reviewing

assistantModel actually has five readers, not the two the issue mentions: permission review,
compaction, session titles, translation, and memory consolidation. Only the reviewer is repointed.

Compaction specifically cannot move to Jev: generateRollingSummary returns a summary string
via BaseLLMProvider.summaries, and Jev does not generate text. This matches the issue's non-goals,
which keep compaction on the assistant model.

UI changes

BEFORE — agent settings, model fields                AFTER — agent settings, model fields
┌──────────────────────────────────────────┐        ┌──────────────────────────────────────────┐
│ chat model           [icon] <model>   ⌄  │        │ chat model           [icon] <model>   ⌄  │
│ assistant model      [icon] <model>   ⌄  │        │ assistant model      [icon] <model>   ⌄  │
│ vision model         [icon] <model>   ⌄  │        │ judgment model       [icon] <model>   ⌄  │ ← new
│ image generation     [icon] <model>   ⌄  │        │ vision model         [icon] <model>   ⌄  │
└──────────────────────────────────────────┘        │ image generation     [icon] <model>   ⌄  │
                                                    └──────────────────────────────────────────┘
                                                    judgment picker lists only ModelType.Judgment
BEFORE — chat model picker                AFTER — chat model picker
lists every enabled model type            Judgment models hidden unless the picker
                                          explicitly asks for that type
BEFORE — add custom provider, API type    AFTER
┌────────────────────────────┐            ┌──────────────────────────────────┐
│ OpenAI                     │            │ OpenAI                           │
│ OpenAI Completions         │            │ OpenAI Completions               │
│ Gemini                     │            │ Gemini                           │
│ Anthropic                  │            │ Anthropic                        │
│ Ollama                     │            │ Ollama                           │
│ Mistral AI                 │            │ Mistral AI                       │
└────────────────────────────┘            │ TypeSafe Jev (System One)        │ ← new
                                          └──────────────────────────────────┘
BEFORE — TypeSafe provider row            AFTER
[generic fallback icon] TypeSafe          [TypeSafe mark] TypeSafe

Verification

  • pnpm run format — clean
  • pnpm run i18n — 23 locales, no missing or invalid keys
  • pnpm run lint — 0 warnings, 0 errors
  • pnpm run typecheck — node + web clean
  • pnpm exec vitest run test/main/provider …819 passed / 72 files, no regressions in the existing provider suite
  • Commit 1 also typechecks in isolation, so the history stays bisectable
  • The new icon registry keys were checked against all 77 existing provider id/api-type values: the
    only resolutions that change are the two new ones

New tests pin real contracts: the non-chat guarantee, the System One request body, the
critical/high floor, each auto-allow threshold, failure and malformed answers resolving to
ask_user, abort propagation, and the unchanged generative path when the slot is unset.

What this PR does NOT do

  • No evaluation, and thresholds are placeholders. Every constant in jevPermissionQuestions.ts
    is marked provisional. [Feature] 实验性探索:使用 Jev 作为独立的权限审核后端 #2326 treats evidence (false-allow rate, false-block rate, Chinese
    authorization, injection resistance, latency, cost) as the condition for adoption — a working API
    call is explicitly not a pass. Hence Refs #2326, not Closes #2326.
  • No privacy notice. Once a judgment model is configured, tool arguments and recent conversation
    are sent to the configured third-party service, and the UI says nothing about it. The issue lists
    data-sending scope as a to-verify item; this is the most user-facing gap remaining.
  • src/types/i18n.d.ts is already stale in the repo relative to zh-CN; running pnpm run i18n:types
    rewrites ~336 unrelated lines. The one judgmentModel leaf was inserted by hand. Wholesale
    regeneration belongs in its own change.
  • The Mongolian and Tibetan labels are plausible localizations, not translations the author can
    vouch for — worth a native check.
  • The new option in the add-provider select is hardcoded English, matching every neighbouring option
    in that select (none of them are i18n'd).
  • opencode-go is the one built-in provider without a registered mark (acp legitimately resolves
    its icon dynamically from the ACP registry). Pre-existing, unrelated to this PR.

Summary by CodeRabbit

  • New Features
    • Added optional judgment models for reviewing tool-permission requests with safety-aware outcomes.
    • Added TypeSafe Jev (System One) provider support, including model discovery and bundled fallback models.
    • Added judgment model selection in agent settings and model configuration.
    • Judgment models are clearly labeled and excluded from standard chat model pickers.
  • Safety
    • Critical-risk actions are blocked; high-risk, uncertain, failed, or invalid reviews request user confirmation.
  • Localization
    • Added judgment-model and TypeSafe Jev translations across supported locales.
  • Documentation
    • Added implementation plans and specifications for judgment models and TypeSafe Jev support.

TypeSafe's Jev is a System One decision model, not a chat model: its
documented surface is a single POST /v1/systemone that evaluates typed
questions against a state and returns typed answers. It has no chat
messages, streaming, tool calls, or text generation, so it cannot reuse
any existing @ai-sdk/* transport.

Add a `jev` protocol served by a dedicated JevProvider:

- discovery and connection check use the authenticated GET /v1/models
  call, which spends no tokens;
- every chat-shaped entry point refuses instead of issuing a request, so
  selecting a Jev model as a chat model fails at selection time rather
  than at runtime;
- the API key stays in the main process and is sent only to the
  configured base URL.

Add ModelType.Judgment so these models are never offered by a chat
picker, and exclude them from type-less ModelSelect pickers to enforce
that at selection time.

Ship a disabled built-in `typesafe` profile with a static fallback
catalog, and let custom providers select the same protocol, which is
added to the import and deeplink allow-lists so an imported config keeps
its api type instead of degrading to openai-completions.

`jev` is deliberately not registered in PROVIDER_API_TYPE_REGISTRY: that
registry maps to an AiSdkProviderDefinition and would route the protocol
to a transport that cannot express it. The instance branch mirrors how
ollama is already handled.

Refs #2326
assistantModel serves five readers: permission review, compaction,
session titles, translation, and memory consolidation. Issue #2326 wants
a System One (Jev) reviewer, which is impossible while review and
compaction share one setting.

Add a separate, opt-in `judgmentModel` slot restricted to
ModelType.Judgment models and read only by the permission reviewer.
Compaction and the other four readers stay on assistantModel: Jev does
not generate text, so it cannot produce the rolling summary that
compaction needs, and the issue's non-goals keep compaction where it is.

When the slot is set, review issues one System One call and composes the
typed answers in code. When it is unset, the existing generative path
runs unchanged.

Safety floors are enforced in code and are not overridable by the model:
critical still blocks, high still asks the user, and failure, timeout,
or malformed answers ask the user. The generative path's actionHash echo
is replaced by code-side binding, since Jev cannot echo anything, which
keeps a verdict bound to one exact action and its arguments. Rationale is
fixed local copy derived from the classification, not model-authored
text.

Questions, thresholds, and composition live in one file because
TypeSafe's own guidance is that those are the parts a human must review.
Every threshold is provisional pending the evaluation the issue requires
before adoption.

Refs #2326
@coderabbitai

coderabbitai Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: d1677b5b-460b-4ca1-8081-058303b5541c

📥 Commits

Reviewing files that changed from the base of the PR and between 0748902 and 93551d5.

⛔ Files ignored due to path filters (1)
  • src/renderer/src/assets/llm-icons/typesafe.png is excluded by !**/*.png
📒 Files selected for processing (83)
  • docs/features/agent-judgment-model/plan.md
  • docs/features/agent-judgment-model/spec.md
  • docs/features/typesafe-jev-provider/plan.md
  • docs/features/typesafe-jev-provider/spec.md
  • src/main/agent/deepchat/deepChatAgentRepository.ts
  • src/main/agent/deepchat/runtime/jevPermissionQuestions.ts
  • src/main/agent/deepchat/runtime/toolPermissionReviewer.ts
  • src/main/agent/deepchat/runtime/toolRuntimeBindings.ts
  • src/main/provider/defaults.ts
  • src/main/provider/index.ts
  • src/main/provider/managers/providerInstanceManager.ts
  • src/main/provider/providerImportService.ts
  • src/main/provider/providers/jevProvider.ts
  • src/renderer/settings/components/AddProviderFlow.vue
  • src/renderer/settings/components/DeepChatAgentsSettings.vue
  • src/renderer/settings/components/ProviderConfigImportDialog.vue
  • src/renderer/settings/components/ProviderModelList.vue
  • src/renderer/src/components/ModelChooser.vue
  • src/renderer/src/components/ModelSelect.vue
  • src/renderer/src/components/icons/modelIconRegistry.ts
  • src/renderer/src/components/settings/ModelConfigDialog.vue
  • src/renderer/src/i18n/bo-CN/model.json
  • src/renderer/src/i18n/bo-CN/settings.json
  • src/renderer/src/i18n/da-DK/model.json
  • src/renderer/src/i18n/da-DK/settings.json
  • src/renderer/src/i18n/de-DE/model.json
  • src/renderer/src/i18n/de-DE/settings.json
  • src/renderer/src/i18n/en-US/model.json
  • src/renderer/src/i18n/en-US/settings.json
  • src/renderer/src/i18n/es-ES/model.json
  • src/renderer/src/i18n/es-ES/settings.json
  • src/renderer/src/i18n/fa-IR/model.json
  • src/renderer/src/i18n/fa-IR/settings.json
  • src/renderer/src/i18n/fr-FR/model.json
  • src/renderer/src/i18n/fr-FR/settings.json
  • src/renderer/src/i18n/he-IL/model.json
  • src/renderer/src/i18n/he-IL/settings.json
  • src/renderer/src/i18n/id-ID/model.json
  • src/renderer/src/i18n/id-ID/settings.json
  • src/renderer/src/i18n/it-IT/model.json
  • src/renderer/src/i18n/it-IT/settings.json
  • src/renderer/src/i18n/ja-JP/model.json
  • src/renderer/src/i18n/ja-JP/settings.json
  • src/renderer/src/i18n/ko-KR/model.json
  • src/renderer/src/i18n/ko-KR/settings.json
  • src/renderer/src/i18n/mn-Mong-CN/model.json
  • src/renderer/src/i18n/mn-Mong-CN/settings.json
  • src/renderer/src/i18n/ms-MY/model.json
  • src/renderer/src/i18n/ms-MY/settings.json
  • src/renderer/src/i18n/pl-PL/model.json
  • src/renderer/src/i18n/pl-PL/settings.json
  • src/renderer/src/i18n/pt-BR/model.json
  • src/renderer/src/i18n/pt-BR/settings.json
  • src/renderer/src/i18n/ru-RU/model.json
  • src/renderer/src/i18n/ru-RU/settings.json
  • src/renderer/src/i18n/tr-TR/model.json
  • src/renderer/src/i18n/tr-TR/settings.json
  • src/renderer/src/i18n/ug-CN/model.json
  • src/renderer/src/i18n/ug-CN/settings.json
  • src/renderer/src/i18n/vi-VN/model.json
  • src/renderer/src/i18n/vi-VN/settings.json
  • src/renderer/src/i18n/zh-CN/model.json
  • src/renderer/src/i18n/zh-CN/settings.json
  • src/renderer/src/i18n/zh-HK/model.json
  • src/renderer/src/i18n/zh-HK/settings.json
  • src/renderer/src/i18n/zh-TW/model.json
  • src/renderer/src/i18n/zh-TW/settings.json
  • src/shared/contracts/domainSchemas.ts
  • src/shared/jevProtocol.ts
  • src/shared/model.ts
  • src/shared/providerDeeplink.ts
  • src/shared/providerImport.ts
  • src/shared/types/agent-interface.d.ts
  • src/shared/types/provider.ts
  • src/types/i18n.d.ts
  • test/main/agent/deepchat/runtime/jevPermissionQuestions.test.ts
  • test/main/agent/deepchat/runtime/toolPermissionReviewer.test.ts
  • test/main/provider/defaultProviders.test.ts
  • test/main/provider/jevProvider.test.ts
  • test/main/provider/providerImportService.test.ts
  • test/renderer/components/DeepChatAgentsSettings.test.ts
  • test/renderer/components/ModelChooser.test.ts
  • test/renderer/components/ModelSelect.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The change adds an opt-in Jev judgment model, a TypeSafe System One provider, typed permission-review composition, judgment-model configuration, picker filtering, localization, and related tests. The existing generative review path remains when no judgment model is configured.

Changes

Jev judgment review

Layer / File(s) Summary
Judgment contracts and model selection
src/shared/..., docs/features/...
Adds Jev request and response types, ModelType.Judgment, optional judgmentModel configuration, and provider/runtime contracts.
Jev provider transport and catalog
src/main/provider/..., test/main/provider/...
Adds TypeSafe catalog discovery, System One requests, fallback catalogs, unsupported chat capability errors, provider registration, and import tagging.
Typed permission-review routing
src/main/agent/deepchat/runtime/..., test/main/agent/deepchat/runtime/...
Adds fixed Jev questions, local decision thresholds, bounded head-and-tail review context, typed verdict composition, abort handling, and generative-path fallback.
Agent settings and picker integration
src/renderer/..., src/types/i18n.d.ts, test/renderer/...
Adds the judgment-model selector, Jev provider setup, judgment-model filtering, icons, localized strings, and renderer tests.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant AgentSettings
  participant ToolPermissionReviewer
  participant ProviderRuntime
  participant JevProvider
  participant TypeSafeSystemOne
  AgentSettings->>ToolPermissionReviewer: configure judgmentModel
  ToolPermissionReviewer->>ProviderRuntime: runJudgment(state, questions)
  ProviderRuntime->>JevProvider: runJudgment(request, signal)
  JevProvider->>TypeSafeSystemOne: POST /v1/systemone
  TypeSafeSystemOne-->>JevProvider: typed answers
  JevProvider-->>ProviderRuntime: JevJudgmentResult
  ProviderRuntime-->>ToolPermissionReviewer: review result
Loading

Suggested reviewers: zerob13

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.17% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 24 functions across 26 files. (57 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: adding the TypeSafe Jev provider and judgment model support.
Linked Issues check ✅ Passed The changes satisfy the coding objectives in issue #2326. The PR adds an opt-in per-agent judgmentModel that is separate from assistantModel, preserves the existing generative path, and keeps the …
Out of Scope Changes check ✅ Passed The changes stay within issue #2326. Provider discovery and error handling support the independent Jev backend. Model classification, picker filtering, agent settings, import handling, model managemen…
Full details: Docstring Coverage

Explanation

Docstring coverage is 29.17% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 24 functions across 26 files. (57 skipped: 57 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@zhangmo8 zhangmo8 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reviewed at 246ce7477. I read the code rather than only the description, and verified locally: pnpm run typecheck clean, pnpm run format:check clean, and the three touched test files pass (28 tests). CI's test-renderer job also passes at this SHA.

What holds up

  • The non-chat guarantee is real, not just documented: every chat-shaped entry point refuses, coreStream emits an error event instead of issuing a request, and discovery reports ModelType.Judgment so the type-level filters keep these models out.
  • The safety floors are enforced in code and cannot be overridden by the model, rationale is fixed local copy rather than model-authored text, and failure / timeout / malformed answers all resolve to ask_user.
  • Binding the verdict with the caller-computed actionHash instead of a model echo is the right call for a model that cannot echo, and it removes a whole class of "verdict applies to the wrong action" bugs.
  • The new tests pin contracts that matter rather than implementation shape: non-chat refusal, the System One request body, the critical/high floors, each auto-allow threshold, failure and malformed answers, abort propagation, and the unchanged generative path when the slot is unset.

Findings

Ordered by how much they change behavior. Nothing here blocks a draft, but the first two are worth settling before this leaves draft.

  1. The static fallback catalog appears unreachable from the picker it is meant to populate, and an empty catalog fetch can clear the cache instead of falling back to it (inline on defaults.ts).
  2. Under the judgment path a medium-risk action can never be auto-allowed, which makes it strictly more interruptive than the reviewer it replaces — and that policy difference sits in a literal rather than in the threshold block (inline on jevPermissionQuestions.ts).
  3. The judgment branch skips the post-call abort re-check the generative path performs (inline on toolPermissionReviewer.ts).
  4. Tool results are truncated from the head only, which limits what the injection question can see (inline on jevPermissionQuestions.ts).
  5. The new picker exclusion has no test (inline on ModelSelect.vue).
  6. Minor: unused protocol helper, and a redundant provider-id branch (inline on jevProtocol.ts and providerInstanceManager.ts).

Note on the failures I did not report: this machine times out two cases in DeepChatAgentsSettings.test.ts (10s limit), but the base commit 074890266 times out the same two under identical conditions and CI passes test-renderer at this SHA, so I attribute that to the machine, not to this change.

enable: false,
// Static fallback so the judgment-model picker is populated before the first catalog refresh.
// Live discovery from `GET /v1/models` stays authoritative once it succeeds.
models: [

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The static fallback catalog looks unreachable from the picker it is meant to populate. ModelSelect renders modelStore.enabledModels (src/renderer/src/components/ModelSelect.vue:108), and that store is filled from getDbProviderModels (there is no typesafe entry in the provider DB) plus the main-process per-provider model store (src/renderer/src/stores/modelStore.ts:677-717). That store is only ever written by BaseLLMProvider.fetchModels (src/main/provider/baseProvider.ts:314); nothing seeds it from DEFAULT_PROVIDERS[].models. providerModelHelper defaults to models: [] (src/main/provider/providerModelHelper.ts:104-107) and reads only its own store (:239-246), so these two static entries never reach a picker.

There is a second half to this: JevProvider.fetchProviderModels returns [] when apiKey is empty (:228) and on any catalog error (:233-236), and fetchModels persists whatever it receives — so enabling TypeSafe before entering a key, or one transient /v1/models failure, stores an empty catalog and clears anything previously discovered, rather than falling back to the static entries.

If the fallback is meant to be user-visible, it needs a seeding path (or a rule that an empty fetch must not overwrite a non-empty cache). If it is not meant to be user-visible, the claim in the description should be corrected. Please confirm which one is intended.

: 0

const mayAutoAllow =
riskLevel === 'low' &&

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

mayAutoAllow is hardcoded to riskLevel === 'low', so under the judgment path a medium-risk action can never be auto-allowed. The generative reviewer this replaces explicitly allows medium ("Allow low and medium risk actions", toolPermissionReviewer.ts:206), and the medium criteria defined above (line 65: "installing a dependency, or running a routine local command") describe exactly the routine actions the current reviewer approves without asking.

So switching an agent to a judgment model will make it noticeably more interruptive, which may well be the intent — but the difference lives in a literal instead of in JEV_REVIEW_THRESHOLDS, where the rest of the policy is reviewable. Suggest an explicit autoAllowMaxRiskLevel threshold and one line in the spec stating the intended difference between the two paths.

if (judgmentProviderId && judgmentModelId) {
// Bound by the same review timeout as the generative path so a stalled judgment still falls
// back to asking the user instead of hanging the permission flow.
return await reviewWithJudgmentModel(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The judgment branch returns straight out of reviewWithJudgmentModel, so it skips the throwIfAbortRequested(context.signal) re-check that the generative path performs after its provider call (lines 395 and 418). If the user cancels while the judgment call is in flight and the call still resolves, a verdict is returned for a turn that was already cancelled. Adding the same re-check after the judgment call makes the two paths behave alike.

Same family, lower impact: JevProvider.createRequestSignal (src/main/provider/providers/jevProvider.ts:301-317) registers the parent abort listener without checking callerSignal.aborted first, so an already-aborted caller signal is silently dropped — unlike coreStream, which calls throwIfAborted() on entry (:180).

* to see them.
*/
export const JEV_REVIEW_MAX_RECENT_MESSAGES = 6
export const JEV_REVIEW_MAX_CONTENT_CHARS = 1500

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Tool results are deliberately retained because they are a primary injection vector, but truncateReviewText keeps only the head of each message (value.slice(0, maxChars), toolPermissionReviewer.ts:65). An instruction that tries to steer the decision and appears after the first 1500 characters of a long tool result never reaches the injection_pressure question — the control this is supposed to feed. Head+tail truncation, or a larger budget for role: 'tool' messages, would close that gap.

!props.type ||
props.type.length === 0 ||
(model.type !== undefined && props.type.includes(model.type as ModelType))
props.type && props.type.length > 0

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This changes the contents of every type-less picker, but nothing tests it: test/renderer/components/ModelSelect.test.ts is untouched, and the settings test only asserts the prop wiring ([ModelType.Judgment] is passed for the judgment slot). Since "a chat-shaped picker never lists a judgment model" is the user-visible guarantee this PR calls out, please add a ModelSelect case covering both directions: absent with no type, present with type=[Judgment].

Comment thread src/shared/jevProtocol.ts Outdated
return answer.type === 'choice'
}

export function isJevScoreAnswer(answer: JevAnswer): answer is JevScoreAnswer {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

isJevScoreAnswer has no caller in src/ or test/; JevScoreQuestion/JevScoreAnswer are only referenced from the JevQuestion/JevAnswer unions. Likewise jevProvider.ts:320 re-exports JevQuestion even though every consumer imports it from @shared/jevProtocol. Either use them or drop them — an unused type guard on a protocol boundary reads as a supported answer type that the reviewer does not actually handle.

return new OllamaProvider(provider, this.options.providerSettings, this.options.locale)
}

if (provider.id === 'typesafe' || provider.apiType === 'jev') {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

provider.id === 'typesafe' is redundant with apiType === 'jev' for the built-in, and it carries a foot-gun: if a user edits the built-in TypeSafe entry to a different apiType (for example to point it at an OpenAI-compatible endpoint), it still resolves to JevProvider, which refuses every chat call. Prefer the apiType-only check unless the id branch is load-bearing for a case I am missing.

Provider marks are not a `websites.icon` field — no code reads that.
The convention is a static asset plus a registry entry, which the
original provider commit missed.

Add the official square mark from typesafe.ai under
assets/llm-icons/, and register it under both `typesafe` (the provider
id) and `jev` (the api type, which also covers the `jev-latest` and
`jev-1.13.0` model ids).

The mark is colour on its own background, so it is deliberately not
added to monoIconUrls, which drives dark-mode inversion for monochrome
currentColor marks.

Verified that adding these two keys changes no existing provider's
resolved icon: 73 of 75 built-in providers already resolved, and the
only two resolutions that change are the new ones.

Refs #2326
@zhangmo8
zhangmo8 requested review from yyhhyyyyyy and zerob13 and removed request for zerob13 September 20, 2026 09:11
Make the bundled Jev catalog load-bearing rather than decorative. It is
now returned whenever the live catalog is unavailable or empty, because
`BaseLLMProvider.fetchModels` persists whatever the provider returns and
an empty list would clear a previously discovered catalog. The earlier
claim that the static entries populated the picker on their own was
wrong: the renderer reads the persisted per-provider model store, and
nothing seeds it from `DEFAULT_PROVIDERS[].models`.

Promote the auto-allow risk cap from a literal to `autoAllowMaxRiskLevel`
so the intentional difference from the generative path — which allows
medium — is reviewable, and record its consequence for the evaluation:
the two paths do not share a policy, so interruption counts do not
isolate the model.

Restore parity with the generative path on cancellation: the judgment
branch now performs the same post-call abort re-check, so a judgment that
resolves after cancellation cannot return a verdict for a cancelled turn.
The provider also stops silently dropping an already-aborted caller
signal.

Keep the tail as well as the head of each message in the judgment state.
Head-only truncation hid an instruction placed at the end of a long tool
result, which is exactly what the injection question exists to see. The
generative path's truncation is unchanged.

Select the provider by api type only. The id branch was redundant — the
built-in already declares `apiType: 'jev'` — and it would pin the
provider to JevProvider if a user repointed that entry.

Drop the unused `isJevScoreAnswer` guard and the redundant `JevQuestion`
re-export, which read as support for an answer type nothing handles.

Refs #2326
@zhangmo8

Copy link
Copy Markdown
Collaborator Author

Addressed all six in f07402f9c. Thanks — findings 1 and 2 were real bugs, not style notes.

1. Fallback catalog — you were right, and I had it backwards

Both halves confirmed before changing anything:

  • BaseLLMProvider.fetchModels persists whatever the provider returns (baseProvider.ts:314), so returning [] on a missing key or a failed fetch does clear a previously discovered catalog.
  • Nothing seeds the renderer's store from DEFAULT_PROVIDERS[].models. typesafe was the only entry in DEFAULT_PROVIDERS with a static models array, so I had invented a pattern that does not work.

Answering your question directly: the fallback is meant to be user-visible, so it now has a real path to the picker. fetchProviderModels returns the bundled catalog whenever the live catalog is unavailable or empty — no API key, transport or status failure, or a 200 with { models: [] }. The bundled entries reach the provider instance through the stored provider config (settings.ts:74-87 carries models, and :414 appends new default providers whole on upgrade), which is where the fallback reads them from. The spec claim was corrected; it previously asserted something untrue.

Three cases are now pinned by tests.

2. Medium risk — promoted to a named threshold, and documented

autoAllowMaxRiskLevel replaces the literal, and the risk comparison goes through an explicit order map. I kept the cap at low rather than silently matching the generative path, because that choice should be made with evidence, not in a review fix.

You are right that this makes the path strictly more interruptive, and I recorded the consequence you implied: the two paths do not share a policy, so an evaluation that counts interruptions is not measuring the model alone. The spec now says the cap must be aligned before drawing a conclusion about Jev's judgment quality. A test pins medium -> ask_user so the difference cannot drift unnoticed.

3. Cancellation parity

Both fixed. The judgment branch now re-checks context.signal after the call, matching lines 395/418. createRequestSignal returns an already-aborted signal instead of dropping it — coreStream's throwIfAborted() on entry was the precedent I should have followed.

4. Truncation direction

Head+tail in the judgment state. I did not change the generative path's truncation, since its prompt content is the baseline the evaluation compares against and changing it belongs in its own change. Worth noting the same latent weakness exists there.

5. ModelSelect test

Added, both directions: absent with no type, present with type: [ModelType.Judgment], and the chat model absent from the judgment picker.

6. Cleanups

Dropped isJevScoreAnswer and the JevQuestion re-export. I kept JevScoreQuestion/JevScoreAnswer in the unions because they describe the wire format the API accepts, and added a comment stating that nothing composes a score answer — the types document the protocol, the guards track what is actually consumed.

Provider selection is now api-type-only.

On the two DeepChatAgentsSettings failures

I verified your attribution properly rather than taking it on trust. My first attempt was invalid — git switch --detach aborted on uncommitted doc changes, so the "base" run had actually executed against my branch. Re-run on a clean tree at 074890266:

FAIL  … > loads, resets, normalizes, and saves per-Agent output limits
FAIL  … > shows pending and success feedback while deriving save availability from canonical data
Tests  2 failed | 25 passed (27)

Identical failures, same names, at base. Machine, not this change. Note the second one is an assertion (listAgents called twice), not a timeout — it is a cascade of the first timing out mid-flight, which is why it looks unrelated at a glance.

Gates after the fixes

format clean · i18n clean · lint 0/0 · typecheck node+web clean · focused suites 22 passed, including the new fallback, medium-risk, tail-retention, and already-aborted-signal cases.

@zerob13 zerob13 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.

Reviewed at f07402f9c (54 files, +2128/-23). Two things block; the rest are smaller and listed after.

Blocking

1. The bundled fallback catalog disappears after a provider reorder

JevProvider.fetchProviderModels falls back to this.provider.models (src/main/provider/providers/jevProvider.ts:232), which is the copy of the static catalog stored in the providers settings JSON. That copy does not survive a provider reorder. The settings sidebar reorders by sending provider summaries, and getProviderSummaries() omits models, customModels, enabledModels and disabledModels (LlmProviderSummarySchema, src/shared/contracts/domainSchemas.ts:429); reorderProvidersAtomic then writes the received array over the whole providers list (src/main/provider/providerHelper.ts:237). After any drag or move up/down, the typesafe entry has no models key, and getProviders() passes entries that already have apiType through unchanged, so nothing restores it.

This matters because the fallback is the only thing standing between a failed fetch and an empty picker. BaseLLMProvider.fetchModels persists whatever the provider returns (src/main/provider/baseProvider.ts:313), so an empty return clears the per-provider model store the picker actually reads. Net effect: after a reorder, the first failed or key-less refresh wipes the last-known TypeSafe catalog and the judgment picker goes empty — the exact regression this fallback was added to prevent.

Suggested fix: prefer the last-known catalog and use the static seed only when there is none.

const bundled = this.models.length > 0 ? this.models : this.getBundledCatalog()

this.models is already loaded from the per-provider store in the base constructor (loadCachedModels, src/main/provider/baseProvider.ts:180-189), so it survives a reorder. The comment at jevProvider.ts:228-231 and docs/features/typesafe-jev-provider/spec.md:114-123 should be corrected with it — they claim the fallback protects the previously discovered catalog, which is only true while the settings-JSON seed survives. (The reorder path dropping those four arrays is pre-existing and probably deserves its own issue; it is not something to fix here.)

2. Jev models are still selectable in the MCP sampling picker

ModelSelect now hides ModelType.Judgment when no type filter is passed, but the other model picker in the app did not get the same treatment. ModelChooser returns every enabled model when type is empty (src/renderer/src/components/ModelChooser.vue:135-141), and its only caller — the MCP sampling dialog — does not pass a type (src/renderer/src/components/mcp/McpSamplingDialog.vue:187). A Jev model can be selected there, and the request reaches generateCompletionStandalone (src/main/mcp/mcpClient.ts:1064) before failing with jev-unsupported-capability.

The spec lists this as an invariant and an acceptance criterion (docs/features/typesafe-jev-provider/spec.md:173,194), so either the filter goes into ModelChooser (one line, same condition), or the spec should say which surfaces are actually covered.

Should fix

3. Imported jev providers put Jev models in chat pickers

jev was added to the import allow-list, but the models built during import carry no type (src/main/provider/providerImportService.ts:1388-1400), and the picker filter treats "no type" as "not a judgment model". An imported Jev model therefore appears in every chat picker and fails only at runtime. Tag them with ModelType.Judgment when the target api type is jev.

4. Out-of-range probabilities are accepted

readNoulProbability and the confidence read only check typeof === 'number' && Number.isFinite (src/main/agent/deepchat/runtime/jevPermissionQuestions.ts:119-122,197-206). A noul or confidence outside [0, 1] is invalid output, but it can only ever push toward auto_allow, because authorization >= 0.8 and confidence >= 0.6 are both satisfied by an oversized value. Everything else in that file fails closed; this is the one input that does not. Reject values outside [0, 1] so they resolve to ask_user, or clamp them.

This is not reachable through prompt injection — it needs the endpoint to return off-spec numbers — but the code already tried to validate here and stopped halfway, and a silent scale change on TypeSafe's side would read as "strong yes" instead of an error.

5. Two more surfaces don't know about the new type

  • The import dialog renders the raw string jev, because apiTypeLabel has no case for it and settings.data.providerImport.apiTypes has no key (src/renderer/settings/components/ProviderConfigImportDialog.vue:831-851).
  • The model manager cannot filter by judgment: TYPE_ORDER does not include it (src/renderer/settings/components/ProviderModelList.vue:385-393), so the TYPE_ICONS entry added at :408 is unreachable and the filter chip never appears.

6. The judgment model sends data to a third party and the UI does not say so

Once a judgment model is configured, tool arguments and the recent conversation go to the configured service. The field is a label plus a picker and nothing else. Worth one line of copy under the field before this leaves draft; it is also absent from the plan's Deferred list, so nothing tracks it.

7. Spec and plan disagree with the code

  • spec.md:3 says Status: proposed while plan.md:3 says implemented.
  • plan.md:42 still lists id === 'typesafe' || apiType === 'jev' as done; the code is api-type-only.
  • spec.md:173,194 — see finding 2.

Minor

  • A failed judgment review is invisible to the user. The reviewer returns a generic rationale for ask_user, the caller drops it, and the only trace is a console.warn (toolPermissionReviewer.ts:464-481, dispatch.ts:1694-1704). A misconfigured judgment model silently turns every auto-approve into a prompt. Fail-safe, but there is no way for a user to find out why.
  • spec.md:79-82 says the request "is keyed by the action hash". Nothing is keyed by it: the hash is computed and returned inside one call, and no consumer compares it. The binding is real (same call, same request object, no caching), but the wording promises a check that does not exist. Either add the comparison in dispatch.ts or soften the sentence.
  • Thresholds are pinned by direction only, never by value: no test uses authorization exactly 0.8, confidence exactly 0.6, or injection exactly 0.2, so moving a threshold keeps the suite green. composeJevReviewDecision is exported and never imported directly — a boundary test there is cheaper than more end-to-end cases.
  • The failure test asserts ask_user but not that the generative path was skipped (test/main/agent/deepchat/runtime/toolPermissionReviewer.test.ts:262-278). One line: expect(generateCompletionStandalone).not.toHaveBeenCalled().
  • Head-and-tail truncation ignores the marker length (17 chars over budget) and can split a surrogate pair at the cut (toolPermissionReviewer.ts:76-79). Cosmetic; the JSON stays valid.
  • release_date is parsed but never read, and JEV_PERMISSION_QUESTION_IDS, JEV_RISK_LEVELS and JEV_REVIEW_THRESHOLDS are exported without a consumer outside the module.
  • JevProvider re-implements the request-signal helper that BaseLLMProvider.createModelRequestSignal already provides (baseProvider.ts:125-161). The base version aborts with a distinguishable provider_request_timeout code; the private copy aborts bare, so a timeout and a caller cancel are indistinguishable downstream.
  • test/renderer/components/DeepChatAgentsSettings.test.ts:1005 — the test name still says "image generation" but it now asserts the judgment selector too.
  • ModelConfigDialog has no judgment option in its type select, so the dialog for a Jev model shows the placeholder (ModelConfigDialog.vue:186-206).

Verified as holding

  • The refusal surface is real: completions/summaries/generateText/summaryTitles throw the typed error, coreStream yields an error event and stops, and no request is issued. Cleanup sits in finally; no unhandled rejections.
  • The safety floors hold branch by branch. critical → block and high → ask_user come before anything else; an unknown or non-string risk level, a missing risk_level, a missing confidence, a missing noul signal, and empty answers all resolve to ask_user. The four gates are an AND, and only low can reach auto_allow.
  • Explicit user confirmation is never overridden: the reviewer is only consulted in auto_approve mode (dispatch.ts:2324-2333,3265-3267).
  • Abort and timeout semantics match the generative path, and an already-aborted caller signal is no longer dropped. Both are pinned by tests.
  • Keeping jev out of PROVIDER_API_TYPE_REGISTRY is correct: its only consumer resolves an AI SDK factory and warns on a miss, so no Jev model can reach the AI SDK transport.
  • ModelType.Judgment is additive. The only exhaustive Record<ModelType, …> is patched, there is no switch over model types, isChatSelectableModelType already excludes it, and LlmProviderSchema.models is optional so stored rows still parse.
  • The icon keys are safe: resolveModelIconKey is a first-substring match over declaration order, and no existing key is a substring of typesafe/jev or the reverse, so no existing provider's icon changes.
  • i18n: the key appears exactly once in all 23 locales, the JSON is valid, and node scripts/validate-i18n.mjs passes. The Tibetan and Mongolian strings are consistent with their neighbours; I cannot vouch for them as a native speaker either.
  • The settings row reuses the existing field loop instead of bolting on a special case, and no chat-shaped picker can silently select a judgment model (chatSelectableModelGroups already filters by isChatSelectableModelType).
  • runJudgment stays in the main process; nothing about it crosses preload or IPC, and the API key never leaves main.
  • The new tests pin contracts rather than shapes: the System One request body, the { models: [...] } catalog shape, the three fallback branches, the non-chat refusal, the floors, the medium-risk policy, head+tail retention, and the unchanged generative path.

@zerob13
zerob13 marked this pull request as ready for review September 20, 2026 11:44
Fix the fallback preference order. Seeding only from
`this.provider.models` was wrong: the settings sidebar reorders by
sending provider summaries, which omit `models`, and the reorder writes
that array over the whole providers list, so the seed disappears after
any drag or move. The last-known catalog (`this.models`, loaded from the
per-provider store, which survives the reorder) is now preferred, with
the bundled seed used only when nothing has been discovered. Without
this the fallback was empty exactly when it was needed, and the next
failed or key-less refresh would have wiped the picker.

Extend the judgment exclusion to `ModelChooser`, the MCP sampling
picker's source. It was the remaining picker that could select a Jev
model and reach `generateCompletionStandalone` before failing.

Tag imported models as `ModelType.Judgment` when the target api type is
`jev`. Imported sources carry no type, which the picker filters read as
"not a judgment model", so an imported Jev model landed in every chat
picker and failed only at request time.

Reject out-of-range probabilities. An oversized `noul` or `confidence`
satisfied its `>=` gate, so invalid output could only push toward
`auto_allow` — the one input in the composition that did not fail
closed. A silent scale change on the provider's side now reads as an
error rather than a strong yes.

Teach the remaining surfaces about the type: the import dialog's api
type label, the model manager's type filter order and chip label, and
the model config dialog's type select.

Disclose that a configured judgment model sends tool arguments and the
recent conversation to the configured service.

Delegate the request signal to `BaseLLMProvider.createModelRequestSignal`
instead of re-implementing it, so a timeout aborts with
`provider_request_timeout` and stays distinguishable from a caller
cancel.

Pin the policy boundaries by literal value in a `composeJevReviewDecision`
test and make the threshold constants module-private, so moving a
boundary turns the suite red instead of staying green.

Reconcile spec and plan with the code: status, the api-type-only branch,
the action-binding wording (nothing compares a returned hash), and the
picker surfaces actually covered.

Refs #2326
@zhangmo8

Copy link
Copy Markdown
Collaborator Author

Both blockers confirmed and fixed in 93551d549. Everything else addressed except two items I deliberately left, noted at the end.

1. Fallback catalog after a provider reorder — confirmed

I verified both halves before changing anything: LlmProviderSummarySchema omits models, customModels, enabledModels and disabledModels, and reorderProvidersAtomic writes the received array over the whole providers list. So this.provider.models is gone after any drag, and the fallback I added in the previous round was empty exactly when it was needed.

Applied your suggestion — the last-known catalog wins, the static seed is only used when nothing has been discovered:

const fallback = this.models.length > 0 ? this.models : this.getBundledCatalog()

this.models is loaded from the per-provider store by the base constructor, so it survives the reorder. Pinned by a test that fetches successfully, then fails, and asserts the previously discovered list comes back rather than the seed.

You are right that my comment and the spec overstated this. Both claimed the fallback protected the previously discovered catalog, which was only true while the settings-JSON seed survived. Corrected in both places, and the reorder path's data loss is now recorded in the plan's Deferred list rather than fixed here — agreed it is pre-existing and deserves its own issue.

2. MCP sampling picker — fixed

Same condition as ModelSelect, now in ModelChooser. Test covers both directions (absent with no type, present with type: [Judgment]). The spec's invariant now names both pickers instead of implying blanket coverage.

3. Imported models — fixed

buildModelMeta now takes the resolved api type and tags ModelType.Judgment when it is jev. One correction to the finding as written: the observable path is addCustomModel, not the provider's models array — imported models go to the custom-model store, which getModels() merges. The test asserts there, and my first attempt asserted on getCurrentProviders()[].models and correctly failed.

4. Out-of-range probabilities — fixed

Both reads now go through a readProbability that rejects anything outside [0, 1], so an oversized value resolves to ask_user rather than satisfying its gate. Pinned with authorization: 1.5, authorization: -1, confidence: 1.5, and injection: -0.5.

5. Surfaces — fixed

  • Import dialog: apiTypeLabel case added, plus the settings.data.providerImport.apiTypes.jev key in all 23 locales.
  • Model manager: judgment added to TYPE_ORDER. This needed one more thing you did not mention — the chip label resolves model.filter.typeOptions.${type}, and no locale's model.json had a judgment entry, so a bare TYPE_ORDER addition would have rendered the raw key. Added that key in all 23 model.json files.
  • Model config dialog: a judgment option was added to the type select.

6. Privacy disclosure — added

One line under the judgment field, in all 23 locales. Also added to the plan's Deferred list alongside the other untracked items.

7. Spec and plan — reconciled

Status, the api-type-only branch, and the covered picker surfaces. Also softened the action-binding wording: you were right that nothing compares a returned hash. The spec now says the binding is structural (one call, one request object, no reuse) and explicitly states what it does not claim.

Minors

Done: policy boundaries pinned by literal value in a composeJevReviewDecision test with the constants made module-private; the failure test now asserts the generative path was skipped; head-and-tail truncation accounts for the marker and will not split a surrogate pair; JevProvider delegates to BaseModelProvider.createModelRequestSignal so a timeout carries provider_request_timeout; the settings test name no longer says only "image generation"; failed-review invisibility is recorded in Deferred.

Deliberately not done:

  • release_date stays. It is parsed but unused today; the spec's Deferred list is to surface it in the model manager, so I kept it and commented that rather than deleting it.
  • The getProviderSummaries reorder data loss is recorded in Deferred, per your note that it is not something to fix here.

Gates

format clean · i18n 23 locales, no missing or invalid keys · lint 0/0 · typecheck node+web clean · 1827 passed / 124 files.

@zerob13 zerob13 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.

Re-review: delta f07402f93551d5 (second review round)

Verdict: Approve. The new commit addresses every blocking and should-fix finding from the previous review, plus the relevant minor ones. I re-verified each fix against the source, ran the affected test suites and the i18n validation on this branch — all pass. No new blocking findings in the delta.

Blocking findings from last round — both resolved

  1. Fallback catalog wiped after a provider reorder — fixed. fetchProviderModels now prefers the last-known catalog over the bundled seed: this.models.length > 0 ? this.models : this.getBundledCatalog() (jevProvider.ts). This was the exact suggested fix: this.models is loaded from the per-provider model store, which survives the reorder that strips models from the settings JSON. A new test ("prefers the last-known catalog over the bundled seed when a later fetch fails") proves a failed refresh after a successful one keeps the discovered catalog instead of regressing to the seed.
  2. Jev models selectable in the MCP sampling picker — fixed. ModelChooser.vue now filters out ModelType.Judgment when the caller passes no type filter (which is the MCP sampling dialog's case), and still shows them when a picker explicitly requests that type. New test covers both directions.

Should-fix findings — all resolved

  • Imported jev providers now tag models as ModelType.Judgment (providerImportService.ts buildModelMeta), so imported Jev models no longer leak into chat pickers; tested via the Hermes import path.
  • Out-of-range noul/confidence values are now rejected by a shared readProbability gate ([0, 1], finite) and fail closed to ask_user; boundary tests pin the exact thresholds by literal value, and the threshold constants are now module-private so the tests are the contract.
  • The import dialog renders a localized "Jev" api-type label instead of the raw string; i18n keys added across all 23 locales and validation passes.
  • The model manager type filter now includes Judgment in its ordered list with a localized label, and the model config dialog's type select offers it.
  • The judgment model setting now shows a disclosure that tool arguments and the recent conversation are sent to the configured third-party service (en + zh-CN wording checked, both accurate).
  • Spec and plan are reconciled with the code: status updated to implemented, the api-type-only branch rationale documented, the action-binding wording now correctly states the binding is structural (one call, one request object) rather than a returned-hash comparison, and the covered picker surfaces are listed.

Also in the delta (beyond the findings) — good

  • JevProvider.createRequestSignal now delegates to BaseLLMProvider.createModelRequestSignal instead of re-implementing it, so a timeout aborts with provider_request_timeout and stays distinguishable from a caller cancel. Signature verified against the base implementation.
  • Head-and-tail truncation in toolPermissionReviewer.ts now counts the truncation marker against the char budget and refuses to cut inside a surrogate pair — a real correctness improvement for non-ASCII content.
  • The deferred items (silent judgment-failure visibility, and the pre-existing provider-reorder summary drop that strips models) are now explicitly documented in the plan instead of being silently ignored. Both are correctly out of scope for this PR.

Verification on this branch

  • pnpm run i18n:validate — passed (23 locales).
  • pnpm exec vitest run on jevPermissionQuestions, toolPermissionReviewer, jevProvider, providerImportService (main) and ModelChooser, DeepChatAgentsSettings (renderer): 85/85 passed.

Detailed references

  • src/main/provider/providers/jevProvider.ts:229-256 — fallback preference order.
  • src/renderer/src/components/ModelChooser.vue:136-144 — type-less picker judgment exclusion.
  • src/main/provider/providerImportService.ts:1389-1412 — judgment tagging on import.
  • src/main/agent/deepchat/runtime/jevPermissionQuestions.ts:119-131,210-213readProbability and confidence fail-closed.
  • src/main/provider/providers/jevProvider.ts:319-334 — signal delegation.
  • src/main/agent/deepchat/runtime/toolPermissionReviewer.ts:82-95 — surrogate-safe truncation.
  • test/main/provider/jevProvider.test.ts:142-170 — last-known catalog test.
  • test/renderer/components/ModelChooser.test.ts:146-157 — both-direction exclusion test.

@zhangmo8
zhangmo8 merged commit 27d6b7c into dev Sep 21, 2026
12 checks passed
@zhangmo8
zhangmo8 deleted the feat/typesafe-jev-judgment-model branch September 21, 2026 02:11
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.

2 participants