diff --git a/internal/runtime/embedding/api.go b/internal/runtime/embedding/api.go index 41bde16..9986e7b 100644 --- a/internal/runtime/embedding/api.go +++ b/internal/runtime/embedding/api.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "io" - "log/slog" "net/http" "strings" "sync" @@ -94,7 +93,6 @@ var ErrNoAPIURL = errors.New("embedding: api url is empty") type APIEmbedder struct { cfg config.EmbeddingConfig client *http.Client - log *slog.Logger // warmupLeft 是**一次性**冷启动放宽的开关:为真时下一个请求用 warmupTimeout, // 取走即置假(CompareAndSwap ⇒ 并发下也只有一个请求能拿到)。 // 它**不是**「未成功就一直放宽」—— 失败也照样花掉这一次,因此服务挂死时 @@ -127,24 +125,17 @@ func WithRoundTripper(rt http.RoundTripper) APIOption { } } -// WithLogger sets the logger used for embed failures. A nil logger is ignored -// and the default discard logger is kept. -func WithLogger(l *slog.Logger) APIOption { - return func(e *APIEmbedder) { - if l != nil { - e.log = l - } - } -} - // NewAPI builds an APIEmbedder from cfg using the default HTTP client. -// 单次超时不可由选项更换:它是 budget.go 那条推演的输入,批次上限与它同源, -// 因此**只允许改常量**(改了之后批次上限自动跟着改,测试的编译期/常量断言立刻复核)。 +// +// 本包**不记日志**(没有任何 logger 缝):失败一律沿错误返回,由四个调用方 +// (审查召回 / 整理召回 / 向量索引写入 / 检索)各自带 source 记账 —— 它们本来 +// 就会把错误原文写进各自的 WARN,因此这里再记一条只会是重复的第二份。 +// 单次超时也不可由选项更换:它是 budget.go 那条推演的输入,批次上限与它同源, +// 因此**只允许改常量**(改了之后批次上限自动跟着改,测试的常量断言立刻复核)。 func NewAPI(cfg config.EmbeddingConfig, opts ...APIOption) *APIEmbedder { e := &APIEmbedder{ cfg: cfg, client: defaultHTTPClient(), - log: slog.New(slog.NewTextHandler(io.Discard, nil)), } // 冷启动放宽是一个**一次性**许可:见 APIEmbedder.warmupLeft。 e.warmupLeft.Store(true) @@ -200,21 +191,18 @@ func (e *APIEmbedder) Embed(ctx context.Context, texts []string) ([]Vector, erro // (成功、失败都算花掉)。因此: // - 本进程的第一个请求最多等 warmupTimeout(冷启动), // - 从第二个请求起一律 DefaultRequestTimeout —— 服务挂死时不会每次都慢。 -// -// 返回值 relaxed 表示这一次是不是拿到了放宽,供失败时留痕。 -func (e *APIEmbedder) callTimeout() (timeout time.Duration, relaxed bool) { +func (e *APIEmbedder) callTimeout() time.Duration { if e.warmupLeft.CompareAndSwap(true, false) { - return warmupTimeout, true + return warmupTimeout } - return DefaultRequestTimeout, false + return DefaultRequestTimeout } // embedChunk 发**一次** /embeddings 请求。chunk 必须已经落在 batchLimitsFor 推出来 // 的边界内(Embed 负责切),因此它的耗时上界不超过单次超时的一半。 func (e *APIEmbedder) embedChunk(ctx context.Context, texts []string) ([]Vector, error) { // 先取超时(取走即花掉那次放宽),再建 ctx:放宽许可与这次请求一一对应。 - timeout, relaxed := e.callTimeout() - callCtx, cancel := context.WithTimeout(ctx, timeout) + callCtx, cancel := context.WithTimeout(ctx, e.callTimeout()) defer cancel() req, err := e.newEmbeddingRequest(callCtx, texts) @@ -224,16 +212,6 @@ func (e *APIEmbedder) embedChunk(ctx context.Context, texts []string) ([]Vector, resp, err := e.client.Do(req) if err != nil { - // 冷启动放宽被花掉且没成功时留一条 WARN:它是「本进程第一次请求」这个 - // 判断唯一能被事后复核的地方(此后一切失败都按常态超时快速失败)。 - // 只记两个预算与错误(错误文本里已带端点,不另记 URL,避免重复落一条 - // 可能含 userinfo 的地址)。 - if relaxed { - e.log.Warn("embedding: one-shot cold-start allowance spent without a successful response", - "warmup_timeout", warmupTimeout.String(), - "steady_timeout", DefaultRequestTimeout.String(), - "error", err) - } return nil, fmt.Errorf("embedding: request: %w", err) } defer func() { _ = resp.Body.Close() }()