Commit Graph

145 Commits

Author SHA1 Message Date
wizardchen 6fb858105a feat(llm): add prompt-cache observability and wiki cache-friendly prompts
Normalize provider-specific cache counters into TokenUsage, propagate call
purpose and prefix fingerprints through context for logging and Langfuse,
and restructure wiki page-modify prompts with warmup/singleflight to improve
prefix cache hit rates during reduce batches.
2026-07-23 19:17:30 +08:00
wizardchen 146e341748 refactor(http): share SSRF-safe transport across model clients
Extract NewSSRFSafeTransport and NewSSRFSafeHTTPClientWithTransport so
embedding and rerank can reuse one connection pool without allocating a
throwaway transport per client. SSRF dial and redirect policies are
unchanged; each client still keeps its own timeout.
2026-07-23 12:00:05 +08:00
zhangzherui ccf02e1a2a fix(embedding): reuse SSRF-safe HTTP transport 2026-07-23 11:51:54 +08:00
Martín Alcalá Rubí 83da0903b6 fix(rerank): stop sending vLLM truncate_prompt_tokens by default
The generic OpenAI-compatible reranker hardcoded `truncate_prompt_tokens:
511` on every /rerank request. Providers that honor this vLLM-specific
field (e.g. SiliconFlow serving Qwen3-Reranker) keep only the last 511
tokens of the templated rerank prompt, cutting the instruction and query
off any document longer than ~511 tokens. Every relevance score then
collapses to near zero (~0.0067 instead of ~0.998), falls below the score
threshold, and knowledge-base search returns empty results. A direct curl
without the field returns the correct scores.

WeKnora already removed this exact hardcoded parameter from the embedding
path in #960; the rerank path was never fixed.

- Add `omitempty` to `truncate_prompt_tokens` and `additional_data` so
  empty optional fields are not sent.
- Make truncation opt-in via `extra_config.truncate_prompt_tokens`
  (validated > 0 at construction), preserving an escape hatch for
  self-hosted vLLM deployments with small-context rerankers.
- Add regression tests reproducing the collapsed-score symptom and the
  opt-in path.

Fixes #2143
2026-07-21 12:12:56 +08:00
wizardchen b1b42abe23 fix(rerank): batch Volcengine rerank requests over API document limit
Split oversized candidate sets into concurrent batches instead of failing when retrieval exceeds the VikingDB rerank per-request cap, and align ko/ru locale hints with zh-CN.
2026-07-20 20:07:21 +08:00
KKtwo 3011f132e9 feat: support Volcengine rerank 2026-07-20 19:58:48 +08:00
wizardchen 6d14cc3536 feat(resource): add stable resource registry with LLM-context alias compaction
Introduce resource://<handle> as the stable application identity for stored
objects, decoupling API responses, persisted rich text, and LLM prompts from
physical storage paths. Adds a resource catalog service (register/resolve/bind,
revocable expiring access grants), a request-local alias registry that compacts
resource references while in the LLM context and restores them (including across
streaming chunks), plus router, IM, chat-pipeline, and multimodal integration.

Includes schema migrations (resources, resource_bindings, resource_access_grants)
for sqlite, and hardens URL/output security handling.
2026-07-15 17:02:43 +08:00
Thibault Jaigu 35b92f73aa feat(provider): add Requesty as an OpenAI-compatible model provider
Requesty is an OpenAI-compatible LLM gateway that uses the same provider/model
naming convention as OpenRouter, so this mirrors the existing OpenRouter provider.

