Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

A model listing over 4 KiB reads as 'did not answer': the connect probe truncates the body before parsing

未关闭
#1,413 0 条评论 0 个 reaction 已指派 1 人 在 GitHub 查看

@AbirAbbas 已经在做这个了。

开始于 2026年9月23日。

评估

这个 Issue 还没有评估数据。

描述

area:provider bug sev:serious

What happened

Connecting a Custom OpenAI-compatible API row to an endpoint that is up and healthy is reported as unreachable — the surface prints its own words:

custom did not answer · nothing was saved

Observed on dev@d6bb3679a (2026-09-23). The endpoint answers correctly: GET <address>/models returns 200 with a valid OpenAI listing. Nothing is wrong with it except that the listing is larger than 4096 bytes. Point the same row at the same server with a smaller catalog and it connects; with the larger catalog it is "not answered". The only variable is the size of the model list.

Replication

Deterministic (no model).

In internal/config/sources_test.go, beside TestAListingProbeConnectsAndCountsModels, drive the same door with a listing that crosses 4096 bytes:

// enough names that the encoded listing passes 4096 bytes:
// sourcestub.serveModels writes one {"id":"<name>"} per model, so the body
// grows by about len(name)+12 each — ~40 names of ~95 chars, or ~50 ordinary.
server := sourcestub.New(names...)
defer server.Close()
source := vendoredCustomSource(t)          // the CustomID row
source.Address = server.URL()
row := PersistedSource{ID: source.ID, Written: source.Written, Key: "any-key", Order: 1}
outcome, err := ConnectService(context.Background(), t.TempDir(), row, source, nil)

Today this yields OutcomeUnanswered (outcome.Kind == modelsource.OutcomeUnanswered) and PersistedSources(dir) is empty — nothing was saved. The two-model case (TestAListingProbeConnectsAndCountsModels) yields OutcomeConnected with Listed == true; the expectation here is identical, with Models equal to the count the stub served.

Field (real models).

The door a person uses is the connect surface (/connect → Custom OpenAI-compatible API, internal/tui3/modelservices.go). Point it at any OpenAI-compatible gateway, proxy or aggregator that resells 50+ models — the listing alone then crosses 4 KiB — with any non-empty key. The listing probe is the only call made, so wall time is seconds and cost is $0, and no provider key of ours is involved. What makes it fire is the catalog size, not the vendor: the same address with a handful of models connects.

Where

  • runServiceProbe in internal/config/sources.go — the read is io.ReadAll(io.LimitReader(response.Body, 4<<10)), i.e. only the first 4096 bytes, and LimitReader stops at the cap without an error, so the caller is told the machine answered.
  • listedOutcome in the same file — the json.Unmarshal of that body. On a truncated body it fails, returns ok == false, and ConnectService turns that straight into OutcomeUnanswered.
  • The wording a person meets: internal/tui3/modelservices.go (search did not answer · nothing was saved).
  • The row it hits is CustomID in internal/modelsource/modelsource.go (Listing: ListingModels, Probe: listingProbe()) — the one row meant for exactly the proxies, gateways and aggregators whose catalogs are the biggest, so the cap bites the row it was least meant to.

The fix

Two things, and the second is the important one:

  1. Read the whole body. A 4 KiB bound on the read and a whole-document json.Unmarshal cannot both be satisfied by any listing past 4 KiB — the parse needs the entire document, so the bound must exceed any real catalog (order of a megabyte) or the decode must be a streaming json.Decoder that never depends on having the last byte.
  2. Never let a failed parse read as silence. OutcomeUnanswered should mean "no HTTP response at all" — refused, timed out, DNS — which is what the existing server.Hang case asserts it for. A 2xx whose body will not parse is a different fact and deserves its own outcome (a wrong-shape or refused outcome carrying the vendor's first line), so a reachable-but-odd endpoint is never reported with the same words as a dead one.

The choice this settles: the bound, if kept, is a safety valve and not a probe window; the outcome vocabulary stays three-way, with "not answered" reserved for transport silence.

Acceptance

  • e2e: through the connect surface (internal/tui3/modelservices.go), a stub serving a listing over 4096 bytes connects — the row is persisted and the shelf shows its models, not the did-not-answer line.
  • e2e: the control — the same stub with a two-model listing still connects with Models == 2 (TestAListingProbeConnectsAndCountsModels stays green).
  • Unit: runServiceProbe + listedOutcome return ok == true and the full model count for a body over 4096 bytes; and an unreachable address still returns OutcomeUnanswered (the existing server.Hang case).
  • The manual page for connecting a service quotes the new wording (internal/manual/chat/), and the change entry's invalidates names what people believed before — that "did not answer" meant the endpoint was unreachable.

—
Drafted with CodeAF · reviewed and owned by the author

主要语言
Go
星标
115
派生
14
平均合并
9 小时 38 分钟
30 天内合并 PR
730

贡献指南

这个仓库没有索引到贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

Agent-Field/CodeAF 的其他 Issue

查看 Agent-Field/CodeAF 的全部 Issue

相似的 Issue

更多 Go Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。