A model listing over 4 KiB reads as 'did not answer': the connect probe truncates the body before parsing
@AbirAbbas đang làm issue này rồi.
Từ ngày 23/9/2026.
Đánh giá
Issue này chưa được đánh giá.
Mô tả
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
runServiceProbeininternal/config/sources.go— the read isio.ReadAll(io.LimitReader(response.Body, 4<<10)), i.e. only the first 4096 bytes, andLimitReaderstops at the cap without an error, so the caller is told the machine answered.listedOutcomein the same file — thejson.Unmarshalof that body. On a truncated body it fails, returnsok == false, andConnectServiceturns that straight intoOutcomeUnanswered.- The wording a person meets:
internal/tui3/modelservices.go(searchdid not answer · nothing was saved). - The row it hits is
CustomIDininternal/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:
- Read the whole body. A 4 KiB bound on the read and a whole-document
json.Unmarshalcannot 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 streamingjson.Decoderthat never depends on having the last byte. - Never let a failed parse read as silence.
OutcomeUnansweredshould mean "no HTTP response at all" — refused, timed out, DNS — which is what the existingserver.Hangcase asserts it for. A2xxwhose 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(TestAListingProbeConnectsAndCountsModelsstays green). - Unit:
runServiceProbe+listedOutcomereturnok == trueand the full model count for a body over 4096 bytes; and an unreachable address still returnsOutcomeUnanswered(the existingserver.Hangcase). - The manual page for connecting a service quotes the new wording (
internal/manual/chat/), and the change entry'sinvalidatesnames what people believed before — that "did not answer" meant the endpoint was unreachable.
—
Drafted with CodeAF · reviewed and owned by the author
- Ngôn ngữ chính
- Go
- Star
- 115
- Fork
- 14
- Merge trung bình
- 9 giờ 38 phút
- Pull request đã merge (30 ngày)
- 730
Hướng dẫn đóng góp
Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của Agent-Field/CodeAF
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Agent-Field/CodeAF#1489 ·
-
area:chat bug good first issue sev:papercut
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Agent-Field/CodeAF#1470 ·
-
area:chat bug good first issue sev:papercut
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
Agent-Field/CodeAF#1469 ·
-
area:chat bug sev:papercut
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
Agent-Field/CodeAF#1468 ·
-
area:tests bug hygiene sev:papercut
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
Agent-Field/CodeAF#1460 ·
Tất cả issue của Agent-Field/CodeAF
Issue tương tự
-
ai-generated
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
SiaFoundation/indexd#1116 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
-
triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
wakatime/wakatime-cli#1585 ·