- internal/models/provider/requesty.go: RequestyProvider mirroring openrouter.go
  (base URL https://router.requesty.ai/v1, REQUESTY_API_KEY, RequiresAuth)
- provider.go: ProviderRequesty const + AllProviders() entry + DetectProvider case
- internal/types/model.go + client/model.go: ModelSourceRequesty
- provider_test.go: TestRequestyProviderValidation + DetectProvider case
- frontend model editor + i18n (en/zh/ko/ru) + docs/BUILTIN_MODELS.md provider list
- docs/swagger.{json,yaml} + docs.go: requesty enum value (hand-edited to match; these
  are normally swag-generated)

Verified: go build/vet/test pass; live call to https://router.requesty.ai/v1 succeeds.
Signed-off-by: Thibault Jaigu <thibault.jaigu@gmail.com>
2026-07-13 17:03:23 +08:00
wizardchen b9cccc7bee feat(system): expose model concurrency stats on runtime queues dashboard
Surface per-model active/waiting/limit from the background task limiter so admins can distinguish task scheduling from model-service throttling, with full i18n coverage.
2026-07-11 07:17:28 +08:00
wizardchen 4404ea74e3 feat(models): implement model.max_concurrency for background tasks
Add a new `max_concurrency` setting to the model configuration, allowing for fine-tuned control over the maximum number of concurrent background tasks (ingestion/enrichment) per model. This update includes localization support across multiple languages and integrates the setting into the system's concurrency governor, enabling immediate application of changes without requiring a service restart.
2026-07-09 20:40:51 +08:00
wizardchen 20256287ec feat(models): add max concurrency setting for background tasks
Introduce a new `max_concurrency` parameter to the model configuration, allowing control over the maximum number of concurrent background tasks (ingestion/enrichment) per model. This feature is applicable to chat, embedding, and VLM models, with a value of 0 or undefined falling back to the global default. Update UI components and localization files to support this new setting, ensuring a seamless user experience.
2026-07-09 20:40:51 +08:00
wizardchen 200c344065 feat(models): cap background embedding concurrency per model
Extend the per-model background concurrency governor to embedding, the
highest-volume background model call (document ingestion vectorises every
chunk, so a batch upload can burst the whole worker pool against one provider).

The wrapper sits innermost (below debug/langfuse) because BatchEmbedWithPool
fans a batch out into per-sub-batch BatchEmbed calls via the pooler, and the
pooler invokes BatchEmbed on whichever Embedder is threaded down as `model`;
sitting innermost routes those per-sub-batch provider round-trips through Gate
so the semaphore bounds real concurrent calls rather than one coarse per-doc
unit. Only background (asynq) calls are throttled; interactive query embedding
is never gated.
2026-07-09 20:40:51 +08:00
wizardchen 98ad41cda4 fix(models): harden background concurrency governor
Follow-up hardening on the per-model background concurrency governor:

- limiter: heartbeat now refreshes the ZSET key's PEXPIRE alongside the
  lease score, so a semaphore that stays saturated with no slot turnover
  can't let the whole key expire (which would drop every lease and admit
  over the limit); reuse one poll timer instead of allocating time.After
  each iteration under contention.
- governor: extract the process-wide governor + Gate helper into the
  limiter package so chat and vlm share one limiter/limit without
  importing each other; add an in-process localLimiter for Lite mode.
- vlm: gate background multimodal (VLM) calls through the same governor;
  previously only chat was throttled.
- chat: ChatStream releases its slot when the consumer abandons the
  stream and cancels ctx (select on ctx.Done, drain inner), fixing a
  slot/goroutine leak.
- container/sync_task: install the local governor in Lite mode and tag
  SyncTaskExecutor runs as background; honour model.max_concurrency=0 as
  an explicit disable instead of falling back to the default.
- tests: cover the governor gating, local limiter, and stream release.
2026-07-09 20:40:51 +08:00
wizardchen 63198c5d9f feat(models): cap background LLM concurrency per model
Background ingestion/enrichment stages (summary, question, graph,
multimodal) all target the same chat model, and asynq queue weights are
scheduling priority rather than throttling. A large upload could therefore
burst up to the entire worker pool of concurrent chat calls against a
single provider, unbounded across replicas, with no model-side rate limit.

Govern concurrency at the chat-client layer (the one place that sees every
task type), keyed by model ID:

- internal/models/limiter: Redis ZSET + Lua distributed semaphore with a
  lease heartbeat, expired-lease reclaim for crashed holders, and fail-open
  on any backend error or context cancellation.
- chat: concurrencyChat decorator (outermost in NewChat) acquires a
  per-model slot only for background tasks; interactive chat is never gated.
- router/task: asynq middleware tags every worker execution as background.
- container: wire the Redis limiter with WEKNORA_MODEL_MAX_CONCURRENCY
  (model.max_concurrency), default 8; enabled only when Redis is present.

Tested with miniredis: concurrency cap, expired-lease reclaim, per-key
isolation, and all fail-open paths (nil client, backend down, cancelled ctx).
2026-07-09 20:40:51 +08:00
wizardchen 1661827d6e fix(security): harden model client redirects 2026-07-02 19:15:46 +08:00
wizardchen 663d13eb58 fix(security): harden outbound requests and file previews 2026-07-02 19:15:46 +08:00
lawrence fdfc7297de fix(vlm): allow configurable temperature for VLM models
Some VLM models (e.g. certain vision models) only accept temperature=1.
Previously temperature was hardcoded to 0.1. Now reads from model's
ExtraConfig["temperature"] with fallback to default 0.1.

