Commit Graph

78 Commits

Author SHA1 Message Date
matevip
9e93c52d9a fix(goal): real evaluator, retry refactor, hardened node + extra edges 2026-05-21 22:27:00 +08:00
matevip
c34e8290ac feat(goal,ui): inline set-goal prompt, terminal system-line, sidebar dot 2026-05-21 22:26:40 +08:00
matevip
9c5ad29d42 chore(llm): drop unused imports, add XIAOMI_MIMO cross-turn cache tests 2026-05-21 17:24:53 +08:00
倪程伟
7861f603eb
fix(llm): MiMo thinking 模式 reasoning_content 多轮对话兼容修复 (#189)
MiMo V2 系列在 thinking 模式下,assistant 消息携带 tool_calls 时必须同时包含 reasoning_content,否则提供方返回 400。

- ModelFamily 新增 MIMO_THINKING 族,detect() 添加 mimo* 匹配
- FallbackPolicy 新增 XIAOMI_MIMO(patchCrossTurn=true, patchNonToolCall=true)
- 新增 ReasoningContentCache,按 tool_call_ids 回放真实推理内容
- 缓存作用范围:所有 patchCrossTurn=true 的 thinking provider(MiMo + DeepSeek)
- NodeStreamingChatHelper 流式响应完成后写入缓存

Closes #188
2026-05-21 17:15:35 +08:00
matevip
5cc567a689 refactor(llm): downgrade embedding connectivity test failure log to warn 2026-05-20 10:58:21 +08:00
倪程伟
2e4f88c612
fix(llm): switch slash-bearing modelId from path variable to query parameter (#177)
Closes #174

Model identifiers like 'Qwen/Qwen3-Embedding-8B' or
'Pro/deepseek-ai/DeepSeek-V3' carry forward slashes that Spring MVC
decodes from %2F before path matching, so even with the frontend's
encodeURIComponent the request never reaches the handler and 404s out.

The two affected endpoints take modelId as a request param instead:

  DELETE /{providerId}/models/{modelId}      -> DELETE /{providerId}/models?modelId=...
  POST   /{providerId}/models/{modelId}/test -> POST   /{providerId}/models/test?modelId=...

modelApi.removeProviderModel / testModel in the UI follow suit, passing
the id via axios params so axios handles the URL encoding consistently.
providerId stays as a path variable — provider ids are kebab-case and
never contain slashes.
2026-05-20 10:22:44 +08:00
倪程伟
461f81ccb5
fix(llm): add @Slf4j and log embedding test failures with full stack trace (#176)
Closes #175

ModelConfigController.testEmbedding() previously caught and stringified
the exception's getMessage() into the response body without writing
anything to the server log. Operators investigating an Embedding test
failure saw only the truncated client-side message — root causes like
the DashScope-native vs OpenAI-compat routing bug (#166) or the
requireApiKey gap (#167) were invisible server-side.

Add @Slf4j to the controller and log.error the full stack trace
alongside the failing modelId, so future Embedding test regressions are
diagnosable from the server log without redeploying with debug
breakpoints.
2026-05-20 10:18:48 +08:00
matevip
3340885da3 fix(llm): skip chat-style probe for embedding-prefix models in DashScope discovery 2026-05-20 10:14:25 +08:00
matevip
a6b5e3b515 refactor(llm): extract testable embedding protocol routing + drop dead fromProviderId 2026-05-20 10:14:12 +08:00
matevip
70a599e403 fix(embedding): use NoopApiKey for keyless OpenAI-compatible providers 2026-05-20 10:14:05 +08:00
倪程伟
16b5d75d2c
fix(llm): exclude soft-deleted models from uniqueness check in validateModel (#173)
Closes #169

ModelConfigService.validateModel() flagged a duplicate when re-adding a
manually-typed (provider, modelName) pair that happened to match a row
with deleted=1 in mate_model_config. The user-visible symptom: adding
'dashscope/qwen3-plus' fails with 'model identifier already exists',
yet the management page shows no such model.

The project itself runs hard-delete via deleteById(), so the user-facing
delete path doesn't create deleted=1 rows. The stale rows come from
schema migrations (V44, V81) that intentionally tombstone bogus catalog
entries — for instance V81 sets deleted=1 on the non-existent
'qwen3-plus' (id=1000000172) so it stays out of routing but preserves
the id for audit. ModelConfigEntity has no @TableLogic, and the project
has no global logic-delete-field config, so LambdaQueryWrapper queries
do not auto-append the deleted filter; the migration tombstones leak
into the validate-model query.

Add an explicit .eq(getDeleted, 0) to the uniqueness check so migration
tombstones don't block legitimate re-adds.

Follow-up: several other queries in ModelConfigService share the same
oversight (list/get methods), and a future migration could drop the
tombstones entirely to align with the V20 hard-delete posture.
2026-05-20 09:33:33 +08:00
倪程伟
828ece526e
fix(llm): add text-embedding- prefix to DashScope native model allow-list (#172)
Closes #168

The native DashScope provider exposes both chat and embedding models, but
DASHSCOPE_NATIVE_ALLOW_PREFIXES only listed chat families
(qwen-/qwen2-/qwen3-/deepseek-/baichuan/yi-/llama). When a user manually
added text-embedding-v1/v2/v3/v4 to the dashscope provider,
assertModelIdAcceptable() rejected the id because no allow prefix matched.

Add 'text-embedding-' to the allow-list and broaden the doc comment from
"native chat protocol" to "native protocol (chat or embedding)" so the
intent is clear.

Discovery probing is chat-based and will still mark embedding entries
probeOk=false; surfacing them as discoverable embedding suggestions is a
separate follow-up.
2026-05-20 09:29:11 +08:00
倪程伟
e0f66eef25
fix(embedding): respect requireApiKey flag in OpenAI-compatible embedding factory (#171)
Closes #167

EmbeddingModelFactory.buildOpenAi() hard-failed on any provider whose API
key was empty or unusable, so keyless providers like Ollama and OpenCode
(declared with requireApiKey=false) could pass the chat connectivity test
but bounce when the same provider's embedding model was tested.

Mirror the chat path in OpenAiCompatibleChatModelBuilder.buildOpenAiApi:

- If requireApiKey is not explicitly false, an unusable key still throws.
- If requireApiKey == false, the key check is skipped and an empty string
  is passed to OpenAiApi.builder() so no Authorization: Bearer header is
  attached to the outgoing request.
2026-05-20 09:25:16 +08:00
倪程伟
8e00613e69
fix(embedding): use chatModel field for protocol routing instead of providerId matching (#170)
Closes #166

EmbeddingModelFactory used EmbeddingProtocol.fromProviderId() to pick the
embedding protocol, which substring-matches 'dashscope' / 'qwen' / 'aliyun'
in the providerId. The dashscope-compat provider carries 'dashscope' in its
id but runs in OpenAI compatible mode (chatModel='OpenAIChatModel',
baseUrl='https://dashscope.aliyuncs.com/compatible-mode/v1'). Routing it to
DASHSCOPE_EMBEDDING made DashScopeApi build its native path against the
compat base, producing 404s on every embedding call.

Switch to the chatModel column instead — the same signal ModelProtocol
.fromChatModel() uses for the chat path. chatModel='DashScopeChatModel'
takes the native protocol; everything else (including dashscope-compat)
takes OpenAI-compatible.

EmbeddingProtocol.fromProviderId() is retained for reference but is no
longer called; future callers should follow the chatModel pattern.
2026-05-20 09:22:06 +08:00
matevip
db16ff02a5 chore: drop external project name references from code comments 2026-05-20 08:09:43 +08:00
matevip
d53d66abe3 feat(chat): per-conversation model selection (#150) 2026-05-18 16:27:27 +08:00
matevip
0ff8da0caa fix(llm): stop MODEL_NOT_FOUND from evicting the whole provider (#150) 2026-05-18 16:27:20 +08:00
matevip
75107ac815 feat(llm): native Gemini chat builder, Nano Banana image gen, xAI/Grok provider 2026-05-18 10:00:55 +08:00
matevip
a88edbdd07 refactor(llm): decouple model construction from the agent graph layer (#147) 2026-05-18 07:47:49 +08:00
matevip
ce33c1c652 feat(llm): add UI to configure embedding models from any provider (#79) 2026-05-17 09:25:09 +08:00
matevip
26df2c94d0 fix(rbac): let viewers read active model so they can actually chat 2026-05-15 10:19:03 +08:00
matevip
bba9975749 feat(llm,datasource): gate model/provider/oauth/datasource endpoints to admin 2026-05-15 10:18:03 +08:00
matevip
2ef806aa64 chore(deps): bump Spring AI 1.1.5 -> 1.1.6 and Spring AI Alibaba 1.1.2.2 -> 1.1.2.3 2026-05-13 07:39:27 +08:00
srant1
5f77434953
fix(llm): activate default model after OpenAI OAuth and gate provider checks by enabled
OAuth token save now promotes the first available chat model when no usable default exists. Default-model resolution and provider availability checks require Provider.enabled=true alongside credentials, so disabled Providers no longer return stale defaults. /models/enabled drops the single-Provider hard-code so OpenAI OAuth and other enabled chat models surface in selectors. Docker exposes the 1455 PKCE callback via MATECLAW_OAUTH_OPENAI_CALLBACK_BIND_HOST (default 127.0.0.1) and a port mapping; deployment mode stays Host-driven.
2026-05-12 09:58:30 +08:00
matevip
08fcab12d1 fix(deploy): drop DASHSCOPE_API_KEY requirement so Docker boots with no LLM key configured (#99) 2026-05-11 20:44:31 +08:00
matevip
9fd5843dda fix(llm): pin OpenAI-compatible HTTP client to HTTP/1.1 (#89) 2026-05-11 11:22:23 +08:00
matevip
c2aecf18ef feat(agent,llm): multimodal sidecar routing for unsupported attachments (#87) 2026-05-09 16:41:26 +08:00
matevip
d2886bdaa3 fix(llm): let custom OpenAI-compatible providers opt out of API Key requirement (#89) 2026-05-09 14:26:35 +08:00
matevip
d5969bb32c fix(chat,llm): liveness-aware model popup with row-based configured check (#81) 2026-05-09 11:07:08 +08:00
matevip
609da7029f fix(mcp): expand JVM system properties in stdio args/env/cwd 2026-05-05 14:46:08 +08:00
matevip
704c6317e3 feat(llm): live model discovery for ChatGPT OAuth provider 2026-05-05 13:06:19 +08:00
matevip
bf7bc73f5f fix(llm): JSON body + 403/404 status mapping for OpenAI device code 2026-05-05 13:06:11 +08:00
matevip
be2235a493 feat(llm): OAuth device authorization grant for ChatGPT remote deploy 2026-05-05 10:39:47 +08:00
matevip
6fe9b6e5f7 fix(llm): skip Ollama auto-discovery network probe when provider disabled 2026-05-03 20:32:17 +08:00
matevip
55f4ba1195 feat(llm): per-Model HTTP read-timeout override 2026-05-02 15:40:59 +08:00
matevip
7ca568c69b fix(skill): knowledge wrappers + provider routing + feature gates 2026-05-01 09:49:37 +08:00
matevip
d927521d51 feat(skill): install/uninstall split + Requirements API + provider router 2026-05-01 09:48:59 +08:00
matevip
47bdb97a3a fix(agent): per-model multimodal capability resolution (issue #44) 2026-04-30 17:25:51 +08:00
matevip
977e181949 feat(cron): unify output, add reminder task type, in-flight progress UI
Three layers landed together because they share the same routing /
lifecycle plumbing:

1. Cron output unification
   - New CronConversationResolver routes web-origin jobs to the per-workspace
     tasks_<wsId> conversation; IM-bound jobs go to the channel session
     conversation when one exists (matched by senderId then targetId);
     legacy cron_<id> remains as the fallback.
   - CronJobLifecycleService inserts a system-role header divider when a
     run starts so users browsing the unified tasks_<wsId> view can tell
     which job started a run. BaseAgent.sanitizeForLlm filters these
     headers so they never reach the model.
   - WorkspaceService seeds tasks_<wsId> on workspace creation; V65
     migration backfills existing workspaces.
   - DeliveryConfig gains a userId field so IM session lookup can match
     by senderId (replyToken-based targetId is not stable across runs).
   - ConversationVO recognizes tasks_/cron_ underscore prefix as cron
     source. MessageList renders the system header as a labeled divider.
   - ChatConsole pins tasks_* conversations and tracks per-conversation
     read state so new cron output gets a visible unread dot.

2. Reminder task type
   - New task_type='reminder' in CronJobEntity + service validation.
   - CronJobRunner short-circuits 'reminder' jobs: hands trigger_message
     to finishRunAndPublish verbatim, no LLM call. Fixes a regression
     where reminders were rephrased into echoed wrappers.
   - New create_reminder tool alongside create_cron_job, with descriptions
     tightened so the model picks the right one (verbatim push vs LLM
     query that needs computation).
   - CronJobs.vue gets a third radio option + dedicated reminder field.

3. In-flight progress placeholder
   - Cron uses non-streaming chat()/execute(); tool-heavy ReAct loops
     can run 1-5 minutes between start and finish with no visible
     state, looking hung.
   - New GET /api/v1/cron-jobs/active-runs returns runs in status=running
     for a conversation. ChatConsole polls it on the existing 4s tick
     (and on conversation switch) and shows a spinner bar with elapsed
     time. When run count drops to zero, it refetches messages so the
     assistant bubble appears within ~1s of finish.
2026-04-30 15:01:24 +08:00
matevip
69f065e212 fix(llm): support Volcano Ark base URLs and surface friendly errors
- Generalize the OpenAI-compatible chat/models path resolver so any
  baseUrl ending in /v{N} (Ark /v3, Zhipu /v4, ...) drops the duplicate
  /v1 prefix. Volcano Engine test-connection and chat were posting to
  /api/v3/v1/chat/completions and getting 404.
- Replace the six pre-seeded Doubao alias rows (doubao-1.5-*) with five
  valid Ark direct-call ids (doubao-seed-1-8-251228 etc.) and flip
  support_model_discovery=TRUE so users can refresh their account's
  actual catalog. Aliases were marketing names, not API names, so every
  call hit InvalidEndpointOrModel.NotFound.
- Translate Ark business errors into actionable Chinese hints: include
  the response body in the error chain, match ModelNotOpen and
  InvalidEndpointOrModel codes, extract the offending model id, and
  classify them as MODEL_NOT_FOUND so failover skips retries.
2026-04-28 19:26:58 +08:00
matevip
6a3df2a6e0 feat(llm): enabled column for providers + Add Provider drawer
Adds explicit user-intent gating to the provider catalog. Fresh installs
get an empty dropdown by default — built-in cloud + local providers
(OpenAI, Anthropic, Ollama, LM Studio, MLX, llama.cpp, etc.) live in a
new 'Add Provider' drawer until the user opts them in. Existing installs
upgrade conservatively: V55 promotes any provider with evidence of use
(real api_key, OAuth token, recent chat usage, or current default model).

Backend
- V55 migration (H2 + MySQL): adds enabled BOOLEAN DEFAULT FALSE on
  mate_model_provider, plus 4 promote-to-true UPDATE rules. Also
  CREATE INDEX idx_message_runtime_provider_time so the 30-day usage
  lookup doesn't full-scan mate_message on heavy users.
- ModelProviderEntity, ProviderInfoDTO: enabled field.
- ModelProviderService:
    * listProviders() now filters WHERE enabled = TRUE — chat path,
      ModelSelector, Settings/Models main grid see only opted-in rows.
    * listCatalog() new — full catalog (enabled + disabled) for the drawer.
    * setEnabled(id, enabled) flips the flag, publishes
      ModelConfigChangedEvent (re-probe via the existing listener), and
      on disable auto-promotes a replacement default model when the
      disabled provider owned the current default. Returns EnableResult
      so the frontend can fire a toast.
    * createCustomProvider sets enabled=true (user just made the row).
- ProviderInitProbe.listConfiguredProviders also filters enabled=true —
  no point probing rows the user can't see.
- ModelConfigController: GET /catalog, POST /{id}/enable, POST /{id}/disable.
- Plugin-registered ChatModels are unaffected — they live in
  pluginChatModels (in-memory map), don't go through DB listProviders,
  so the enabled filter doesn't strand them.

Frontend
- New types: ProviderInfo.enabled, EnableResult.
- New API: catalog / enableProvider / disableProvider.
- New composable useProviderEnablement: catalog ref, drawerOpen,
  togglingId, loadCatalog, openDrawer / closeDrawer, enableProvider,
  disableProvider (fires defaultSwitchedToast on auto-switch).
- AddProviderDrawer.vue: lazy-loaded, reuses DoctorDrawer's Teleport +
  overlay + slide-in panel pattern. Two groups (cloud / local),
  unenabled rows surface to the top of each group, enabled rows show
  an 'Enabled' badge instead of a button. Mobile: full-screen sheet
  that slides up from below.
- ProviderCard: new 'Disable' button with soft-danger styling on
  enabled providers — soft-hide that keeps the config; user can
  re-enable from the drawer.
- Settings/Models index.vue:
    * Two top CTAs: 'Enable Provider' (drawer) and 'Custom' (existing
      custom-create modal) — distinct workflows, both surfaced.
    * Empty state with prominent 'Enable Provider' CTA when zero
      enabled providers — paired with onMounted auto-open of the
      drawer (sessionStorage guard so closing it doesn't bring it
      back on the next route visit in the same session).
    * Deep-link: ?addProvider=1 query forces the drawer open and
      strips itself after, so a back/forward doesn't re-fire the open.
- ModelSelector: when groups.length === 0 and not searching, show
  'No providers configured -> Configure' CTA linking to
  /settings/models?addProvider=1 — the natural flow when a fresh
  user opens chat before configuring anything.
- i18n: 13 new keys per locale (zh-CN + en-US) plus common.close.

Migration safety
- Conservative default policy: only rows with concrete evidence of
  use are auto-enabled; everything else stays hidden. Upgrade users
  may notice unused built-ins disappearing from their dropdown —
  that's the intended cleanup.
- mate_message index added so the 30-day usage rule doesn't full-scan
  on large installations; FlywayRepairConfig handles redeploy idempotency.

Tests
- ModelProviderServiceEnableTest covers all 7 enable/disable branches:
  flag flip + event publish, no-op on already-{enabled,disabled},
  default-switch when disabled provider owned current default,
  no-switch when default belongs elsewhere, no-replacement returns
  unchanged, getDefaultModel exception path, candidates with no
  models are skipped.
- ProviderInitProbeTest: helper provider() now sets enabled=true so
  the new probe filter doesn't strand existing fixtures.
- vip.mate.llm.** suite: 125 tests green. vue-tsc 0 errors. Browser
  page renders with both new buttons + drawer.
2026-04-28 15:03:40 +08:00
matevip
c0c642380a feat(llm): provider liveness model + honor requireApiKey on chat path
Phase 1 of the model-module refactor: combine pool / cooldown / probe-
completion signals into a single Liveness state surfaced through the
provider DTO, so the dropdown stops listing providers that are provably
unreachable. Zero schema change; one PR backend + frontend.

Backend
- Liveness enum with five mutually-exclusive states: LIVE, COOLDOWN,
  REMOVED, UNPROBED, UNCONFIGURED. Computed in ModelProviderService
  from AvailableProviderPool / ProviderHealthTracker / ProviderInitProbe
  snapshots batched once per listProviders() call.
- ProviderInitProbe.hasBeenProbed exposes a monotonic Set so the UI
  can distinguish 'still booting' from 'probed and removed' — without
  it the startup window flashes false REMOVED states.
- ProviderInfoDTO gains liveness + unavailableReason +
  cooldownRemainingMs + lastProbedAtMs. The legacy 'available' boolean
  stays but is now derived from liveness == LIVE so the chat fallback
  walker and the dropdown agree about what's usable.
- ProviderInitProbe injected into ModelProviderService via
  ObjectProvider to break the startup cycle (probe already depends on
  the service).

Frontend
- ProviderInfo type extended with liveness + the three detail fields.
- ModelSelector filters UNCONFIGURED + REMOVED out of the dropdown,
  shows COOLDOWN / UNPROBED with a status dot and dimmed rows that the
  user can still click to override.
- ProviderCard renders a five-state badge driven by liveness instead
  of the old configured + pool-entry combo. Reprobe button now keys
  off liveness in {REMOVED, COOLDOWN}.
- useProviders drops loadProviderPool / providerPool — pool data ships
  inline on each ProviderInfo, saves a round trip per page load and
  keeps a single source of truth.
- i18n: 8 new keys across zh-CN and en-US for liveness labels and the
  cooldown countdown tooltips.

Bonus fix (discovered during verification): AgentGraphBuilder.buildOpenAiApi
hard-required a usable API key on every OpenAI-compat provider, ignoring
the per-provider requireApiKey flag. That bug stranded keyless local
runtimes (LM Studio / MLX / llama.cpp) the moment a user actually
launched them; Ollama only worked by accident because its seed row
carries a placeholder string in api_key. keyRequired now honors
requireApiKey, and Spring AI's NoopApiKey is used when no key is needed
so the Authorization header is omitted entirely.

Test
- ModelProviderServiceLivenessTest covers all five Liveness states +
  the probe-bean-absent fallback branch.
- vip.mate.llm.** suite (118 tests) green; vue-tsc clean.
- End-to-end browser sanity: 27 raw providers reduce to 6 LIVE groups
  in the chat dropdown; LM Studio / MLX / llama.cpp render REMOVED red
  badges with reprobe buttons; cloud providers without keys show
  UNCONFIGURED.
2026-04-28 14:59:11 +08:00
matevip
410c6c28cd feat(deepseek): integrate DeepSeek V4 (flash + pro) with thinking-mode support 2026-04-26 08:34:34 +08:00
matevip
dfb9fc2cac fix(model-catalog): claude-sonnet-4-7 doesn't exist — Sonnet stays at 4.6 2026-04-26 08:34:12 +08:00
matevip
dbdb585eed fix(anthropic): rewrite system field to array to pass OAuth anti-abuse gate 2026-04-26 08:34:12 +08:00
matevip
ed3ff54f0c fix(anthropic): drop (external, cli) UA suffix — it's the anti-abuse fingerprint 2026-04-26 08:34:11 +08:00
matevip
fb4c013ad8 feat(anthropic): surface Claude Code OAuth in admin UI 2026-04-26 08:34:09 +08:00
matevip
a7938b0e68 feat(anthropic): wire Claude Code OAuth into chat model 2026-04-26 08:34:08 +08:00
matevip
8539fb9407 feat(anthropic): Claude Code OAuth credential plumbing 2026-04-26 08:34:08 +08:00
matevip
9187aed273 fix(oauth): support remote-server deployment via MANUAL_PASTE flow 2026-04-26 08:34:08 +08:00