Implement extensions/vision/src/client.ts (request build, fetch, parse, error mapping) #257
Labels
No labels
bug
chore
documentation
enhancement
feature
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
david/pi-extensions-and-skills#257
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Create
extensions/vision/src/client.ts: build the OpenAI-compatible chat-completions body for avisioncall, POST it to DeepSeek with a hard deadline, parse the response, and translate every HTTP, network, timeout, abort, and parse failure into a typedToolErrorwith an actionable message.Background
Depends on: #253, #255, #256
This is the only module in the extension that talks to the network. It is deliberately split into a pure body builder (
buildRequestBody) and an injectable client (createHttpVisionClient(config, fetchImpl)), so the whole request/response surface — URL, headers, exact JSON body, every error branch — is testable with a fakefetchand never needs a real key or the network.Two behaviours matter beyond "make a request":
thinkingtoggle is always sent explicitly. DeepSeek defaults thinking mode to enabled (pinned docs), so omitting the field would make the model's default leak into the tool's documentedthinking: falsedefault. Sending{"type":"disabled"}when the flag is false overrides it deterministically.signal: undefined, but a wall-clock timeout (VISION_TIMEOUT, default 120 s) must still apply, so the two signals are combined withAbortSignal.anyand the resulting abort is classified as eithertimeout(deadline) oraborted(pi's signal).Documentation Required
A separate process downloads these into the listed folders before this issue is implemented. Check the folder for the actual reference material before starting.
docs/reference/deepseek-api/model,max_tokens,thinking,messages,contentblocks,image_url.detail) and the response envelope (choices[0].message.content,finish_reason,usage).image_urlblock shape (base64 data URL), thedetailvalues (low/high/original/auto), and the 48 MiB body limit.{"thinking":{"type":"enabled|disabled"}}and the fact that thinking mode is enabled by default.docs/reference/nodejs/fetch(no dependency needed).AbortSignal.timeout(ms).AbortSignal.any([...])for combining pi's signal with the deadline.reason/aborted, not the message string).Implementation Details
Public surface:
The body must be exactly:
detailis present insideimage_urlonly when the caller supplied it (nonull/undefinedkey).ValidatedImage, preserving order.thinkingis always{"type":"enabled"}or{"type":"disabled"}.Transport:
POST ${config.baseUrl}/chat/completionswith headersContent-Type: application/jsonandAuthorization: Bearer ${config.apiKey}.AbortSignal.any([toolSignal, AbortSignal.timeout(config.timeoutMs)]), wheretoolSignalis included only when defined (pi may passundefined). Pass the combined signal tofetchImpl.{ "error": { "message", "type", "code" } }, and throwToolErrorwith categoryhttp, message including the status and the API message (fall back to the raw text when the body is not JSON). Hints by status: 401 ⇒ auth hint (set DEEPSEEK_API_KEY/ check the key); 429 ⇒ rate-limit hint; 5xx ⇒ "DeepSeek server error, retry" hint. The category for every non-2xx stayshttp; the 401/429 hints appear in the message (they do not change the category).ToolErrorcategorynetwork.ToolErrorcategorytimeout, message namingVISION_TIMEOUTand its current value.ToolErrorcategoryaborted.DOMExceptionor anAbortErrorwhose wording is not stable).JSON.parsefailure ⇒ToolErrorcategoryresponse.choices[0].message⇒ToolErrorcategoryresponse.ToolError.Test file
extensions/vision/src/client.test.ts(TDD, injected fakefetch, no network):buildRequestBodyasserts method/URL/headers and the exact JSON body: thinking enabled vs disabled,detailpresent vs absent, text block first, image order preserved.fetchreturning 200 with a valid envelope ⇒ parsed response returned.ToolErrorwithcategory === "http"and a message containing the status and API message; assert the 401/429/5xx hints appear.httperror, no crash.fetchrejecting (simulated network failure) ⇒network.fetchplus a tinytimeoutMs⇒timeout.aborted.response; missingchoices⇒response.Acceptance Criteria
buildRequestBodyproduces exactly the JSON shape above:thinkingalways explicit,detailonly when supplied, text block before image blocks, image order preserved.${baseUrl}/chat/completionsas aPOSTwithContent-TypeandBearerauth headers.signal: undefined, and combines with pi's signal when present.ToolErrorwith the documented category:httpfor every non-2xx (with the 401/429/5xx hints in the message),network,timeout,aborted, andresponsefor parse/shape failures — each with an actionable message.complete().extensions/vision/src/client.test.tscovers the exact body, the 2xx parse, each error branch, and the timeout/abort distinction, using an injected fakefetch.node --test extensions/vision/src/client.test.tspasses and no test touches the network.Test Plan
Expected: all tests pass with zero network access (an injected fake
fetchis the only transport).Optional live sanity check once a key exists in
.env(not required for this issue and not part of the automated suite):Expected: a response envelope with
choices[0].message.content; a missing key yields aToolErrorwith an auth hint rather than a stack trace.pi-loop opened and merged a pull request for this issue: #271