Usage: set ExtraConfig.temperature=1 in the VLM model configuration.
2026-07-01 12:20:09 +08:00
wizardchen 6b60d292f8 fix(embedding): add SSRF validation and safe HTTP client for embedders
Align embedding outbound requests with chat module defenses by validating
base URLs at construction time and using SSRFSafeDialContext with redirect checks.
2026-06-26 22:41:04 +08:00
wizardchen 2c44a41f2a feat(models): add model test debugger for saved model configs
Let admins send real requests against configured chat, embedding, rerank,
VLM, and ASR models from settings, with thinking controls shown only when
the model supports them.
2026-06-25 17:05:33 +08:00
wizardchen de525a4f02 feat(observability): enhance Langfuse retrieve and rerank tracing
Add shared retrieval observation helpers, expand rerank generation output
with previews and score stats, and enrich retrieve/rerank pipeline spans.
2026-06-16 01:09:23 +08:00
wizardchen 53e0333a53 Add embedding dimension override support 2026-06-15 19:18:19 +08:00
MaoMeng cf0a64b88e fix:dimensions not sent in embedding API requests (#1654)
fix:dimensions not sent in embedding API requests
2026-06-15 17:32:23 +08:00
Yeongpil Yoon c6fc315042 fix(chat): preserve provider tool-call metadata 2026-06-09 20:55:20 +08:00
wizardchen 28de82a92e refactor(chat): improve model name sanitization and path validation
- Updated the sanitization logic in `newStreamPacketDumper` to exclude the '.' character, enhancing security against path traversal vulnerabilities.
- Added checks to ensure the cleaned file path remains within the specified directory, preventing unauthorized access to the filesystem.
- Improved overall robustness of file handling by enforcing stricter path validation rules.
2026-06-05 19:35:02 +08:00
wizardchen e2a41ecf0a feat(thinkingControl): implement thinking mode configuration in model editor
- Added a new thinking mode control field in the ModelEditorDialog, allowing users to specify how the agent's thinking mode is formatted for API requests.
- Introduced localization strings for thinking mode labels and descriptions in English, Korean, Russian, and Chinese, enhancing user experience across multiple languages.
- Updated the model form data structure to include thinking control settings, ensuring proper handling during model edits and saves.
- Implemented utility functions for resolving and defaulting thinking control values based on provider and model, improving backend integration.
- Added tests for the new thinking control functionality to ensure reliability and correctness in handling user configurations.
2026-06-05 14:56:48 +08:00
wizardchen dda16436d0 feat(lkeap): integrate Tencent Cloud LKEAP Rerank support
- Added support for Tencent Cloud LKEAP Rerank, including new configuration options for SecretId and SecretKey.
- Updated frontend components to handle LKEAP-specific fields and enhance user experience with appropriate labels and placeholders.
- Enhanced backend logic to manage LKEAP credentials and integrate with the new rerank API.
- Improved localization for LKEAP-related terms across multiple languages, ensuring clarity and consistency in user-facing messages.
- Introduced tests for LKEAP Reranker functionality to ensure reliability and correctness in handling requests.
2026-06-04 17:22:18 +08:00
wizardchen d38a6efd87 refactor(agent): remove final_answer tool references and update related logic
- Eliminated all instances of the final_answer tool from the agent's configuration and logic, transitioning to a model where the agent concludes responses with plain text instead.
- Updated event handling and response analysis to reflect this change, ensuring that the agent's output is now directly delivered as text without relying on a dedicated final answer tool.
- Adjusted localization strings across multiple languages to remove references to final_answer, enhancing clarity and consistency in user-facing messages.
- Improved the overall structure of the agent's response handling to streamline the process and reduce potential confusion regarding tool usage.

refactor(agent): streamline answer handling and preamble logic

- Updated the agent's response handling to treat any text streamed before a tool call as a preamble, ensuring it is not included in the final answer.
- Removed the superseded marker logic from the event emission process, simplifying the flow of answer content and enhancing clarity in the UI.
- Adjusted the event handling to ensure that preamble segments are correctly retracted and relocated into the steps tree, improving the overall user experience.
- Enhanced the agent's internal logic to better manage answer segments and their states during streaming, leading to a more coherent response structure.
2026-06-04 15:48:04 +08:00
wizardchen 1c9c17a7cf feat(tests): add unit tests for Langfuse wrapper functions
- Introduced a new test file for the Langfuse wrapper, implementing tests for the `buildLangfuseGenerationOutput` and `buildLangfuseMessages` functions.
- Validated the output structure for various scenarios, ensuring correct handling of content, tool calls, finish reasons, and reasoning content.
- Enhanced test coverage for the Langfuse functionality, improving reliability and maintainability of the codebase.
2026-06-04 15:48:04 +08:00
wizardchen 7b1bb1054f feat(docreader): speed up scanned-PDF parsing, stream image results, isolate heavy async queues
Large scanned PDFs (hundreds of pages) were slow and fragile end-to-end.
This change addresses the parse, transport, and task-scheduling layers:

docreader (parse + transport):
- Parallelize per-page scanned rendering across processes (forkserver/fork),
  with serial fallback. ~4-7x faster on large scanned PDFs; pdfium is not
  thread-safe so we fan out across processes. Configurable via
  DOCREADER_PDF_RENDER_PARALLELISM.
- Add server-streaming ReadStream RPC: emit one meta frame then one frame per
  image, so documents with many page images are no longer capped by the unary
  gRPC message-size limit (a 874-page PDF produced ~193MiB of images, far over
  the 50MB cap) and memory is bounded on both ends. Unary Read is kept for
  backward compatibility; the Go production reader switches to ReadStream.

VLM:
- Make the VLM HTTP timeout configurable (VLM_HTTP_TIMEOUT_SECONDS) and raise
  the default 90s -> 180s so dense scanned-page OCR does not time out with
  "context deadline exceeded".

Async task queues:
- Isolate high-volume, model-heavy fan-out tasks into dedicated asynq queues so
  a single large document cannot saturate the shared worker pool and block
  user-facing document parsing:
    image:multimodal  -> "multimodal"
    chunk:extract     -> "graph"
    question:generation -> "question"
- Register the new queues in the server weight map and the cancel inspector's
  scanned-queue set (so cancelling a knowledge still purges its pending tasks).
2026-06-03 12:29:13 +08:00
octo-patch f22995f835 feat: add MiniMax-M3 to provider model list
- Add MiniMax-M3 to the MiniMax provider description across i18n locales
- Remove deprecated MiniMax-M2.5 reference
- Retain MiniMax-M2.7 and MiniMax-M2.7-highspeed as supported models

MiniMax-M3 is the latest flagship model from MiniMax with extended context
and stronger reasoning capabilities. Users can now see it in the provider
selection UI when configuring a new Minimax model.
2026-06-02 21:38:30 +08:00
Yeongpil Yoon 8f8a276120 fix(embedding): support native Gemini embeddings 2026-06-02 21:37:58 +08:00
wizardchen 482686d17e feat(chat): implement local image resolver for multimodal chat
- Added a LocalImageResolver function to resolve local storage URLs to their byte data using the tenant's storage configuration.
- Integrated the resolver into the container setup to support reading local images in multimodal chat scenarios.
- Enhanced the handling of local storage paths to correctly parse tenant IDs and retrieve files based on the configured storage settings.
2026-06-01 20:54:37 +08:00
young1lin 29820e4cac docs(chat): clarify cached-token semantics for explicit-cache providers
`cached_tokens` is reported by every OpenAI-compatible provider that
supports prompt caching, but how it becomes non-zero differs by mode:

- Implicit caching (OpenAI, Azure OpenAI, DeepSeek, …) populates the
  field automatically whenever a prompt prefix matches a previous
  request within the provider's cache TTL. No client-side opt-in.

- Explicit caching (Qwen on Aliyun, Anthropic Claude, …) only
  populates the field after the caller attaches `cache_control:
  {"type": "ephemeral"}` to the relevant message / content block.
  Until that opt-in is applied upstream of the request, the field
  stays zero even when the prefix is otherwise byte-stable.

Without this distinction documented, the previous commit reads as if
`TokenUsage.CachedTokens` will show non-zero values for Qwen / Claude
once this PR lands — which is not the case. The plumbing here is a
prerequisite (stable prefix via sorted tools) and a meter (visibility
of the field), but the explicit-cache opt-in itself is out of scope
and lives elsewhere.

Document this on `TokenUsage.CachedTokens` and the `cachedTokens`
helper so callers do not mistake observability for activation.
2026-05-25 16:47:14 +08:00
young1lin 582f7b3056 feat(chat): surface cached prompt tokens from upstream usage
OpenAI-compatible providers (Qwen, DeepSeek, OpenAI, Azure, etc.) report
prompt-cache hits in `usage.prompt_tokens_details.cached_tokens`. This
value was being read by go-openai but dropped at the WeKnora boundary,
so there was no way to tell whether prompt caching was actually working.

This change plumbs the field end-to-end:

- `types.TokenUsage.CachedTokens` (json:"cached_tokens,omitempty") —
  zero-values are omitted so payloads stay quiet for providers that
  never report cache hits.
- `cachedTokens` helper in remote_api.go guards against nil
  `PromptTokensDetails` (Ollama and older OpenAI-compat backends omit
  the details block entirely).
- All three response-parsing paths populate it:
    * `parseCompletionResponse` (non-streaming)
    * `processStream` (SDK streaming)
    * `processRawHTTPStream` (raw-HTTP streaming, used when callers
      need to inject custom fields like `cache_control`)
- The five `[LLM Usage]` log lines now print `cached_tokens=%d` so
  cache hit rate is visible in `journalctl` / log tail without going
  through metrics.

Together with the deterministic tool ordering from the previous commit,
this makes Qwen explicit caching observable: a warmed prefix should
show `cached_tokens` ≈ system + tools token count (typically several
thousand) on subsequent requests within the 5-minute TTL.

Tests:
- `TestCachedTokensHelper` — nil safety + round-trip
- `TestParseCompletionResponse_CachedTokens` — populated + missing
  details paths through `parseCompletionResponse`
- `TestTokenUsage_CachedTokensJSONOmitempty` — zero is omitted,
  non-zero is emitted
2026-05-25 16:47:14 +08:00
yuheng.huang 28e595734f fix(moonshot): pin temperature=1 for models that reject other values moonshot-v1-* and kimi-k2.5/k2.6 reject any temperature ≠ 1 with HTTP 400. Detect these models in BuildChatCompletionRequest and force Temperature=1 while leaving kimi-k2/k2-turbo/k2-thinking unaffected. 2026-05-18 21:28:40 +08:00
wizardchen 5141a1230c fix(embedding): repair broken comment in Zhipu embedder
A stray `/` line split what should have been `//`, which prevented `go build` from succeeding in CI.
2026-05-17 21:31:54 +08:00
Wellbek 4fef6826cf feat(embedding): add Zhipu AI embedder implementation 2026-05-17 20:14:51 +08:00
wizardchen b00bc84f35 fix(agent): pass reasoning_content back to providers that require it
MiMo and DeepSeek V3.2/V4 reject multi-turn requests in thinking mode
when the prior assistant message lacks reasoning_content with HTTP 400:
"The reasoning_content in the thinking mode must be passed back to the API."

Agent's ReAct loop is the worst-case scenario — every round produces tool
calls, exactly the case DeepSeek's docs specify reasoning_content MUST
participate in subsequent context.

Plumb reasoning_content through the full assistant-message round-trip:
- chat.Message / types.ChatResponse / types.AgentStep gain a
  reasoning_content field (AgentStep persists via the existing
  Message.AgentSteps jsonb column, no migration needed).
- streamLLMToEventBus accumulates reasoning chunks into
  result.ReasoningContent and surfaces it on the round's ChatResponse.
- engine.runReActIteration writes it onto AgentStep so cross-turn replay
  preserves it.
- observe.appendToolResults attaches it to the same-turn assistant
  message; agent_history.buildAssistantHistoryMessages does the same on
  cross-turn replay.
- RemoteAPIChat.ConvertMessages forwards it on assistant turns to
  openai.ChatCompletionMessage.ReasoningContent (already supported by
  go-openai); providers that don't recognize the field ignore it.

Tests cover the three boundaries: ConvertMessages serializes it for
upstream, appendToolResults preserves it within the same turn, and
buildAssistantHistoryMessages replays it across turns.

Scope is intentionally limited to Agent mode — KnowledgeQA's chat
pipeline and Anthropic's signed thinking_blocks are separate fixes that
require schema changes (rendered_content / thinking_blocks columns).

Fixes #1302
2026-05-13 12:29:10 +08:00
wizardchen 9e08ab7302 fix(chat): map MaxTokens to MaxCompletionTokens for GPT-5/o-series
OpenAI's GPT-5 series and o-series reasoning models (o1/o3/o4-mini) no
longer accept `max_tokens` and reject non-default sampling params
(`temperature`, `top_p`, `frequency_penalty`, `presence_penalty`).
Azure OpenAI propagates the same constraint, returning HTTP 400 with
"this model is not supported MaxTokens, please use MaxCompletionTokens".

Lower the compatibility shim into `RemoteAPIChat.BuildChatCompletionRequest`
(the single OpenAI-protocol egress) so every internal caller keeps using
`MaxTokens` uniformly:

- Add `provider.IsOpenAIReasoningOrGPT5Model` with precise prefix matching
  (covers `gpt-5*`, `o1`/`o1-*`, `o3`/`o3-*`, `o4`/`o4-*`; rejects
  `olympus-1`, `openai-*`, `o3xtra`, etc.).
- When the provider is `openai`/`azure_openai` and the model matches, map
  `MaxTokens` to `MaxCompletionTokens` (explicit `MaxCompletionTokens`
  wins) and skip the unsupported sampling fields; `omitempty` keeps them
  off the wire.
- Behavior is unchanged for gpt-4o, gpt-4, and all other providers.

Add unit tests for the matcher and for the request-building path
(Azure gpt-5.2, OpenAI gpt-5/o1-mini/o3/o4-mini, plus negative cases).

Fixes #1283
2026-05-13 00:40:35 +08:00
langcaiye 4d2b8707ff fix: support Anthropic gateway streaming 2026-05-13 00:14:23 +08:00
langcaiye 42d98261e7 feat: support Anthropic chat provider 2026-05-12 17:33:16 +08:00
wizardchen 24a010ff59 fix(chat): reduce default LLM timeout values to 300s for chat and 600s for stream 2026-05-10 00:16:12 +08:00
wizardchen e83378ecd1 fix(chat): 调大 LLM 兜底超时默认值(chat 600s / stream 1800s) 2026-05-10 00:16:12 +08:00
wizardchen 386df92582 fix(chat): LLM 兜底超时仅在上层无 deadline 时生效,并修复 ChatStream raw HTTP 路径的 cancel 泄漏
PR #1238 引入的 context.WithTimeout 会把上层 ctx 的 deadline 强制截断到
默认 120s/300s,导致调用方无法设置更长的合理超时(例如长推理模型场景)。
同时 ChatStream 在走 chatStreamWithRawHTTP 早返回路径时丢弃了 cancel
函数,timeoutCtx 要等 deadline 到期才释放,go vet/lostcancel 也会告警。

本次修复:
- 新增 withLLMTimeout 辅助函数:仅在上层 ctx 没有 deadline 时附加默认
  超时;上层若已显式设置 deadline(无论比默认更短还是更长),都原样
  尊重,把超时的最终决定权交还给调用方。
- 默认值通过环境变量可覆盖:
  - WEKNORA_LLM_CHAT_TIMEOUT_SECONDS    (默认 120s)
  - WEKNORA_LLM_STREAM_TIMEOUT_SECONDS  (默认 300s)
- 修复 ChatStream raw HTTP 早返回路径未调用 cancel 的泄漏:新增
  wrapStreamCancel,在底层 channel 关闭后统一执行 cancel。
- 补充单测覆盖三种 deadline 场景与环境变量解析。
2026-05-10 00:16:12 +08:00
杨明康 6b8dbaa25b fix(chat): 为 LLM 非流式/流式调用添加超时保护,防止 worker 被 hung 请求永久阻塞
IM 渠道(企微等)的消息处理走 KnowledgeQA 管道,该管道的 LLM 调用此前
未设置任何超时,依赖 context cancellation 控制。然而 QA 管道传入的 context
本身无 deadline,一旦上游 LLM API(如 mimo-v2.5-pro)的网络请求 hang 在
TCP 层面(连接未断但无数据返回),worker goroutine 会永久阻塞在
CreateChatCompletion / CreateChatCompletionStream 上。

随着时间推移,多个 worker 陆续卡死,队列中所有 worker 耗尽后机器人对任何
消息都不再回复。

本提交在 RemoteAPIChat.Chat 和 RemoteAPIChat.ChatStream 两个入口方法中
统一添加 context.WithTimeout:
- 非流式调用(Chat):120s 超时
- 流式调用(ChatStream):300s 超时

如果上层调用方已设置更短的 deadline,WithTimeout 自动遵循更短的那个,
不会覆盖已有的合理超时设置。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 00:03:53 +08:00
wizardchen ff950ef249 fix(ollama): adapt to ToolCallFunctionArguments struct API
The ollama Go SDK changed ToolCallFunctionArguments from
map[string]any to a struct backed by an ordered map. Use ToMap() for
read access and NewToolCallFunctionArguments()+UnmarshalJSON for
construction so the build matches the upgraded SDK.
2026-05-09 13:58:55 +08:00
wizardchen 92e02c1d49 chore(llm): add fire-once OpenAI-layer stream diagnostic logs
Add per-stream once-only logs at the OpenAI-protocol layer so we can
triage streaming behavior (natural-stop vs tool-call, TTFC, ordering of
reasoning/content/tool_calls) without grepping through every delta.

streamState now tracks:
  - streamStartedAt    : baseline for elapsed_ms on each fire-once log
  - firstContentSeen   : delta.Content first appearance
  - firstReasoningSeen : reasoning_content first appearance
  - firstToolCallSeen / noToolCallStopLogged (existing flags retained)

Logs emitted at most once per stream:
  [LLM Stream] First reasoning_content at OpenAI layer (len, preview, elapsed_ms)
  [LLM Stream] First delta.Content at OpenAI layer (len, preview, tool_call_seen, thinking_seen, elapsed_ms)
  [LLM Stream] First tool_calls delta at OpenAI layer (count, first_id, first_name, first_content_seen, thinking_seen, elapsed_ms)
  [LLM Stream] Natural-stop at OpenAI layer (finish=stop, tool_calls field never observed, thinking_seen, first_content_seen, elapsed_ms)

Together with the existing agent-layer "Natural-stop candidate detected"
log this lets a single grep reconstruct the temporal layout of one stream
(reasoning -> content -> tool_call OR natural-stop) and tell apart:
  (A) tool_calls field truly absent (real natural-stop)
  (B) tool_calls field arrived but high-level marker not yet emitted

Pure logging change; no behavior change. Previews use the existing
truncateForDebug helper (rune-safe, capped at 80 chars).
2026-05-07 18:28:28 +08:00
toy0116 8955bf1aa8 fix(embedding): OpenAIEmbedder.doRequestWithRetry shadows err · returns (nil, nil) on connection failure · caller nil-derefs and SIGSEGVs
Reproduce:

  1. Run WeKnora pointing OPENAI_BASE_URL at any OpenAI-compatible
     embedding endpoint.
  2. Stop that endpoint.
  3. Issue any RAG-mode chat / knowledge_search query — the pipeline
     reaches chunk_search_parallel → knowledgeBaseService.GetQueryEmbedding
     → OpenAIEmbedder.Embed → OpenAIEmbedder.BatchEmbed.
  4. Observe the entire WeKnora process SIGSEGV instead of returning a
     graceful 5xx.

Stack (real, observed in production):

  panic: runtime error: invalid memory address or nil pointer dereference
  [signal SIGSEGV: segmentation violation code=0x2 addr=0x40]

  internal/models/embedding/openai.go:195 +0x458   BatchEmbed
  internal/models/embedding/openai.go:93  +0xb0    Embed
  internal/application/service/knowledgebase_search.go:35
                                                   GetQueryEmbedding
  internal/application/service/chat_pipeline/search.go:371
                                                   searchByTargets.func1

Root cause — variable shadowing in doRequestWithRetry:

  func (e *OpenAIEmbedder) doRequestWithRetry(...) (*http.Response, error) {
      var resp *http.Response
      var err error                                        // outer err
      ...
      for i := 0; i <= e.maxRetries; i++ {
          ...
          req, err := http.NewRequestWithContext(...)      // ← `:=` shadows
                                                           //   outer `err`
                                                           //   because `req`
                                                           //   is new in
                                                           //   the loop scope
          if err != nil { ... continue }
          ...
          resp, err = e.httpClient.Do(req)                 // writes to the
                                                           // shadowed
                                                           // loop-local err
          if err == nil { return resp, nil }
          logger.Errorf("attempt %d failed: %v", i+1, err) // logs the inner err
      }
      return nil, err                                      // returns OUTER
                                                           // err — never set
  }

When httpClient.Do returns connection errors on every attempt, the loop
logs them faithfully but the outer err stays nil. The function returns
(nil, nil). Caller in BatchEmbed reads:

  resp, err := e.doRequestWithRetry(ctx, jsonData)
  if err != nil { return nil, ... }       // skipped (err is nil)
  if resp.Body != nil {                   // ← line 195 · resp itself is nil
      defer resp.Body.Close()             //   nil pointer dereference
  }                                       //   → SIGSEGV → process dies

Fix — declare `req` separately so the shared `err` variable is not shadowed:

  var req *http.Request
  req, err = http.NewRequestWithContext(...)               // outer err

After the fix, when all retries fail, doRequestWithRetry returns the real
error (e.g. `dial tcp 127.0.0.1:11434: connect: connection refused`),
the caller propagates it as a regular error, the chat pipeline returns a
5xx to the client, and the process keeps running.

Verified by stopping the embedding upstream while WeKnora was serving
queries · process stayed up · returned graceful errors · resumed normal
operation when the upstream came back. Without the patch the same
sequence killed the process within one query.
2026-05-04 23:18:42 +08:00
wizardchen 492e92580b feat(observability): integrate Langfuse for LLM token tracking and tracing
Closes #620 #497. Add opt-in Langfuse observability covering all five
model types (chat, embedding, rerank, VLM, ASR) with HTTP-request-scoped
traces and Docker Compose support (both cloud and self-hosted).

Core package internal/tracing/langfuse:
- HTTP client with batched async ingestion (non-blocking in request path)
- Sampling, environment / release tagging, and graceful fallback when
  LANGFUSE_* env vars are absent (wrappers become no-ops)
- Gin middleware opens one trace per traced request and finishes it after
  the handler chain returns, attaching method / path / user / session
- Trace context is stored under a typed key exported from internal/types
  so logger.CloneContext can preserve it across handler / goroutine
  boundaries (otherwise each LLM call auto-created an orphan trace,
  fragmenting one request into many)

Per-model generation wrappers (opt-in via NewChat/NewEmbedder/...):
- chat: captures prompt, streaming output, token usage + TTFT
- embedding: approximates tokens when the provider omits usage
- rerank: previews query/docs, summarizes results to keep payload small
- vlm: records image count and total bytes, never uploads raw pixels
- asr: records file size and audio duration, never uploads audio bytes

Async title generation (GenerateTitleAsync) now forwards the trace key
into the goroutine so title calls appear under the parent chat trace.

Docker Compose:
- LANGFUSE_* env passthrough on the `app` service for cloud deployments
- Optional `langfuse` profile spins up a self-hosted Langfuse stack that
  reuses WeKnora's existing PostgreSQL (separate database via an idempotent
  init container that fixes ICU collation drift) and Redis (separate DB
  number), adding only ClickHouse, MinIO, web and worker containers
- web/worker entrypoints URL-encode DB_PASSWORD / REDIS_PASSWORD at start
  to avoid Prisma P1013 when passwords contain @ / # / etc.

Docs: docs/Langfuse集成.md covers cloud vs self-hosted, per-model usage
strategy, code map, and resource footprint.
2026-04-24 10:29:19 +08:00
wizardchen fd3e2992b1 feat(models): support custom HTTP headers across all remote model calls
Introduce per-model `custom_headers` config (similar to OpenAI Python SDK's
`extra_headers`) so users can inject gateway auth tokens, trace IDs, etc.
into every outbound API request for chat / embedding / rerank / VLLM / ASR
models. Reserved headers like Authorization / Content-Type are always
preserved to avoid breaking auth or signing flows.

Along the way, unify production and "test connection" paths onto a single
`ConfigFromModel(*types.Model, appID, appSecret)` constructor in each
model package. Both `service.modelService.GetXxxModel` and the four
`handler.initialization.Check*Model` / `TestEmbeddingModel` endpoints now
go through the same field mapping, so new parameters only need to be
added in one place.

Backend:
- Add `CustomHeaders map[string]string` to `types.ModelParameters` with
  JSON/YAML `omitempty` tags for forward compatibility.
- New `internal/utils/extraheaders.go` providing `ApplyCustomHeaders`
  (for hand-rolled HTTP paths) and `WrapHTTPClientWithHeaders` /
  `CustomHeadersRoundTripper` (for SDK paths like go-openai). Reserved
  headers (Authorization, api-key, Content-Type, Accept, Host,
  Content-Length, User-Agent) are filtered out.
- Inject headers in every remote model path: chat (SDK + raw HTTP),
  embedding (openai/aliyun/jina/volcengine/nvidia/azure_openai), rerank
  (remote_api/aliyun/jina/nvidia/zhipu — via shared `customHeaderSetter`
  interface), VLM and ASR (via http.Client wrapping).
- Add `ConfigFromModel` to each of chat/embedding/rerank/vlm/asr,
  consolidating field mapping (ExtraConfig, CustomHeaders, InterfaceType
  defaulting, WeKnoraCloud credentials, etc.) and cover it with
  dedicated unit tests per package.
- Refactor `service/model.go`: replace ~100 lines of hand-written Config
  literals with one-line `ConfigFromModel` calls; drop the now-unused
  `stringMapToAnyMap` helper.
- Refactor `handler/initialization.go` test-connection endpoints: merge
  four ad-hoc request structs into one `ModelTestRequest`, extract
  `buildTestModel` and `resolveTenantWeKnoraCloudCreds` helpers, and
  route all four endpoints through `ConfigFromModel` + `NewXxx` so the
  test path is now behaviorally identical to production.

Frontend:
- Add `custom_headers` to the `ModelConfig` API type.
- `ModelEditorDialog.vue`: new Key-Value header editor (add/remove rows),
  auto-converts map <-> array when loading/saving, and passes the
  current header set to the `Test Connection` button so the preview
  reflects exactly what production will send.
- `ModelSettings.vue`: serialize the header array back into a map for
  the backend (drops empty rows).
- i18n: add custom-header labels / descriptions / placeholders to
  zh-CN, en-US, ru-RU, ko-KR.

Tests:
- `internal/utils/extraheaders_test.go` covers reserved-header filtering
  and round-tripper wrapping (including nil-client fallback).
- New `config_from_model_test.go` in chat / embedding / rerank / vlm /
  asr verifies all fields (CustomHeaders, ExtraConfig, InterfaceType
  defaulting, AppID/AppSecret) are propagated end-to-end.
2026-04-23 23:36:40 +08:00