dsh-packycode-api 1.4.4 → 1.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.en.md +17 -15
- package/README.md +17 -15
- package/lib/client.js +4 -3
- package/lib/index.js +459 -41
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.6
|
|
4
|
+
|
|
5
|
+
- **Hy4 joins the catalog.** PackyCode added the `hunyuan-officially` group (腾讯混元官方渠道) and Tencent's `hy4` model: on 2026-11-04 `/api/pricing` lists `hy4` in that group alone, with `anthropic`, `openai` and `openai-response` endpoints. `hy4` takes the Responses route: `openai-responses` at `https://cf.api.fan/v1`, text and image input, 1,024,000 context and 64,000 output. TokenHub's [Hunyuan guide](https://cloud.tencent.com/document/product/1823/132252) gives Hy4 a 1024K context, of which at most 960K input, 64K output and image input on all three protocols, and its [model overview](https://cloud.tencent.com/document/product/1823/130079) marks Hy4's Responses support as native rather than converted. The gateway does not check `max_output_tokens` (1,000,000 was accepted), so the output ceiling is the documented 64K, read as 64,000 like DeepSeek's 384K. Hy4 sits after Grok 4.7 in the picker, and the settings panel counts it on a Hunyuan line under each key.
|
|
6
|
+
- **Its efforts are `off` and `high`.** TokenHub's [deep thinking page](https://cloud.tencent.com/document/product/1823/131208) lists `reasoning_effort` `high` and `no_think` for Hy4, with thinking on by default. `high` is sent as is, and `off` sends `reasoning.effort: "no_think"`, the value Hy4's own row names. pi-ai's default `none` answered the same, and TokenHub's [Responses compatibility page](https://cloud.tencent.com/document/product/1823/133813) accepts both. The gateway also accepts `minimal`, `low`, `medium`, `xhigh` and `max`, but Hy4 does not document them and did not answer them differently from `high`, so they are not offered. Session titles run on Hy4 itself at `off`, the least effort it offers.
|
|
7
|
+
- **Reasoning, caching and tools.** Reasoning streams as `response.reasoning_summary_text.delta` into `summary_text` items, which pi-ai shows as thinking and replays unchanged with the turn's tool calls and in later turns, as TokenHub's [preserved thinking](https://cloud.tencent.com/document/product/1823/133534) asks for Hy4. The prefix cache hits as often without `prompt_cache_key` as with it, so Hy4 is sent none, like DeepSeek. Tools go without `strict`: TokenHub documents a function tool's `strict` as defaulting to true, but Hy4 filled DSH's optional sandbox fields about as rarely with `strict` omitted as with `strict: false`.
|
|
8
|
+
- **Hy4 is told who made it.** DSH's system prompt opens with "You are an AI agent powered by DeepSeek Harness." and "You are a coding agent powered by the hy4 model.": it names the harness, and the model only by its id. In the desktop app Hy4 then said it was a DeepSeek model, and asked whether it was Hunyuan, it denied it and claimed `hy` did not stand for Hunyuan. Every Hy4 request now ends its leading instructions with one sentence (`src/hy4-identity.js`): the hy4 model is Hy4 from Tencent Hunyuan, reached through PackyCode, and DeepSeek Harness is the agent app it runs in, not its model or its maker. DSH's text stays as written, and the sentence never changes, so the cached prefix stays stable; a caller's own payload hook still sees the final body. No other model's request changes: under DSH's prompt with their own ids, GPT-6.1 Sol and Terra named OpenAI, Grok 4.7 xAI, Gemini 3.8 Flash Google, and DeepSeek Flash and V4 Pro DeepSeek.
|
|
9
|
+
- Validation, 2026-11-04:
|
|
10
|
+
- **Offline:** 557 tests (new: Hy4's catalog entry, efforts, wire dialect and resolved metadata; `high` sent without the cache key or `strict`; `off` sent as `no_think`; summary reasoning streamed as thinking and replayed unchanged with its tool call and in the next user turn; undocumented efforts refused through the route before any request; Hy4 titling its own sessions at `off`; an image reaching Responses through DSH's `LlmRuntime`; pricing and coverage for `hunyuan-officially`; the identity sentence appended once after DSH's instructions, or sent alone without them, ahead of a caller's payload hook, and absent from the other models' requests), both rebuilt bundles and the versioned tarball. The suite also passed on Node 22.23.1.
|
|
11
|
+
- **Live, direct to `/v1/responses` with the test key (`hunyuan-officially` among its groups):** with no effort the gateway echoed `high`. `no_think` and `none` both answered with no reasoning item and 4 output tokens, at 52 prompt tokens against 49 for every thinking level, and a made-up effort was refused with 400. On one arithmetic problem, four requests per level all had 69 prompt tokens, and mean reasoning tokens were 4,160 at `low`, 2,917 at `medium`, 4,117 at `high`, 3,823 at `xhigh` and 3,506 at `max`, against a spread of 1,270–7,737 within the levels. With DSH's bash tool, the optional sandbox pair was filled in 1 of 14 calls with `strict` omitted, 1 of 14 with `strict: false` and 0 of 14 with `strict: true`. A prefix of about 13,000 tokens sent 8 times was cached on 4 of the 7 repeats both with and without `prompt_cache_key`.
|
|
12
|
+
- **Live, through the built route:** `off` sent `no_think` and answered `ok` without thinking, and `high` answered `ok` with streamed thinking; `minimal`, `low`, `medium`, `xhigh` and `max` were refused before any request. An `echo` tool call continued to `ok` 2 of 2 times at `high` and 2 of 2 at `off`, replaying the reasoning item and the `fc_` call item, and a following user turn carried every earlier reasoning item and answered with the echoed text, 4 of 4. DSH's title request at `off` came back as 整理十月发货单并标缺货. `scripts/live-check.mjs --model hy4 --with-vision` answered `ok` and named a red and a blue image. With the identity sentence in place, the efforts, 3 more `echo` continuations and the title came back the same; the vision check named both images in 1 of 3 runs, and in the other two Hy4 reasoned past the script's 2,048-token output cap at `high` on the red image and was cut off without an answer. Sent directly with the same image, 5 of 10 requests without the sentence and 4 of 10 with it ended at that cap, so the sentence is not the cause; DSH's own requests use the model's 64,000-token ceiling. In the full `scripts/live-check.mjs`, the key's groups read as reaching Hy4 and every model answered except `gemini-4-argon`: `/api/pricing` now lists Argon only in `gemini-officially`, which the test key lacks, and the gateway refused it with `分组 gemini-slb 下模型 gemini-4-argon 无可用渠道(distributor)`. That is upstream, and nothing in this change touches Gemini. Key rotation, the rejected-key classification, both auto reviewers and the RPC projections were unchanged. Not checked in the desktop app.
|
|
13
|
+
- **Live, Hy4's identity:** the desktop session's log showed both answers coming from `packycode / hy4` in Hy4's own reply format. Sent directly without a system prompt, Hy4 answered "我是混元,是由腾讯开发的大模型" 3 of 3 times. With DSH's system prompt it named DeepSeek Harness or no maker at all, never Tencent, in 3 of 3; after the desktop session's first answer it denied being Hunyuan 6 of 6 times; with the harness renamed it no longer claimed DeepSeek. With the sentence, sent directly with DSH's prompt, it named Tencent Hunyuan in 4 of 4 first answers, confirmed being Hunyuan in 4 of 4 follow-ups and named Tencent in 4 of 4 English answers; through the built route, 3 of 3 for each turn. The sentence did not leak into other answers: `ok` 3 of 3, a JavaScript question 3 of 3 and DSH titles 3 of 3 carried no identity talk, and a bare 你好 introduced itself as Tencent Hunyuan's Hy4 in 2 of 3. Not checked in the desktop app.
|
|
14
|
+
|
|
15
|
+
## 1.4.5
|
|
16
|
+
|
|
17
|
+
- **Diagnostics now say how a request to PackyCode failed on the network.** An Ubuntu user running `dsh --profile web` saw model requests retried over and over: one session had 39 failed attempts in 12 steps, logged as `Request timed out.`, a later one `Connection error.`, and the balance read `fetch failed (ETIMEDOUT)` from every status host. Each attempt failed about 0.5 s after it started. `cf.api.fan` and `www.packyapi.ai` resolve to two IPv4 and two IPv6 addresses. Node tries them in turn and gives every address but the last 250 ms (`net.getDefaultAutoSelectFamilyAttemptTimeout()`). On that machine the lookup returned the IPv6 addresses but they could not be reached, so the last address failed at once, and the connection failed whenever both IPv4 handshakes took longer than 250 ms. Node's own undici reports that as an `AggregateError`, which the OpenAI SDK calls `Connection error.`. The undici 8 that DSH installs with a proxy policy rewrites it as a `ConnectTimeoutError` that still says `timeout: 10000ms`, which the SDK calls `Request timed out.`. The report kept neither the cause nor the addresses, so the two texts looked like two problems.
|
|
18
|
+
- **The report has a `network` section** (`src/network.js`). From the moment the plugin loads, it listens on undici's `diagnostics_channel` for requests to the plugin's own hosts: `cf.api.fan`, `www.packyapi.ai`, `www.packyapi.com` and any host override. For each host it records requests, answered requests by status, failures by stage (connect, request or mid-response), cancellations, time to response headers, and connections that needed a later address or went over IPv6. Each of the 12 latest failures keeps the path without its query, its stage and duration, the error chain's names and codes, each address tried with its error, and the message, redacted and shortened. The section also gives the Node version and platform, `autoSelectFamily`, the per-address attempt timeout, the DNS result order and the scheme each proxy variable names. It looks each host up the way `net.connect` does, waiting at most 3 s. Headers, bodies, queries, proxy addresses and other hosts are never read. The section covers the whole process rather than one session, and the card's privacy note and both READMEs say so.
|
|
19
|
+
- Validation, 2026-10-30:
|
|
20
|
+
- **Offline:** 544 tests (new: requests to a loopback server counted by status and timed, and another host name for the same server ignored; a connection refused on both addresses of a host recorded with each address; a cancellation counted apart from failures, a timeout recorded at the request stage and a cut body mid-response; undici 8's rewritten connect timeout and a connection that needed a later address read from published events, with stored keys redacted and only the newest failures kept; proxy schemes without proxy hosts or credentials; host lookups bounded at 3 s; the plugin's own wiring through the RPC), both rebuilt bundles and the versioned tarball.
|
|
21
|
+
- **Under DSH's runtime (the desktop app's Node 24.21 with its undici 8.11.2):** one host resolved to two IPv4 addresses that never answered and two IPv6 addresses that refused at once. The connection failed after 510–525 ms. Through Node's undici the log recorded `AggregateError ETIMEDOUT`; through DSH's undici 8 it recorded `ConnectTimeoutError UND_ERR_CONNECT_TIMEOUT` over `AggregateError ETIMEDOUT`. Both records listed the four addresses with their codes. On real traffic to `cf.api.fan` (its root path, no key), direct and through a local HTTP proxy with DSH's undici 8, the log counted three requests answered 404, their time to headers and their connections. Going direct, one of the two connections reached the second address after the first took longer than 250 ms. The built `lib/index.js` loads there. Not checked in the desktop app or on Linux.
|
|
22
|
+
- **Gemini 4 Argon is back in the catalog.** PackyCode serves `gemini-4-argon` again: on 2026-10-30 `/api/pricing` lists it in `gemini-slb` and `gemini-officially`, and the gateway answers it. It takes back its earlier entry: `google-generative-ai` at `https://cf.api.fan/v1beta`, 1,048,576 context and 65,536 output per Google's model page, and `low`, `medium` and `high`. The pricing page now lists an `openai` endpoint for it too; it stays on the native protocol with Flash, with `thinkingLevel` and the skip signature, and the one resend stays Flash's alone. The picker offers twelve models, the settings panel counts Argon under Gemini, `scripts/gemini-tool-check.mjs` takes it again, and the READMEs' FAQ entry about the refusal is gone. A session left on Argon works again without being changed.
|
|
23
|
+
- Validation, 2026-10-30:
|
|
24
|
+
- **Offline:** 548 tests (the per-model native Gemini tests run for Argon again, its catalog entry, efforts and resolved metadata are pinned again, the replay guard's non-Flash case is Argon again, and a session on a model that left the catalog is still refused as `UNKNOWN_MODEL` before any credential is read), both rebuilt bundles and the versioned tarball.
|
|
25
|
+
- **Live, through the built route, with the test key (`gemini-slb` among its groups):** Argon answered in `scripts/live-check.mjs`, named a red and a blue image, and read as reached by the key's groups. Native tool calls continued to `ok` in 6 of 10 rounds (4 at `low`, 1 at `medium`, 1 at `high`), each continuation accepted on its first send, with no signature refusal. Of the other four, three passed the script's 90 s limit without finishing, and one was refused at 08:24Z with `分组 gemini-slb 下模型 gemini-4-argon 无可用渠道(distributor)`. Flash continued a tool call to `ok` once at `low`. The other models answered, except `grok-4.7`: `/api/pricing` now lists it only in `grok-sale`, which the test key lacks, and the gateway refused it with `分组 grok-officially 下模型 grok-4.7 无可用渠道(distributor)`. That is upstream, and nothing in the plugin changed for it. Not checked in the desktop app.
|
|
26
|
+
- **Live, direct to the native endpoint:** the stalls are not Argon's alone. Sampled every 20 s from 08:25Z to 08:31Z, Argon answered 15 of 16 requests in 3.8–64 s and Flash 12 of 16 in 2.0–11.6 s; the rest of each passed 90 s. In a run of 12 streaming requests per model, all 24 answered, with headers after 2.8–51 s on Argon and 2.0–10.6 s on Flash.
|
|
27
|
+
|
|
3
28
|
## 1.4.4
|
|
4
29
|
|
|
5
30
|
- **DeepSeek V4 Pro joins the catalog** ([#4](https://github.com/BaronCyrus/dsh-packycode-api/issues/4)). `deepseek-v4-pro` takes DeepSeek Flash's route: `openai-responses` at `https://cf.api.fan/v1`, 1,000,000 context and 384,000 output, with Flash's efforts (`off` as `reasoning.effort: "none"`, `low`, `high`, `max`), per DeepSeek's [models and pricing page](https://api-docs.deepseek.com/quick_start/pricing/) and [API reference](https://api-docs.deepseek.com/api/create-chat-completion). PackyCode's `/api/pricing` lists it in `deepseek-officially` and `deepseek-sale` with `openai-response` among its endpoints, at 4.5 times Flash's input rate. It sits after Flash in the picker, and the settings panel counts it under DeepSeek. pi-ai's own DeepSeek catalog pins `low` unsupported for V4 Pro; DeepSeek documents `low` for both models, and through PackyCode V4 Pro answered it as its own level.
|
package/README.en.md
CHANGED
|
@@ -48,10 +48,10 @@ You need **DeepSeek Harness** and a **PackyCode (PackyAPI) API key**. For DSH se
|
|
|
48
48
|
Open the desktop application's **Plugins** page, click **Add plugin**, and enter the package name **with a version** as the **package name or address**:
|
|
49
49
|
|
|
50
50
|
```text
|
|
51
|
-
dsh-packycode-api@1.4.
|
|
51
|
+
dsh-packycode-api@1.4.6
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
Choose the **official** install source, then click **Install**. `1.4.
|
|
54
|
+
Choose the **official** install source, then click **Install**. `1.4.6` was the latest release when this was written; the [npm page](https://www.npmjs.com/package/dsh-packycode-api) shows the current one.
|
|
55
55
|
|
|
56
56
|
> **Always include the version.** With a bare `dsh-packycode-api`, the install prompt shows the newest version on npm, but DSH's bundled pnpm by default installs only versions published at least 24 hours ago, so you can end up several releases behind with no sign of it in the prompt. With an exact version, pnpm installs that version and adds it to the profile's `minimumReleaseAgeExclude` itself. Afterwards, check **Current version** under **Settings → PackyCode → Plugin version**.
|
|
57
57
|
|
|
@@ -65,14 +65,14 @@ Open **Settings → PackyCode**. Paste the API key under **Add a key**. A label
|
|
|
65
65
|
|
|
66
66
|
### 3. Select a model
|
|
67
67
|
|
|
68
|
-
Choose a model from the `PackyCode` group. Of the
|
|
68
|
+
Choose a model from the `PackyCode` group. Of the thirteen models, Claude runs through this device's Claude Code CLI on `cc` keys; everything else uses the plugin’s HTTP route. Your key’s groups must cover the selected model.
|
|
69
69
|
|
|
70
70
|
<details>
|
|
71
71
|
<summary>Verify the install / other profiles / a local checkout</summary>
|
|
72
72
|
|
|
73
73
|
The **Plugins** page shows the installed state and version, and is also where you enable, disable, or uninstall.
|
|
74
74
|
|
|
75
|
-
**For non-desktop profiles only:** `dsh plugin --profile <name> add dsh-packycode-api@1.4.
|
|
75
|
+
**For non-desktop profiles only:** `dsh plugin --profile <name> add dsh-packycode-api@1.4.6`. Do not use it for `desktop` — the CLI refuses it (`profile "desktop" is managed exclusively by the Electron application`).
|
|
76
76
|
|
|
77
77
|
For a local checkout, keep the `link:` prefix: `dsh plugin --profile <name> add link:/path/to/dsh-packycode-api`. **Do not pass a bare path or `file:`** — that install copies the whole checkout, `node_modules` included, into the profile, where it then shadows the packages the DSH installation itself provides and the plugin looks uninstalled; see the FAQ. If you already installed that way, uninstall first and reinstall as above.
|
|
78
78
|
|
|
@@ -86,7 +86,7 @@ A profile without a browser can still use the model route. It just has no settin
|
|
|
86
86
|
|
|
87
87
|
**Switch keys.** Keys are used in the order they were added. **Use this key** picks the preferred key, effective from the next turn; **Rename** changes its label (at most 60 characters, empty names are refused); **Re-enable** puts a disabled key back after a top-up or replacement.
|
|
88
88
|
|
|
89
|
-
**See groups.** Under **Accounts and keys**, each key lists its PackyCode groups (hover for the group's description) and which models they reach: ✓ yes, ◐ partly, ✕ no. The groups come from the gateway itself (its refusal of a model id it cannot route names the token's groups, as “已选分组=[…]”, or for a single-group key as “分组 … 下模型 … 无可用渠道”; nothing is billed), matched against PackyCode's public model-group table. Check Claude group coverage per model; do not assume `cc-sale` serves all three models; GPT needs `codex` / `codex-sale`; DeepSeek needs `deepseek-officially` / `deepseek-sale`.
|
|
89
|
+
**See groups.** Under **Accounts and keys**, each key lists its PackyCode groups (hover for the group's description) and which models they reach: ✓ yes, ◐ partly, ✕ no. The groups come from the gateway itself (its refusal of a model id it cannot route names the token's groups, as “已选分组=[…]”, or for a single-group key as “分组 … 下模型 … 无可用渠道”; nothing is billed), matched against PackyCode's public model-group table. Check Claude group coverage per model; do not assume `cc-sale` serves all three models; GPT needs `codex` / `codex-sale`; DeepSeek needs `deepseek-officially` / `deepseek-sale`; Hy4 needs `hunyuan-officially`.
|
|
90
90
|
|
|
91
91
|
**Read the balance.** Each key under **Accounts and keys** shows its own remaining, used, and limit (limit = remaining + used); an unlimited key shows **No quota limit**. Keys are not added up into a total. An API key can read only its own quota, not the PackyCode account's balance, so check the account balance in the PackyCode console. A key that could not be read says why on its own row; a key whose read failed this time but succeeded before keeps showing its last balance with the time it was read (the composer shows it as `≈US$…`) until DSH restarts. The composer shows the active key's remaining balance only while a PackyCode model is selected. **Refresh** bypasses the roughly 60-second cache. The default model host is `https://cf.api.fan` and the balance host is `https://www.packyapi.ai`; both can be changed under **Advanced: endpoints**. The old default, `https://www.packyapi.com`, is DNS-poisoned on mainland networks and answers `fetch failed` without a proxy; even when settings still name it, `.ai` is tried first.
|
|
92
92
|
|
|
@@ -227,6 +227,8 @@ The card shows whether DSH's plugin is on, how many reviews this plugin answered
|
|
|
227
227
|
| `gpt-6-astra` | Same | Cannot be off; `low` / `medium` / `high` / `xhigh` / `max` | Same |
|
|
228
228
|
| `gpt-5.6-sol` | Same | Same | Same |
|
|
229
229
|
| `grok-4.7` (Grok 4.7) | HTTP, Responses, text/image input; 500K context | Cannot be off; `low` / `medium` / `high` / `xhigh` | The Grok coverage read from the gateway in Settings |
|
|
230
|
+
| `hy4` (Hy4, Tencent Hunyuan) | HTTP, Responses, text/image input; 1024K context (at most 960K input), 64K output | Can be off; `high` only | `hunyuan-officially` |
|
|
231
|
+
| `gemini-4-argon` (Gemini 4 Argon) | HTTP, native Gemini streaming, text/image input; 1,048,576 context, 65,536 output | Cannot be off; `low` / `medium` / `high` | `gemini-slb` |
|
|
230
232
|
| `gemini-3.8-flash` (Gemini 3.8 Flash) | HTTP, native Gemini streaming, text/image input; 1,048,576 input, 65,536 output | Cannot be off; `low` / `medium` / `high` | `gemini-slb` |
|
|
231
233
|
| `claude-opus-5-5` | Anthropic Messages, images and native tools: this device's Claude Code CLI as the agent for `cc` keys, HTTP otherwise | Cannot be off; `low` / `medium` / `high` / `xhigh` / `max` | Claude-group coverage reported in Settings |
|
|
232
234
|
| `claude-sonnet-5-5` | Same | Same | Same |
|
|
@@ -236,15 +238,17 @@ A model in the list does not mean the current token group allows it. When changi
|
|
|
236
238
|
|
|
237
239
|
`jev-latest` (TypeSafe Jev) is not in the model picker: it is a decision model that answers classification questions and cannot hold a conversation, so it only serves [Auto review](#auto-review). The plugin calls `/v1/systemone` host-side; it needs a key in the `typesafe-officially` group, and each key's Jev entry under Accounts and keys shows whether it can.
|
|
238
240
|
|
|
239
|
-
Of the
|
|
241
|
+
Of the thirteen models in the current catalog, all but DeepSeek V4 Pro declare text and image input. DeepSeek Flash follows the [official vision guide](https://api-docs.deepseek.com/guides/vision/); images are read through DSH's attachment service and sent with the Responses request. DeepSeek V4 Pro takes text only, per DeepSeek's [models and pricing page](https://api-docs.deepseek.com/quick_start/pricing/): DSH does not let you attach an image to it, and an image already in the session (say, one switched over from Flash) is sent as a one-line "image omitted" note. PackyCode's gateway does not refuse an image sent to V4 Pro; it drops it.
|
|
240
242
|
|
|
241
|
-
Gemini
|
|
243
|
+
Gemini capabilities and limits follow Google's model pages: [Gemini 4 Argon](https://ai.google.dev/gemini-api/docs/models/gemini-4-argon), [Gemini 3.8 Flash](https://ai.google.dev/gemini-api/docs/models/gemini-3.8-flash). Both models use the native Gemini protocol through `/v1beta/models/{model}:streamGenerateContent`, streaming text, thinking and tool calls; images are read through DSH's attachment service. Thinking levels are sent with `thinkingLevel`. Both routes use the single-stream output limit without automatic continuation beyond 65,536 tokens. Available endpoints and groups follow the [PackyAPI live model catalog](https://www.packyapi.com/api/pricing).
|
|
242
244
|
|
|
243
|
-
**Gemini tool continuation and thought signatures:** PackyCode's native Gemini route does not handle thought signatures in tool calls correctly. It answers HTTP 400 `Invalid thought signature` / `Corrupted thought signature`, and resending the same request does not help (PackyCode support's answer). For Gemini 3.8 Flash the plugin therefore sends none of the signatures the model returned. Wherever a signature is required, it sends Google's documented validator-skip value `skip_thought_signature_validator` ([Google's guide](https://ai.google.dev/gemini-api/docs/thought-signatures)). The protocol, endpoint and model stay the same, and DSH's replay record keeps the original signatures. Google notes that skipped signatures can weaken reasoning continuity across tool steps. Flash keeps its earlier safeguard: after an HTTP 400 signature refusal before any stream event, it waits one second and resends once. Such a 400 is never treated as a failed key.
|
|
245
|
+
**Gemini tool continuation and thought signatures:** PackyCode's native Gemini route does not handle thought signatures in tool calls correctly. It answers HTTP 400 `Invalid thought signature` / `Corrupted thought signature`, and resending the same request does not help (PackyCode support's answer). For Gemini 4 Argon and Gemini 3.8 Flash the plugin therefore sends none of the signatures the model returned. Wherever a signature is required, it sends Google's documented validator-skip value `skip_thought_signature_validator` ([Google's guide](https://ai.google.dev/gemini-api/docs/thought-signatures)). The protocol, endpoint and model stay the same, and DSH's replay record keeps the original signatures. Google notes that skipped signatures can weaken reasoning continuity across tool steps. Flash keeps its earlier safeguard: after an HTTP 400 signature refusal before any stream event, it waits one second and resends once. Such a 400 is never treated as a failed key.
|
|
244
246
|
|
|
245
247
|
Grok 4.7's context and reasoning levels follow the [official guide](https://docs.x.ai/developers/grok-4-7). There is no separate text-output limit; the plugin uses the context size as its numeric output ceiling, bounded by the remaining context per request.
|
|
246
248
|
|
|
247
|
-
|
|
249
|
+
Hy4's protocols, context, output limit and image input follow Tencent Cloud TokenHub's [Hunyuan guide](https://cloud.tencent.com/document/product/1823/132252), and its effort levels follow the [deep thinking page](https://cloud.tencent.com/document/product/1823/131208): Hy4 thinks by default, and `reasoning_effort` takes only `high` and `no_think`. The plugin sends `high` as is; with effort off it sends `reasoning.effort: "no_think"`, and the answer comes back without reasoning. The gateway also accepts `low`, `medium`, `xhigh` and `max`, but Hy4's documentation does not list them, and in testing they came back with the same prompt tokens as `high` and no consistent difference in reasoning, so the plugin does not offer them. Images are read through DSH's attachment service and sent with the Responses request, as for DeepSeek Flash. Reasoning comes back as `summary_text` items; as TokenHub's [preserved thinking](https://cloud.tencent.com/document/product/1823/133534) asks, the plugin sends it back unchanged with tool continuations and in later turns. DSH's system prompt says only "powered by DeepSeek Harness" and "powered by the hy4 model", without saying whose model hy4 is, and Hy4 then calls itself a DeepSeek model and denies being Hunyuan (the other models recognize their own ids). So the plugin ends the system prompt of every Hy4 request with one sentence: hy4 is Tencent Hunyuan's Hy4, reached through PackyCode, and DeepSeek Harness is only the app it runs in. DSH's own prompt is kept as written, and other models' requests are unchanged.
|
|
250
|
+
|
|
251
|
+
Only GPT Responses requests keep `prompt_cache_key`; other Responses models omit it by default. This gateway's Grok route reports a misleading `Incorrect API key provided` error when the field is present, and DeepSeek Flash and Hy4 hit their prefix caches without it. GPT tool definitions also carry an explicit `strict: false`: PackyCode's GPT route treats a tool without `strict` as strict, which forces the model to fill every optional parameter (see the FAQ).
|
|
248
252
|
|
|
249
253
|
GPT-6.1 Terra's context, output limit and effort levels follow OpenAI's [model page](https://developers.openai.com/api/docs/models/gpt-6.1-terra). It is the only GPT model whose reasoning can be off: with effort off it sends `reasoning.effort: "none"`, which the gateway passes through, and the answer comes back without reasoning. GPT-5.6 Sol's documentation lists `none` too, but through PackyCode it answers at `medium`, so it cannot be off.
|
|
250
254
|
|
|
@@ -271,9 +275,9 @@ DeepSeek Flash uses the gateway's Responses endpoint (`/api/pricing` lists `open
|
|
|
271
275
|
|
|
272
276
|
## Update and uninstall
|
|
273
277
|
|
|
274
|
-
> **Upgrading from 1.3.2 or earlier:** install `dsh-packycode-api@1.4.
|
|
278
|
+
> **Upgrading from 1.3.2 or earlier:** install `dsh-packycode-api@1.4.6` on the Plugins page instead of clicking **Update plugin** in the old settings card. The old button bypasses pnpm's release-age policy: if the version it installs is less than 24 hours old, the profile refuses every other plugin install until that version is 24 hours old. After the install and a restart, the button is the fixed one, and later updates can use it.
|
|
275
279
|
|
|
276
|
-
**Update:** open **Settings → PackyCode** and, when a newer version is listed, click **Update plugin**. It installs that exact version into the owning profile with DSH's bundled pnpm and reads `package.json` back before reporting success. The install keeps pnpm's release-age policy instead of overriding it: by default pnpm installs the version at once and records it in the profile's `minimumReleaseAgeExclude`; under a stricter pnpm policy the button says when the update can run. The newer version is read from npm, while pnpm downloads it from the profile's own install source: right after a release that source (a regional mirror in particular) may not have it yet, and the button asks you to update again in a few minutes; when the source cannot be reached, it asks you to check the network or proxy. A local `link:` checkout does not get the button. Then quit and reopen the desktop application. A page refresh does not reload the Host. Reinstalling from the **Plugins** page works too, again with the version in the package name (for example `dsh-packycode-api@1.4.
|
|
280
|
+
**Update:** open **Settings → PackyCode** and, when a newer version is listed, click **Update plugin**. It installs that exact version into the owning profile with DSH's bundled pnpm and reads `package.json` back before reporting success. The install keeps pnpm's release-age policy instead of overriding it: by default pnpm installs the version at once and records it in the profile's `minimumReleaseAgeExclude`; under a stricter pnpm policy the button says when the update can run. The newer version is read from npm, while pnpm downloads it from the profile's own install source: right after a release that source (a regional mirror in particular) may not have it yet, and the button asks you to update again in a few minutes; when the source cannot be reached, it asks you to check the network or proxy. A local `link:` checkout does not get the button. Then quit and reopen the desktop application. A page refresh does not reload the Host. Reinstalling from the **Plugins** page works too, again with the version in the package name (for example `dsh-packycode-api@1.4.6`); without it you may get a version that is merely the newest one published at least 24 hours ago.
|
|
277
281
|
|
|
278
282
|
**Coming back from a local-path install, uninstall first.** An update is a `pnpm add`: it rewrites the files inside the package directory and leaves a `node_modules` that an earlier local-path install left there. That older tree keeps shadowing the packages the DSH installation provides, so the plugin row fails to load with no visible reason. **Uninstall** runs `pnpm remove`, which deletes the package directory outright — only a reinstall after that is clean. `node scripts/install-check.mjs` reports whether a profile is in that state.
|
|
279
283
|
|
|
@@ -289,7 +293,7 @@ ls ~/.dsh/profiles/desktop/node_modules/dsh-packycode-api/node_modules
|
|
|
289
293
|
|
|
290
294
|
Output means a leftover tree: a registry install ships no `node_modules`, so this one usually comes from an earlier local-path install (`file:`, or a bare path instead of `link:`). It sits inside the profile directory, and DSH resolves a plugin's imports from inside the profile first, so the plugin gets those old copies — for example `@deepseek-ai/schemastery` 3.18.2, which has no `Schema.volatile()`, so the module throws while it is imported. When an import fails the Loader creates no fiber for that row and no client bundle is composed either, so the model route, the RPC channel, and the settings panel all stay absent.
|
|
291
295
|
|
|
292
|
-
Recovery: **uninstall** the plugin on the **Plugins** page, **install** it again with the version in the package name (for example `dsh-packycode-api@1.4.
|
|
296
|
+
Recovery: **uninstall** the plugin on the **Plugins** page, **install** it again with the version in the package name (for example `dsh-packycode-api@1.4.6`), then quit and reopen the desktop application. Uninstall runs `pnpm remove` and deletes the whole package directory; reinstalling or clicking **Update** only rewrites files and keeps the leftover tree. Deleting that `node_modules` by hand and restarting works too.
|
|
293
297
|
|
|
294
298
|
**Right after a first install, every card in Settings shows `transport failure for /packycode-api/…: HTTP 405`?** The installed version is 1.1.6 or earlier, usually because only the bare package name was entered: the install prompt shows the newest version, but pnpm installs the newest one published at least 24 hours ago (see **Install the plugin**). Those older versions can register their settings channel only after DSH restarts and rebuilds its `connection` service. So right after a first install `/packycode-api` does not exist yet, requests fall through to the static page, and it answers 405; the model route and the settings panel itself load normally. From 1.1.7 on, a first install needs no restart. Check which version was installed:
|
|
295
299
|
|
|
@@ -305,8 +309,6 @@ Recovery: quit and reopen the desktop application, and the old version works. Th
|
|
|
305
309
|
|
|
306
310
|
**A GPT model's file edit fails with `invalid justification: expected a non-empty sentence`?** This affected 1.4.2 and earlier; update to 1.4.3. PackyCode's GPT route treats a tool without `strict` as strict, so the model had to fill every optional parameter. It sent DSH's sandbox escalation pair as `sandbox_permissions: "workspace-write"` with an empty `justification`, and DSH's edit and write refused the call. From 1.4.3 GPT tool definitions carry an explicit `strict: false`. The model usually finishes the step another way, and existing sessions need nothing.
|
|
307
311
|
|
|
308
|
-
**A session says `pi-ai provider "packycode" has no configured model "gemini-4-argon"`?** PackyCode stopped serving Gemini 4 Argon on 2026-10-27: the gateway refuses it, and the plugin removed it from its catalog. Pick another model for that session in the model picker.
|
|
309
|
-
|
|
310
312
|
**Claude reports an official-client-only or client-anomaly refusal?** This is a gateway client-policy refusal, not proof of an invalid key. See whether the key is marked “Claude via HTTP” or “via official CLI”: a `cc` key always uses the CLI, and other groups use HTTP. The cc group judges whether a request comes from Claude Code itself; from 1.4.0 cc sessions are carried out by Claude Code and are normally accepted. If one is still refused, resend it; if it keeps happening, send the gateway's own words from the error (with the request id) to PackyCode support, or use a `cc-sale` / `cc-expensive` / `claude-officially` key. Updating the Claude Code CLI does not help with this refusal. The plugin does not disable the key or switch providers.
|
|
311
313
|
|
|
312
314
|
**“Claude Code CLI is not available”?** The key is in `cc`, which needs the CLI on this device. When the message says the key's groups are not read yet, the group reading failed for now (common on an unstable network or proxy): resend shortly, and a key outside `cc` moves to HTTP once its groups are read. Under **Settings → PackyCode → Claude Code CLI** click Install, or run `npm install -g @anthropic-ai/claude-code --allow-scripts=@anthropic-ai/claude-code` and click Check again; set `claudeCliPath` for an unusual location. Otherwise use a `cc-sale` / `cc-expensive` key.
|
|
@@ -325,7 +327,7 @@ Recovery: quit and reopen the desktop application, and the old version works. Th
|
|
|
325
327
|
|
|
326
328
|
**Do I need Claude Code installed?** Only for Claude models on a `cc` key. The plugin runs it with its own config home and never touches your Claude Code login or settings; it updates the installation only when you click **Update Claude Code CLI**. Other groups and models do not need it.
|
|
327
329
|
|
|
328
|
-
**Replies stop at the output token limit (已达到输出 token 上限), or requests keep failing — how do I report it?** Open **Settings → PackyCode → Diagnostics**, pick the session (the 20 most recent, newest first, labelled with their folder name), click **Generate diagnostics** and then **Copy**, and paste the text into an [Issue](https://github.com/BaronCyrus/dsh-packycode-api/issues) or send it to the plugin author. It holds settings and numbers only: the plugin version, the session id and start time, each key's position and groups (no labels, no keys), each request's model, reasoning effort and output limit, the latest steps' token usage and finish reasons, and retry and failure codes (messages redacted and shortened). It contains no conversation text, file paths or keys. Where copying is unavailable, the text is selected instead; press ⌘C or Ctrl+C.
|
|
330
|
+
**Replies stop at the output token limit (已达到输出 token 上限), or requests keep failing — how do I report it?** Open **Settings → PackyCode → Diagnostics**, pick the session (the 20 most recent, newest first, labelled with their folder name), click **Generate diagnostics** and then **Copy**, and paste the text into an [Issue](https://github.com/BaronCyrus/dsh-packycode-api/issues) or send it to the plugin author. It holds settings and numbers only: the plugin version, the session id and start time, each key's position and groups (no labels, no keys), each request's model, reasoning effort and output limit, the latest steps' token usage and finish reasons, and retry and failure codes (messages redacted and shortened). A `network` section covers this plugin's connections to PackyCode's servers since it loaded, across sessions: the Node version, its connection settings (address family autoselection and how long each address gets) and which scheme each proxy variable names (never the proxy itself), then for each server the addresses it resolves to now, its requests, statuses, failures, cancellations and time to response headers, and for the 12 latest failures the stage (connect, request or mid-response), the time taken, the underlying error codes and how each tried address failed. It contains no conversation text, file paths or keys. Where copying is unavailable, the text is selected instead; press ⌘C or Ctrl+C.
|
|
329
331
|
|
|
330
332
|
## Security and scope
|
|
331
333
|
|
package/README.md
CHANGED
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
打开桌面应用的**插件**页,点击**添加插件**,在**包名或地址**中填入**带版本号**的包名:
|
|
49
49
|
|
|
50
50
|
```text
|
|
51
|
-
dsh-packycode-api@1.4.
|
|
51
|
+
dsh-packycode-api@1.4.6
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
在**安装源**中选择**官方源**,然后点击**安装**。`1.4.
|
|
54
|
+
在**安装源**中选择**官方源**,然后点击**安装**。`1.4.6` 是写这份说明时的最新版,当前最新版本号以 [npm 页面](https://www.npmjs.com/package/dsh-packycode-api) 为准。
|
|
55
55
|
|
|
56
56
|
> **一定要带版本号。** 只填 `dsh-packycode-api` 时,安装前的提示显示的是 npm 上的最新版,但 DSH 自带的 pnpm 默认只安装发布满 24 小时的版本,实际装上的可能是好几个版本之前的旧版,而安装提示里看不出这一点。填写精确版本时,pnpm 会直接安装这个版本,并自动把它加入 profile 的 `minimumReleaseAgeExclude`。装好后可以在 **设置 → PackyCode → 插件版本** 核对「当前版本」。
|
|
57
57
|
|
|
@@ -65,14 +65,14 @@ dsh-packycode-api@1.4.4
|
|
|
65
65
|
|
|
66
66
|
### 3. 选择模型
|
|
67
67
|
|
|
68
|
-
回到对话,在模型选择器里选 `PackyCode`
|
|
68
|
+
回到对话,在模型选择器里选 `PackyCode` 分组下的模型。十三个模型里,Claude 在 `cc` 分组的 Key 上由本机 Claude Code CLI 执行,其余都通过插件内 HTTP 调用;Key 的分组必须覆盖所选模型。
|
|
69
69
|
|
|
70
70
|
<details>
|
|
71
71
|
<summary>检查安装 / 其它 profile / 本地目录</summary>
|
|
72
72
|
|
|
73
73
|
桌面应用的**插件**页可以看到已安装状态与版本,也可以在那里启用、停用或卸载。
|
|
74
74
|
|
|
75
|
-
**仅用于非 desktop profile:** `dsh plugin --profile <name> add dsh-packycode-api@1.4.
|
|
75
|
+
**仅用于非 desktop profile:** `dsh plugin --profile <name> add dsh-packycode-api@1.4.6`。不要对 `desktop` 使用它 —— CLI 会直接拒绝(`profile "desktop" is managed exclusively by the Electron application`)。
|
|
76
76
|
|
|
77
77
|
本地开发要加 `link:` 前缀:`dsh plugin --profile <name> add link:/path/to/dsh-packycode-api`。**不要直接写路径(或 `file:`)** —— 那样的安装会把整个仓库目录(连同 `node_modules`)复制进 profile,之后它会在 profile 内部挡住 DSH 自带的包,插件看起来像没装,见「常见问题」。已经那样装过的话,先卸载再按上面的方式重装。
|
|
78
78
|
|
|
@@ -86,7 +86,7 @@ dsh-packycode-api@1.4.4
|
|
|
86
86
|
|
|
87
87
|
**换 Key。** Key 按添加顺序使用。点「切到此 Key」指定优先使用的 Key,下一轮就生效;「重命名」改备注(最多 60 字,不能为空);充值或更换后点「重新启用」。
|
|
88
88
|
|
|
89
|
-
**看分组。** 「账户与 Key」里每个 Key 下面列出它所在的 PackyCode 分组(鼠标悬停可看分组说明),以及能用哪些模型:✓ 可用、◐ 部分可用、✕ 不可用。分组由网关自己报出(对一个不存在的模型 id 的拒绝里会写明「已选分组」,单分组的 Key 则是「分组 … 下模型 … 无可用渠道」,不产生费用),再对照 PackyCode 公开的模型分组表。Claude 的分组覆盖要按具体模型确认,不能假定 `cc-sale` 支持全部三个型号;GPT 需要 `codex` / `codex-sale`;DeepSeek 需要 `deepseek-officially` / `deepseek-sale`。
|
|
89
|
+
**看分组。** 「账户与 Key」里每个 Key 下面列出它所在的 PackyCode 分组(鼠标悬停可看分组说明),以及能用哪些模型:✓ 可用、◐ 部分可用、✕ 不可用。分组由网关自己报出(对一个不存在的模型 id 的拒绝里会写明「已选分组」,单分组的 Key 则是「分组 … 下模型 … 无可用渠道」,不产生费用),再对照 PackyCode 公开的模型分组表。Claude 的分组覆盖要按具体模型确认,不能假定 `cc-sale` 支持全部三个型号;GPT 需要 `codex` / `codex-sale`;DeepSeek 需要 `deepseek-officially` / `deepseek-sale`;Hy4 需要 `hunyuan-officially`。
|
|
90
90
|
|
|
91
91
|
**看余额。** 「账户与 Key」里每个 Key 显示它自己的剩余、已用与额度(额度 = 剩余 + 已用),不限额的 Key 显示「不限额度」;多个 Key 不相加成合计。API Key 只能读到自己的额度,读不到 PackyCode 账号的总余额,账号余额请到 PackyCode 控制台查看。读失败的 Key 会在它那一行注明原因;某个 Key 这次读失败但之前读到过,会继续显示上次的余额并注明读取时间(输入框里显示为 `≈US$…`),重启 DSH 后才清空。输入框右侧只在选中 PackyCode 模型时显示当前 Key 的剩余额度。「刷新」会跳过约 60 秒的缓存。默认模型接口是 `https://cf.api.fan`,余额接口是 `https://www.packyapi.ai`,可在「高级:接口地址」里改。旧默认值 `https://www.packyapi.com` 在国内网络下 DNS 被污染、直连会 `fetch failed`,即使设置里保存的是它,插件也会先试 `.ai`。
|
|
92
92
|
|
|
@@ -227,6 +227,8 @@ DSH 自带一个实验性插件「自动授权审查」,默认关闭。开启
|
|
|
227
227
|
| `gpt-6-astra` | 同上 | 不能关;`low` / `medium` / `high` / `xhigh` / `max` | 同上 |
|
|
228
228
|
| `gpt-5.6-sol` | 同上 | 同上 | 同上 |
|
|
229
229
|
| `grok-4.7`(Grok 4.7) | HTTP,Responses,可带图;500k 上下文 | 不能关;`low` / `medium` / `high` / `xhigh` | 以设置页从网关读取的 Grok 分组覆盖为准 |
|
|
230
|
+
| `hy4`(Hy4,腾讯混元) | HTTP,Responses,可带图;1024K 上下文(输入最多 960K),64K 输出 | 可关;只有 `high` | `hunyuan-officially` |
|
|
231
|
+
| `gemini-4-argon`(Gemini 4 Argon) | HTTP,Gemini 原生流式,可带图;1,048,576 上下文,65,536 输出 | 不能关;`low` / `medium` / `high` | `gemini-slb` |
|
|
230
232
|
| `gemini-3.8-flash`(Gemini 3.8 Flash) | HTTP,Gemini 原生流式,可带图;1,048,576 输入,65,536 输出 | 不能关;`low` / `medium` / `high` | `gemini-slb` |
|
|
231
233
|
| `claude-opus-5-5` | Anthropic Messages,可带图和原生工具:`cc` 分组由本机 Claude Code CLI 当 agent,其它分组走 HTTP | 不能关;`low` / `medium` / `high` / `xhigh` / `max` | 以设置页读取的 Claude 分组覆盖为准 |
|
|
232
234
|
| `claude-sonnet-5-5` | 同上 | 同上 | 同上 |
|
|
@@ -236,15 +238,17 @@ DSH 自带一个实验性插件「自动授权审查」,默认关闭。开启
|
|
|
236
238
|
|
|
237
239
|
`jev-latest`(TypeSafe Jev)不在模型选择器里:它是只回答分类问题的决策模型,不能对话,只用于[自动审查](#自动审查)。插件在宿主侧直接调用 `/v1/systemone`,需要 Key 在 `typesafe-officially` 分组;「账户与 Key」里每个 Key 的「Jev」一项会显示能否使用。
|
|
238
240
|
|
|
239
|
-
当前目录的
|
|
241
|
+
当前目录的 13 个模型中,除 DeepSeek V4 Pro 外都声明了文字和图像输入。DeepSeek Flash 的识图能力按 [官方图像理解文档](https://api-docs.deepseek.com/zh-cn/guides/vision/) 配置;图片通过 DSH 附件服务读取,再随 Responses 请求发送。DeepSeek V4 Pro 按 [官方模型与价格页](https://api-docs.deepseek.com/zh-cn/quick_start/pricing/) 只收文字:DSH 不让给它附图,会话里已有的图片(比如从 Flash 切过来的会话)会换成一行「图片已省略」的说明再发送。PackyCode 网关收到发给 V4 Pro 的图片不报错,而是直接丢掉。
|
|
240
242
|
|
|
241
|
-
Gemini
|
|
243
|
+
Gemini 的能力与限制来自 Google 官方模型页:[Gemini 4 Argon](https://ai.google.dev/gemini-api/docs/models/gemini-4-argon)、[Gemini 3.8 Flash](https://ai.google.dev/gemini-api/docs/models/gemini-3.8-flash)。两款模型统一使用 Gemini 原生协议,通过 `/v1beta/models/{model}:streamGenerateContent` 流式返回文字、思考和工具调用,图片仍由 DSH 附件服务读取。思考档位通过 `thinkingLevel` 传入。两者均使用单次流式输出上限,不执行超过 65,536 token 的自动续写。可用端点与分组以 [PackyAPI 实时模型目录](https://www.packyapi.com/api/pricing) 为准。
|
|
242
244
|
|
|
243
|
-
**Gemini 工具续聊的思维签名:** PackyCode 的 Gemini 原生通道在工具调用场景下不能正确处理思维签名,会返回 HTTP 400 `Invalid thought signature` / `Corrupted thought signature`,原样重发也无济于事(PackyCode 客服的答复)。因此插件对 Gemini 3.8 Flash 不再回传模型返回的签名,而是按 [Google 文档](https://ai.google.dev/gemini-api/docs/thought-signatures) 在需要签名的位置填入跳过校验的值 `skip_thought_signature_validator`。协议、端点和模型都不变,DSH 的回放记录仍保留原始签名。Google 提示跳过签名可能削弱多步工具调用之间的推理连贯性。Flash 仍保留原有兜底:HTTP 400 签名拒绝且尚未输出任何流事件时,等待 1 秒原样重发一次。这类 400 不会被当作 Key 失效。
|
|
245
|
+
**Gemini 工具续聊的思维签名:** PackyCode 的 Gemini 原生通道在工具调用场景下不能正确处理思维签名,会返回 HTTP 400 `Invalid thought signature` / `Corrupted thought signature`,原样重发也无济于事(PackyCode 客服的答复)。因此插件对 Gemini 4 Argon 和 Gemini 3.8 Flash 不再回传模型返回的签名,而是按 [Google 文档](https://ai.google.dev/gemini-api/docs/thought-signatures) 在需要签名的位置填入跳过校验的值 `skip_thought_signature_validator`。协议、端点和模型都不变,DSH 的回放记录仍保留原始签名。Google 提示跳过签名可能削弱多步工具调用之间的推理连贯性。Flash 仍保留原有兜底:HTTP 400 签名拒绝且尚未输出任何流事件时,等待 1 秒原样重发一次。这类 400 不会被当作 Key 失效。
|
|
244
246
|
|
|
245
247
|
Grok 4.7 的上下文与推理档位来自 [官方文档](https://docs.x.ai/developers/grok-4-7)。官方没有单独限制文字输出长度;插件将输出上限设为上下文大小,并按每次请求的剩余上下文收紧。
|
|
246
248
|
|
|
247
|
-
|
|
249
|
+
Hy4 的协议、上下文、输出上限和识图能力来自腾讯云 TokenHub 的 [混元调用指南](https://cloud.tencent.com/document/product/1823/132252),推理档位来自 [深度思考](https://cloud.tencent.com/document/product/1823/131208):Hy4 默认开启思考,`reasoning_effort` 只有 `high` 和 `no_think` 两档。插件按 `high` 发送,关闭推理时发送 `reasoning.effort: "no_think"`,返回时不带思考。网关也接受 `low`、`medium`、`xhigh`、`max`,但 Hy4 的文档没有这些档位,实测时它们的提示 token 与 `high` 相同,推理长度也看不出差别,所以插件不提供。图片和 DeepSeek Flash 一样由 DSH 附件服务读取,随 Responses 请求发送。推理以 `summary_text` 条目返回;按 TokenHub [保留式思考](https://cloud.tencent.com/document/product/1823/133534) 的要求,插件把推理原样回传给工具续聊和之后的轮次。DSH 的系统提示词只写了「powered by DeepSeek Harness」和「powered by the hy4 model」,没说 hy4 是谁家的模型,hy4 会因此自称 DeepSeek 的模型、否认自己是混元(其它模型都认得出自己的模型名)。所以插件在 hy4 每个请求的系统提示词末尾补一句英文说明:hy4 是腾讯混元的 Hy4,经 PackyCode 接入,DeepSeek Harness 只是运行它的应用。DSH 自己的提示词原样保留,其它模型的请求不受影响。
|
|
250
|
+
|
|
251
|
+
Responses 请求只有 GPT 模型保留 `prompt_cache_key`,其他 Responses 模型默认省略。当前网关的 Grok 路由会将这个字段误报成 `Incorrect API key provided`;DeepSeek Flash 和 Hy4 不带它也能命中前缀缓存。GPT 的工具定义还会显式带上 `strict: false`:PackyCode 的 GPT 通道把没写 `strict` 的工具当作严格模式,模型会被迫填满所有可选参数(见「常见问题」)。
|
|
248
252
|
|
|
249
253
|
GPT-6.1 Terra 的上下文、输出上限和推理档位来自 [OpenAI 模型页](https://developers.openai.com/api/docs/models/gpt-6.1-terra)。它是唯一可以关闭推理的 GPT 模型:关闭时发送 `reasoning.effort: "none"`,网关原样转发,返回时不带思考。GPT-5.6 Sol 的文档虽然也列出 `none`,经 PackyCode 却按 `medium` 回答,所以不能关。
|
|
250
254
|
|
|
@@ -271,9 +275,9 @@ DeepSeek Flash 走网关的 Responses 接口(`/api/pricing` 列出 `openai-res
|
|
|
271
275
|
|
|
272
276
|
## 更新与卸载
|
|
273
277
|
|
|
274
|
-
> **从 1.3.2 或更早版本升级:** 请在插件页填写 `dsh-packycode-api@1.4.
|
|
278
|
+
> **从 1.3.2 或更早版本升级:** 请在插件页填写 `dsh-packycode-api@1.4.6` 安装,不要点旧版设置页里的「更新插件」。旧按钮安装时会绕过 pnpm 的发布冷却期:装到的版本如果发布还不满 24 小时,这个 profile 在它满 24 小时之前就装不了其他插件。装好并重启后,按钮就换成了修好的版本,以后直接点即可。
|
|
275
279
|
|
|
276
|
-
**更新:** 打开 **设置 → PackyCode**,有新版本时点「更新插件」。它会在当前 profile 目录里用 DSH 自带的 pnpm 安装精确版本,并读回 `package.json` 确认版本落地。安装遵守 pnpm 的发布冷却策略,不会绕过它:默认情况下 pnpm 会直接装上这个版本,并把它记进 profile 的 `minimumReleaseAgeExclude`;如果你给 pnpm 配了更严格的策略,按钮会提示什么时候可以更新。新版本号是从 npm 读到的,pnpm 却从 profile 配置的安装源下载:刚发布的版本,安装源(尤其是国内镜像)可能还没同步到,这时按钮会提示过几分钟再更新;连不上安装源时,会提示检查网络或代理。本地 `link:` 开发安装不会出现这个按钮。完成后完全退出并重新打开桌面应用。只刷新页面不会重载 Host。也可以在**插件**页重新安装,同样要填写带版本号的包名(例如 `dsh-packycode-api@1.4.
|
|
280
|
+
**更新:** 打开 **设置 → PackyCode**,有新版本时点「更新插件」。它会在当前 profile 目录里用 DSH 自带的 pnpm 安装精确版本,并读回 `package.json` 确认版本落地。安装遵守 pnpm 的发布冷却策略,不会绕过它:默认情况下 pnpm 会直接装上这个版本,并把它记进 profile 的 `minimumReleaseAgeExclude`;如果你给 pnpm 配了更严格的策略,按钮会提示什么时候可以更新。新版本号是从 npm 读到的,pnpm 却从 profile 配置的安装源下载:刚发布的版本,安装源(尤其是国内镜像)可能还没同步到,这时按钮会提示过几分钟再更新;连不上安装源时,会提示检查网络或代理。本地 `link:` 开发安装不会出现这个按钮。完成后完全退出并重新打开桌面应用。只刷新页面不会重载 Host。也可以在**插件**页重新安装,同样要填写带版本号的包名(例如 `dsh-packycode-api@1.4.6`),否则装上的可能是发布满 24 小时的旧版。
|
|
277
281
|
|
|
278
282
|
**从本地路径安装切回官方源时,必须先卸载。** 「更新」是 `pnpm add`,它只重写包目录里的文件,不会删除更早一次本地路径安装留在包目录内的 `node_modules`;那份旧依赖会继续挡住 DSH 自带的包,插件行加载失败且看不出原因。「卸载」执行的是 `pnpm remove`,会删掉整个包目录,重装才是干净的。用 `node scripts/install-check.mjs` 可以确认当前 profile 是否处于这个状态。
|
|
279
283
|
|
|
@@ -289,7 +293,7 @@ ls ~/.dsh/profiles/desktop/node_modules/dsh-packycode-api/node_modules
|
|
|
289
293
|
|
|
290
294
|
有输出就是残留:从 npm 安装的包不会自带 `node_modules`,这一份通常来自更早一次「本地路径」安装(`file:` 或直接写路径,而不是 `link:`)。它位于 profile 目录内部,而 DSH 解析插件导入时优先使用 profile 内的副本,所以插件拿到的仍是那份旧依赖 —— 例如 `@deepseek-ai/schemastery` 3.18.2 没有 `Schema.volatile()`,模块在导入阶段就抛错。导入失败时 Loader 不会为这一行创建 fiber,客户端 bundle 也不会被组合,于是模型路由、RPC 通道和设置页全都不会出现。
|
|
291
295
|
|
|
292
|
-
修复:到**插件**页**卸载**本插件,再用带版本号的包名重新**安装**(例如 `dsh-packycode-api@1.4.
|
|
296
|
+
修复:到**插件**页**卸载**本插件,再用带版本号的包名重新**安装**(例如 `dsh-packycode-api@1.4.6`),然后完全退出并重新打开桌面应用。卸载执行的是 `pnpm remove`,会删掉整个包目录;直接重装或点「更新」只重写文件,不会清掉这份残留。也可以手动删掉上面那个 `node_modules` 再重启。
|
|
293
297
|
|
|
294
298
|
**首次安装后,设置页每张卡片都显示 `transport failure for /packycode-api/…: HTTP 405`?** 实际装上的是 1.1.6 或更早的版本,通常是因为安装时只填了包名:安装提示显示的是最新版,pnpm 实际装的却是发布满 24 小时的旧版(见「安装插件」)。这些旧版要等 DSH 重启、重建它的 `connection` 服务之后,才能注册设置通道。所以首次安装后 `/packycode-api` 通道还不存在,请求落到静态页面,返回 405;模型路由和设置页本身照常加载。1.1.7 起首次安装不需要重启。先确认装上的版本:
|
|
295
299
|
|
|
@@ -303,8 +307,6 @@ grep '"version"' ~/.dsh/profiles/desktop/node_modules/dsh-packycode-api/package.
|
|
|
303
307
|
|
|
304
308
|
**GPT 模型改文件时报 `invalid justification: expected a non-empty sentence`?** 这是 1.4.2 及更早版本的问题,更新到 1.4.3 即可。PackyCode 的 GPT 通道把没写 `strict` 的工具当作严格模式,模型被迫填满所有可选参数,把 DSH 的沙箱提权参数填成 `sandbox_permissions: "workspace-write"` 加空的 `justification`,DSH 的 edit / write 于是拒绝这次调用。1.4.3 起 GPT 的工具定义显式带上 `strict: false`。模型通常会换一种方式完成这一步,已有会话不需要处理。
|
|
305
309
|
|
|
306
|
-
**会话报 `pi-ai provider "packycode" has no configured model "gemini-4-argon"`?** PackyCode 自 2026-10-27 起不再提供 Gemini 4 Argon,网关拒绝这个模型,插件也从目录中移除了它。在模型选择器里给这个会话换一个模型即可。
|
|
307
|
-
|
|
308
310
|
**Claude 报「只接受官方 Claude Code CLI」或「客户端存在异常」?** 这是网关的客户端政策拒绝,不等于 Key 失效。看这个 Key 下方标的是「Claude 经 HTTP」还是「经官方 CLI」:`cc` 分组的 Key 总是走 CLI,其它分组走 HTTP。cc 分组按请求内容判断是不是 Claude Code 自己在工作;1.4.0 起 cc 会话由 Claude Code 自己完成,正常不会被拒。偶尔仍被拒时重发一次;反复出现时,把错误里的网关原文(含 request id)发给 PackyCode 客服,或改用 `cc-sale` / `cc-expensive` / `claude-officially` 分组的 Key。更新 Claude Code CLI 不能解决这个拒绝。插件不会因此禁用 Key 或切换提供商。
|
|
309
311
|
|
|
310
312
|
**提示「本机没有可用的 Claude Code CLI」?** 当前 Key 在 `cc` 分组,需要本机 CLI。提示里写着「插件还没读到当前 Key 的分组」时,是分组读取暂时失败(网络或代理不稳时常见):稍后重发即可,不在 `cc` 分组的 Key 读到分组后会自动改走 HTTP。到 **设置 → PackyCode → Claude Code CLI** 点「安装」,或在终端执行 `npm install -g @anthropic-ai/claude-code --allow-scripts=@anthropic-ai/claude-code` 后点「重新检测」;装在非常规位置时可在配置里填 `claudeCliPath`。不想装的话,换用 `cc-sale` / `cc-expensive` 分组的 Key。
|
|
@@ -325,7 +327,7 @@ grep '"version"' ~/.dsh/profiles/desktop/node_modules/dsh-packycode-api/package.
|
|
|
325
327
|
|
|
326
328
|
**需要安装 Claude Code 吗?** 只有 Key 在 `cc` 分组、又要用 Claude 模型时需要。插件用独立配置目录运行它,不改动你自己的 Claude Code 登录和设置;只有你点「更新 Claude Code CLI」时才会更新这份安装。其它分组和其它模型都不需要。
|
|
327
329
|
|
|
328
|
-
**回答被截断(「已达到输出 token 上限」)、请求反复失败,怎么反馈?** 打开 **设置 → PackyCode → 诊断信息**,选出问题的会话(最近 20 个,按时间倒序,标着所在文件夹名),点「生成诊断信息」再点「复制」,把内容贴到 [Issue](https://github.com/BaronCyrus/dsh-packycode-api/issues) 或发给插件作者。内容只有设置和数字:插件版本、会话 id 与创建时间、每个 Key 的序号与分组(不含备注名和 Key 本身)、每次请求用的模型、推理档位和输出上限、最近若干步的 token
|
|
330
|
+
**回答被截断(「已达到输出 token 上限」)、请求反复失败,怎么反馈?** 打开 **设置 → PackyCode → 诊断信息**,选出问题的会话(最近 20 个,按时间倒序,标着所在文件夹名),点「生成诊断信息」再点「复制」,把内容贴到 [Issue](https://github.com/BaronCyrus/dsh-packycode-api/issues) 或发给插件作者。内容只有设置和数字:插件版本、会话 id 与创建时间、每个 Key 的序号与分组(不含备注名和 Key 本身)、每次请求用的模型、推理档位和输出上限、最近若干步的 token 用量与结束原因,以及重试和失败的错误码(错误信息已脱敏并截短)。另有一段 `network`,记录插件这次运行以来连接 PackyCode 服务器的情况(不分会话):Node 版本、连接设置(地址族自动选择及每个地址的等待时间)和代理变量的协议类型(不含代理地址),每个服务器当前解析到的地址、请求数、各状态码次数、失败与中止次数、响应头耗时,以及最近 12 次失败各自的阶段(连接、请求或响应中途)、耗时、底层错误码和每个尝试地址的结果。不含对话内容、文件路径和 Key。复制不可用时,文本会被全选,按 ⌘C 或 Ctrl+C 即可。
|
|
329
331
|
|
|
330
332
|
## 安全边界
|
|
331
333
|
|
package/lib/client.js
CHANGED
|
@@ -216,7 +216,7 @@ window.__ModuleLoader__.load({
|
|
|
216
216
|
diagCopy: "复制",
|
|
217
217
|
diagCopied: "已复制",
|
|
218
218
|
diagSelected: "已选中全部内容,请按 ⌘C 或 Ctrl+C 复制。",
|
|
219
|
-
diagPrivacy: "只包含模型、推理档位、输出上限、每一步的 token
|
|
219
|
+
diagPrivacy: "只包含模型、推理档位、输出上限、每一步的 token 用量和错误码,以及本次运行中插件连接 PackyCode 服务器的情况(服务器地址、状态码、错误码、耗时和 Node 的连接设置),不含对话内容、文件路径和 Key。",
|
|
220
220
|
diagNoSessions: "还没有会话。",
|
|
221
221
|
diagLive: "进行中",
|
|
222
222
|
diagNoFolder: "无目录",
|
|
@@ -449,7 +449,7 @@ window.__ModuleLoader__.load({
|
|
|
449
449
|
diagCopy: "Copy",
|
|
450
450
|
diagCopied: "Copied",
|
|
451
451
|
diagSelected: "All text is selected; press ⌘C or Ctrl+C to copy.",
|
|
452
|
-
diagPrivacy: "Contains only models, reasoning effort, output limits, per-step token usage and error codes — no conversation text, file paths or keys.",
|
|
452
|
+
diagPrivacy: "Contains only models, reasoning effort, output limits, per-step token usage and error codes, plus how this run's connections to PackyCode's servers went (server addresses, statuses, error codes, timings and Node's connection settings) — no conversation text, file paths or keys.",
|
|
453
453
|
diagNoSessions: "No sessions yet.",
|
|
454
454
|
diagLive: "open",
|
|
455
455
|
diagNoFolder: "no folder",
|
|
@@ -1959,7 +1959,8 @@ window.__ModuleLoader__.load({
|
|
|
1959
1959
|
"gpt-5.6-sol"
|
|
1960
1960
|
]],
|
|
1961
1961
|
["Grok", ["grok-4.7"]],
|
|
1962
|
-
["
|
|
1962
|
+
["Hunyuan", ["hy4"]],
|
|
1963
|
+
["Gemini", ["gemini-4-argon", "gemini-3.8-flash"]],
|
|
1963
1964
|
["Claude", [
|
|
1964
1965
|
"claude-opus-5-5",
|
|
1965
1966
|
"claude-sonnet-5-5",
|
package/lib/index.js
CHANGED
|
@@ -12,6 +12,9 @@ import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messag
|
|
|
12
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
13
|
import { HarnessError, LlmError, QUOTA_EXCEEDED_CODE, createUserMessage, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
14
14
|
import { credentialKey } from "@deepseek-ai/dsh-credentials";
|
|
15
|
+
import { subscribe, unsubscribe } from "node:diagnostics_channel";
|
|
16
|
+
import * as dns from "node:dns";
|
|
17
|
+
import * as net from "node:net";
|
|
15
18
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
16
19
|
import { lazyStream } from "@earendil-works/pi-ai/api/lazy";
|
|
17
20
|
import { clampThinkingLevel, createProvider } from "@earendil-works/pi-ai";
|
|
@@ -1080,8 +1083,10 @@ const DEFAULT_REASONING_LEVEL = "high";
|
|
|
1080
1083
|
* Astra and GPT-5.6 Sol `reasoning.effort` (developers.openai.com model
|
|
1081
1084
|
* pages), DeepSeek thinking mode (`low|high|max` effort, or off, for Flash
|
|
1082
1085
|
* and V4 Pro alike), Grok 4.7
|
|
1083
|
-
* (docs.x.ai/developers/grok-4-7),
|
|
1084
|
-
*
|
|
1086
|
+
* (docs.x.ai/developers/grok-4-7), Hy4 (`high` or `no_think`, TokenHub's
|
|
1087
|
+
* deep-thinking page cloud.tencent.com/document/product/1823/131208), and
|
|
1088
|
+
* Gemini 4 Argon and 3.8 Flash
|
|
1089
|
+
* (ai.google.dev/gemini-api/docs/models/gemini-4-argon and gemini-3.8-flash).
|
|
1085
1090
|
*
|
|
1086
1091
|
* @module dsh-packycode-api/catalog
|
|
1087
1092
|
*/
|
|
@@ -1101,8 +1106,8 @@ const GEMINI_BASE = `${DEFAULT_API_HOST}/v1beta`;
|
|
|
1101
1106
|
* escalation pair then came back as `workspace-write` with an empty
|
|
1102
1107
|
* justification, which DSH's edit and write refuse. Declaring strict-mode
|
|
1103
1108
|
* support makes pi-ai send `strict: false` on each tool, as the official
|
|
1104
|
-
* Codex client does; the schemas stay as DSH wrote them. DeepSeek and
|
|
1105
|
-
*
|
|
1109
|
+
* Codex client does; the schemas stay as DSH wrote them. DeepSeek, Grok and
|
|
1110
|
+
* Hy4 fill optional parameters no more often without `strict` than with it.
|
|
1106
1111
|
*/
|
|
1107
1112
|
const GPT_COMPAT = Object.freeze({ supportsStrictMode: true });
|
|
1108
1113
|
/**
|
|
@@ -1152,7 +1157,23 @@ const GROK_LEVELS = Object.freeze({
|
|
|
1152
1157
|
...ALWAYS_THINKING_LEVELS,
|
|
1153
1158
|
max: null
|
|
1154
1159
|
});
|
|
1155
|
-
/**
|
|
1160
|
+
/**
|
|
1161
|
+
* Hy4 documents `reasoning_effort` high | no_think and nothing else, with
|
|
1162
|
+
* thinking on by default. Through PackyCode `no_think` answers without
|
|
1163
|
+
* reasoning, so `off` sends it. The gateway does not refuse low, medium, xhigh
|
|
1164
|
+
* or max, but they are undocumented, and they came back with high's prompt
|
|
1165
|
+
* tokens and no consistent difference in reasoning, so they are not offered.
|
|
1166
|
+
*/
|
|
1167
|
+
const HY4_LEVELS = Object.freeze({
|
|
1168
|
+
off: "no_think",
|
|
1169
|
+
minimal: null,
|
|
1170
|
+
low: null,
|
|
1171
|
+
medium: null,
|
|
1172
|
+
high: "high",
|
|
1173
|
+
xhigh: null,
|
|
1174
|
+
max: null
|
|
1175
|
+
});
|
|
1176
|
+
/** Gemini 4 Argon and 3.8 Flash support low | medium | high, with no off. */
|
|
1156
1177
|
const GEMINI_LEVELS = Object.freeze({
|
|
1157
1178
|
...ALWAYS_THINKING_LEVELS,
|
|
1158
1179
|
xhigh: null,
|
|
@@ -1255,6 +1276,32 @@ const PACKYCODE_MODELS = Object.freeze([
|
|
|
1255
1276
|
thinkingLevelMap: GROK_LEVELS,
|
|
1256
1277
|
compat: Object.freeze({ supportsLongCacheRetention: false })
|
|
1257
1278
|
}),
|
|
1279
|
+
Object.freeze({
|
|
1280
|
+
id: "hy4",
|
|
1281
|
+
name: "Hy4",
|
|
1282
|
+
api: "openai-responses",
|
|
1283
|
+
provider: PROVIDER,
|
|
1284
|
+
baseUrl: RESPONSES_BASE,
|
|
1285
|
+
reasoning: true,
|
|
1286
|
+
input: Object.freeze(["text", "image"]),
|
|
1287
|
+
cost: NO_COST,
|
|
1288
|
+
contextWindow: 1024e3,
|
|
1289
|
+
maxTokens: 64e3,
|
|
1290
|
+
thinkingLevelMap: HY4_LEVELS
|
|
1291
|
+
}),
|
|
1292
|
+
Object.freeze({
|
|
1293
|
+
id: "gemini-4-argon",
|
|
1294
|
+
name: "Gemini 4 Argon",
|
|
1295
|
+
api: "google-generative-ai",
|
|
1296
|
+
provider: PROVIDER,
|
|
1297
|
+
baseUrl: GEMINI_BASE,
|
|
1298
|
+
reasoning: true,
|
|
1299
|
+
input: Object.freeze(["text", "image"]),
|
|
1300
|
+
cost: NO_COST,
|
|
1301
|
+
contextWindow: 1048576,
|
|
1302
|
+
maxTokens: 65536,
|
|
1303
|
+
thinkingLevelMap: GEMINI_LEVELS
|
|
1304
|
+
}),
|
|
1258
1305
|
Object.freeze({
|
|
1259
1306
|
id: "gemini-3.8-flash",
|
|
1260
1307
|
name: "Gemini 3.8 Flash",
|
|
@@ -6255,6 +6302,10 @@ function claudeTransportFor({ groups } = {}) {
|
|
|
6255
6302
|
* each request used, how every model step ended and what it consumed, and
|
|
6256
6303
|
* which retries and failures happened.
|
|
6257
6304
|
*
|
|
6305
|
+
* The report also carries how this process reached PackyCode's servers
|
|
6306
|
+
* (`network.js`): it is process-wide, not per session, and starts when the
|
|
6307
|
+
* plugin loads.
|
|
6308
|
+
*
|
|
6258
6309
|
* The summary carries numbers and identifiers only. Message text, reasoning,
|
|
6259
6310
|
* tool arguments and results, titles, paths and key material never enter it;
|
|
6260
6311
|
* error messages are redacted and shortened. A user can paste it into a bug
|
|
@@ -6277,12 +6328,12 @@ const DIAGNOSTICS_LIMITS = Object.freeze({
|
|
|
6277
6328
|
});
|
|
6278
6329
|
const finite = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
6279
6330
|
/** Identifier-like strings only (routes, models, efforts, reason kinds, codes). */
|
|
6280
|
-
const word = (value) => typeof value === "string" && /^[\w.:/@[\]-]{1,80}$/u.test(value) ? value : void 0;
|
|
6331
|
+
const word$1 = (value) => typeof value === "string" && /^[\w.:/@[\]-]{1,80}$/u.test(value) ? value : void 0;
|
|
6281
6332
|
const iso = (value) => finite(value) === void 0 ? void 0 : new Date(value).toISOString();
|
|
6282
6333
|
const last = (list, count) => list.slice(Math.max(0, list.length - count));
|
|
6283
|
-
const compact = (object) => Object.fromEntries(Object.entries(object).filter(([, value]) => value !== void 0));
|
|
6334
|
+
const compact$1 = (object) => Object.fromEntries(Object.entries(object).filter(([, value]) => value !== void 0));
|
|
6284
6335
|
const chunksOf = (stream) => Array.isArray(stream) ? stream.map((item) => item?.chunk).filter((chunk) => chunk !== null && typeof chunk === "object") : [];
|
|
6285
|
-
/** A provider or
|
|
6336
|
+
/** A provider, harness or transport error message, redacted and shortened. */
|
|
6286
6337
|
function safeMessage$1(text, secrets) {
|
|
6287
6338
|
if (typeof text !== "string" || text === "") return void 0;
|
|
6288
6339
|
const safe = redactSecrets(text, secrets).replace(/sk-[A-Za-z0-9_-]{8,}/gu, "sk-[redacted]").replace(/Bearer\s+[A-Za-z0-9._~+/=-]{8,}/giu, "Bearer [redacted]").replace(/\s+/gu, " ").trim();
|
|
@@ -6291,7 +6342,7 @@ function safeMessage$1(text, secrets) {
|
|
|
6291
6342
|
function failureOf(reason, secrets) {
|
|
6292
6343
|
const failure = reason?.failure ?? reason?.error;
|
|
6293
6344
|
return failure === void 0 ? {} : {
|
|
6294
|
-
code: word(failure.code),
|
|
6345
|
+
code: word$1(failure.code),
|
|
6295
6346
|
message: safeMessage$1(failure.message, secrets)
|
|
6296
6347
|
};
|
|
6297
6348
|
}
|
|
@@ -6317,12 +6368,12 @@ function summarizeSessionEvents(events, { secrets = [] } = {}) {
|
|
|
6317
6368
|
if (type === "turn/start") turns += 1;
|
|
6318
6369
|
else if (type === "request/header") {
|
|
6319
6370
|
const config = data.header?.config ?? {};
|
|
6320
|
-
requests.push(compact({
|
|
6371
|
+
requests.push(compact$1({
|
|
6321
6372
|
time: iso(event.time),
|
|
6322
|
-
reason: word(data.reason),
|
|
6323
|
-
provider: word(config.provider),
|
|
6324
|
-
model: word(config.model),
|
|
6325
|
-
reasoningEffort: word(config.reasoningEffort),
|
|
6373
|
+
reason: word$1(data.reason),
|
|
6374
|
+
provider: word$1(config.provider),
|
|
6375
|
+
model: word$1(config.model),
|
|
6376
|
+
reasoningEffort: word$1(config.reasoningEffort),
|
|
6326
6377
|
maxTokens: finite(config.maxTokens),
|
|
6327
6378
|
maxTokensFromAdapter: data.header?.adapterDefaults?.maxTokens === true ? true : void 0
|
|
6328
6379
|
}));
|
|
@@ -6331,11 +6382,11 @@ function summarizeSessionEvents(events, { secrets = [] } = {}) {
|
|
|
6331
6382
|
const usage = data.usage ?? chunks.find((chunk) => chunk.type === "usage")?.usage ?? {};
|
|
6332
6383
|
const input = finite(usage.inputTokens);
|
|
6333
6384
|
const cacheRead = finite(usage.cacheReadTokens);
|
|
6334
|
-
steps.push(compact({
|
|
6385
|
+
steps.push(compact$1({
|
|
6335
6386
|
turn: finite(data.turn),
|
|
6336
6387
|
step: finite(data.step),
|
|
6337
6388
|
time: iso(event.time),
|
|
6338
|
-
finish: word(chunks.find((chunk) => chunk.type === "finish")?.reason?.kind) ?? "none",
|
|
6389
|
+
finish: word$1(chunks.find((chunk) => chunk.type === "finish")?.reason?.kind) ?? "none",
|
|
6339
6390
|
input,
|
|
6340
6391
|
cacheRead,
|
|
6341
6392
|
cacheWrite: finite(usage.cacheWriteTokens),
|
|
@@ -6344,26 +6395,26 @@ function summarizeSessionEvents(events, { secrets = [] } = {}) {
|
|
|
6344
6395
|
}));
|
|
6345
6396
|
} else if (type === "assistant/attempt") {
|
|
6346
6397
|
const reason = chunksOf(data.stream).find((chunk) => chunk.type === "finish")?.reason;
|
|
6347
|
-
attempts.push(compact({
|
|
6398
|
+
attempts.push(compact$1({
|
|
6348
6399
|
turn: finite(data.turn),
|
|
6349
6400
|
step: finite(data.step),
|
|
6350
6401
|
time: iso(event.time),
|
|
6351
|
-
finish: word(reason?.kind),
|
|
6402
|
+
finish: word$1(reason?.kind),
|
|
6352
6403
|
...failureOf(reason, secrets)
|
|
6353
6404
|
}));
|
|
6354
|
-
} else if (type === "llm/retry") retries.push(compact({
|
|
6405
|
+
} else if (type === "llm/retry") retries.push(compact$1({
|
|
6355
6406
|
turn: finite(data.turn),
|
|
6356
6407
|
step: finite(data.step),
|
|
6357
6408
|
time: iso(event.time),
|
|
6358
|
-
provider: word(data.provider),
|
|
6409
|
+
provider: word$1(data.provider),
|
|
6359
6410
|
retry: finite(data.retry),
|
|
6360
6411
|
maxRetries: finite(data.maxRetries),
|
|
6361
6412
|
...failureOf({ failure: data.failure }, secrets)
|
|
6362
6413
|
}));
|
|
6363
|
-
else if (type === "turn/end") turnEnds.push(compact({
|
|
6414
|
+
else if (type === "turn/end") turnEnds.push(compact$1({
|
|
6364
6415
|
turn: finite(data.turn),
|
|
6365
6416
|
time: iso(event.time),
|
|
6366
|
-
kind: word(data.reason?.kind),
|
|
6417
|
+
kind: word$1(data.reason?.kind),
|
|
6367
6418
|
...failureOf(data.reason, secrets)
|
|
6368
6419
|
}));
|
|
6369
6420
|
}
|
|
@@ -6389,9 +6440,10 @@ function summarizeSessionEvents(events, { secrets = [] } = {}) {
|
|
|
6389
6440
|
* The diagnostics service behind the settings card.
|
|
6390
6441
|
* @param options - DSH's session query service (read lazily), the session
|
|
6391
6442
|
* event reader the image originals already use, the key ring and group
|
|
6392
|
-
* reader, the stored secrets to redact,
|
|
6443
|
+
* reader, the stored secrets to redact, this package's manifest URL, and
|
|
6444
|
+
* the network log.
|
|
6393
6445
|
*/
|
|
6394
|
-
function createDiagnostics({ sessionQuery, readEvents, ring, groups, secrets = async () => [], ownPackageJsonUrl }) {
|
|
6446
|
+
function createDiagnostics({ sessionQuery, readEvents, ring, groups, secrets = async () => [], ownPackageJsonUrl, network }) {
|
|
6395
6447
|
const query = () => {
|
|
6396
6448
|
const service = sessionQuery?.();
|
|
6397
6449
|
if (typeof service?.listSessions !== "function") throw new Error("PackyCode diagnostics need DSH session history, which this runtime does not provide");
|
|
@@ -6399,7 +6451,7 @@ function createDiagnostics({ sessionQuery, readEvents, ring, groups, secrets = a
|
|
|
6399
6451
|
};
|
|
6400
6452
|
const version = async () => {
|
|
6401
6453
|
try {
|
|
6402
|
-
return word(JSON.parse(await readFile(fileURLToPath(ownPackageJsonUrl), "utf8")).version);
|
|
6454
|
+
return word$1(JSON.parse(await readFile(fileURLToPath(ownPackageJsonUrl), "utf8")).version);
|
|
6403
6455
|
} catch {
|
|
6404
6456
|
return;
|
|
6405
6457
|
}
|
|
@@ -6415,19 +6467,27 @@ function createDiagnostics({ sessionQuery, readEvents, ring, groups, secrets = a
|
|
|
6415
6467
|
}
|
|
6416
6468
|
return listed.map((entry, index) => {
|
|
6417
6469
|
const group = reading?.keys?.find((item) => item.id === entry.id);
|
|
6418
|
-
return compact({
|
|
6470
|
+
return compact$1({
|
|
6419
6471
|
key: index + 1,
|
|
6420
6472
|
active: entry.active === true,
|
|
6421
6473
|
disabled: entry.disabled === true ? true : void 0,
|
|
6422
|
-
groups: Array.isArray(group?.groups) ? group.groups.map(word).filter(Boolean) : void 0,
|
|
6474
|
+
groups: Array.isArray(group?.groups) ? group.groups.map(word$1).filter(Boolean) : void 0,
|
|
6423
6475
|
reaches: group?.coverage === void 0 ? void 0 : Object.keys(group.coverage).filter((model) => group.coverage[model] === true)
|
|
6424
6476
|
});
|
|
6425
6477
|
});
|
|
6426
6478
|
};
|
|
6479
|
+
/** How this process reached PackyCode; a report never fails for want of it. */
|
|
6480
|
+
const networkOf = async (secretList) => {
|
|
6481
|
+
try {
|
|
6482
|
+
return await network?.snapshot({ secrets: secretList });
|
|
6483
|
+
} catch {
|
|
6484
|
+
return;
|
|
6485
|
+
}
|
|
6486
|
+
};
|
|
6427
6487
|
return Object.freeze({
|
|
6428
6488
|
/** Recent top-level sessions, newest first, for the picker. */
|
|
6429
6489
|
async list(signal) {
|
|
6430
|
-
return { sessions: (await query().listSessions(signal)).filter((record) => typeof record?.header?.id === "string" && !(record.header.delegationDepth > 0)).slice(0, SESSION_LIST_LIMIT).map((record) => compact({
|
|
6490
|
+
return { sessions: (await query().listSessions(signal)).filter((record) => typeof record?.header?.id === "string" && !(record.header.delegationDepth > 0)).slice(0, SESSION_LIST_LIMIT).map((record) => compact$1({
|
|
6431
6491
|
id: record.header.id,
|
|
6432
6492
|
createdAt: iso(record.header.createdAt),
|
|
6433
6493
|
live: record.live === true,
|
|
@@ -6446,18 +6506,20 @@ function createDiagnostics({ sessionQuery, readEvents, ring, groups, secrets = a
|
|
|
6446
6506
|
throw new Error("PackyCode could not read that session");
|
|
6447
6507
|
}
|
|
6448
6508
|
const record = (await query().listSessions(signal)).find((item) => item?.header?.id === sessionId);
|
|
6509
|
+
const secretList = await secrets();
|
|
6449
6510
|
return {
|
|
6450
6511
|
format: DIAGNOSTICS_FORMAT,
|
|
6451
6512
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6452
|
-
plugin: compact({ version: await version() }),
|
|
6453
|
-
session: compact({
|
|
6513
|
+
plugin: compact$1({ version: await version() }),
|
|
6514
|
+
session: compact$1({
|
|
6454
6515
|
id: sessionId,
|
|
6455
6516
|
createdAt: iso(record?.header?.createdAt),
|
|
6456
6517
|
live: record === void 0 ? void 0 : record.live === true,
|
|
6457
6518
|
delegationDepth: finite(record?.header?.delegationDepth)
|
|
6458
6519
|
}),
|
|
6459
6520
|
keys: await keys(),
|
|
6460
|
-
...summarizeSessionEvents(events, { secrets:
|
|
6521
|
+
...summarizeSessionEvents(events, { secrets: secretList }),
|
|
6522
|
+
network: await networkOf(secretList)
|
|
6461
6523
|
};
|
|
6462
6524
|
}
|
|
6463
6525
|
});
|
|
@@ -7558,6 +7620,304 @@ function createModelSelectionController({ readPreference, writePreference, isWri
|
|
|
7558
7620
|
});
|
|
7559
7621
|
}
|
|
7560
7622
|
//#endregion
|
|
7623
|
+
//#region src/network.js
|
|
7624
|
+
/**
|
|
7625
|
+
* How this process reached PackyCode: how each request to the plugin's own
|
|
7626
|
+
* hosts ended at the transport level, which addresses a failed connection
|
|
7627
|
+
* tried and how each of them failed, and the runtime settings and name
|
|
7628
|
+
* resolution that decide those connections.
|
|
7629
|
+
*
|
|
7630
|
+
* Node's fetch reports every transport fault as a bare `fetch failed`, and
|
|
7631
|
+
* the OpenAI SDK turns that into "Connection error." or "Request timed out."
|
|
7632
|
+
* depending on which undici runs under fetch: the same connection, refused
|
|
7633
|
+
* after 0.5 s on every address, reads differently on two machines. undici
|
|
7634
|
+
* publishes the real cause on `diagnostics_channel`. This module listens
|
|
7635
|
+
* there for the plugin's hosts only and keeps identifiers and numbers: host
|
|
7636
|
+
* names, server addresses, paths without their query, error names and codes,
|
|
7637
|
+
* statuses and durations. It never reads headers or bodies.
|
|
7638
|
+
*
|
|
7639
|
+
* @module dsh-packycode-api/network
|
|
7640
|
+
*/
|
|
7641
|
+
/** How much the log keeps, and how long one host lookup may take. */
|
|
7642
|
+
const NETWORK_LIMITS = Object.freeze({
|
|
7643
|
+
failures: 12,
|
|
7644
|
+
samples: 50,
|
|
7645
|
+
lookupMs: 3e3
|
|
7646
|
+
});
|
|
7647
|
+
const CHANNELS = Object.freeze({
|
|
7648
|
+
create: "undici:request:create",
|
|
7649
|
+
headers: "undici:request:headers",
|
|
7650
|
+
error: "undici:request:error",
|
|
7651
|
+
connected: "undici:client:connected",
|
|
7652
|
+
connectError: "undici:client:connectError"
|
|
7653
|
+
});
|
|
7654
|
+
const NAME = /^[\w.:-]{1,60}$/u;
|
|
7655
|
+
const PATH = /^\/[\w.:/@%~+-]{0,79}$/u;
|
|
7656
|
+
const word = (value) => typeof value === "string" && NAME.test(value) ? value : void 0;
|
|
7657
|
+
const compact = (object) => Object.fromEntries(Object.entries(object).filter(([, value]) => value !== void 0));
|
|
7658
|
+
function hostnameOf(origin) {
|
|
7659
|
+
try {
|
|
7660
|
+
return new URL(String(origin)).hostname.toLowerCase();
|
|
7661
|
+
} catch {
|
|
7662
|
+
return;
|
|
7663
|
+
}
|
|
7664
|
+
}
|
|
7665
|
+
/** The request path with its query dropped: a query may carry a credential. */
|
|
7666
|
+
function pathOf(path) {
|
|
7667
|
+
const bare = typeof path === "string" ? path.split(/[?#]/u)[0] : void 0;
|
|
7668
|
+
return bare !== void 0 && PATH.test(bare) ? bare : void 0;
|
|
7669
|
+
}
|
|
7670
|
+
/** A caller's cancellation, as opposed to a transport fault. */
|
|
7671
|
+
const isAbort$1 = (error) => error?.name === "AbortError" && typeof error?.code !== "string" || error?.code === "ABORT_ERR";
|
|
7672
|
+
/** `Name CODE` for the error and each cause under it. */
|
|
7673
|
+
function chainOf(error) {
|
|
7674
|
+
const chain = [];
|
|
7675
|
+
for (let current = error, depth = 0; current !== null && typeof current === "object" && depth < 3; current = current.cause, depth++) {
|
|
7676
|
+
const label = [word(current.name) ?? word(current.constructor?.name), word(current.code)].filter(Boolean).join(" ");
|
|
7677
|
+
if (label !== "") chain.push(label);
|
|
7678
|
+
}
|
|
7679
|
+
return chain.length === 0 ? void 0 : chain;
|
|
7680
|
+
}
|
|
7681
|
+
/** Each address a connection tried and how it failed, from the error or a cause under it. */
|
|
7682
|
+
function attemptsOf(error) {
|
|
7683
|
+
const entry = (item) => net.isIP(item?.address ?? "") === 0 ? void 0 : [item.address, word(item.code)].filter(Boolean).join(" ");
|
|
7684
|
+
for (let current = error, depth = 0; current !== null && typeof current === "object" && depth < 3; current = current.cause, depth++) {
|
|
7685
|
+
if (Array.isArray(current.errors)) {
|
|
7686
|
+
const list = current.errors.map(entry).filter(Boolean);
|
|
7687
|
+
return list.length === 0 ? void 0 : list;
|
|
7688
|
+
}
|
|
7689
|
+
const single = entry(current);
|
|
7690
|
+
if (single !== void 0) return [single];
|
|
7691
|
+
}
|
|
7692
|
+
}
|
|
7693
|
+
function summary(samples) {
|
|
7694
|
+
if (samples.length === 0) return void 0;
|
|
7695
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
7696
|
+
const at = (share) => sorted[Math.min(sorted.length - 1, Math.floor(share * sorted.length))];
|
|
7697
|
+
return {
|
|
7698
|
+
median: at(.5),
|
|
7699
|
+
p90: at(.9),
|
|
7700
|
+
max: sorted.at(-1)
|
|
7701
|
+
};
|
|
7702
|
+
}
|
|
7703
|
+
/** Which scheme each proxy variable names: lower case first, as DSH reads them. Never the proxy itself. */
|
|
7704
|
+
function proxySchemes(env) {
|
|
7705
|
+
const read = (name) => {
|
|
7706
|
+
const value = [env?.[name], env?.[name.toUpperCase()]].find((item) => typeof item === "string" && item.trim() !== "");
|
|
7707
|
+
if (value === void 0) return void 0;
|
|
7708
|
+
try {
|
|
7709
|
+
return word(new URL(value.trim()).protocol.replace(/:$/u, "")) ?? "invalid";
|
|
7710
|
+
} catch {
|
|
7711
|
+
return "invalid";
|
|
7712
|
+
}
|
|
7713
|
+
};
|
|
7714
|
+
const schemes = compact({
|
|
7715
|
+
https: read("https_proxy"),
|
|
7716
|
+
http: read("http_proxy"),
|
|
7717
|
+
all: read("all_proxy"),
|
|
7718
|
+
noProxy: read("no_proxy") === void 0 ? void 0 : true
|
|
7719
|
+
});
|
|
7720
|
+
return Object.keys(schemes).length === 0 ? "none" : schemes;
|
|
7721
|
+
}
|
|
7722
|
+
/** The settings that decide how Node opens a connection. */
|
|
7723
|
+
function runtimeOf(env) {
|
|
7724
|
+
const call = (fn) => typeof fn === "function" ? fn() : void 0;
|
|
7725
|
+
return compact({
|
|
7726
|
+
node: process.version,
|
|
7727
|
+
platform: process.platform,
|
|
7728
|
+
autoSelectFamily: call(net.getDefaultAutoSelectFamily),
|
|
7729
|
+
attemptTimeoutMs: call(net.getDefaultAutoSelectFamilyAttemptTimeout),
|
|
7730
|
+
dnsOrder: call(dns.getDefaultResultOrder),
|
|
7731
|
+
proxy: proxySchemes(env)
|
|
7732
|
+
});
|
|
7733
|
+
}
|
|
7734
|
+
/** The addresses a connection to `hostname` would try, asked the way `net.connect` asks. */
|
|
7735
|
+
const defaultLookup = (hostname) => dns.promises.lookup(hostname, process.platform === "win32" ? { all: true } : {
|
|
7736
|
+
all: true,
|
|
7737
|
+
hints: dns.ADDRCONFIG
|
|
7738
|
+
});
|
|
7739
|
+
/**
|
|
7740
|
+
* The network log behind the diagnostics card.
|
|
7741
|
+
* @param options - the plugin's host origins (read on every request), the
|
|
7742
|
+
* environment the proxy variables come from, and test seams for the clock,
|
|
7743
|
+
* the lookup and its bound.
|
|
7744
|
+
*/
|
|
7745
|
+
function createNetworkLog({ hosts, env = process.env, now = Date.now, clock = () => performance.now(), lookupAll = defaultLookup, lookupMs = NETWORK_LIMITS.lookupMs } = {}) {
|
|
7746
|
+
const watched = () => {
|
|
7747
|
+
const names = [];
|
|
7748
|
+
for (const origin of hosts?.() ?? []) {
|
|
7749
|
+
const name = hostnameOf(origin);
|
|
7750
|
+
if (name !== void 0 && !names.includes(name)) names.push(name);
|
|
7751
|
+
}
|
|
7752
|
+
return names;
|
|
7753
|
+
};
|
|
7754
|
+
const stats = /* @__PURE__ */ new Map();
|
|
7755
|
+
const statsFor = (host) => {
|
|
7756
|
+
let entry = stats.get(host);
|
|
7757
|
+
if (entry === void 0) {
|
|
7758
|
+
entry = {
|
|
7759
|
+
requests: 0,
|
|
7760
|
+
answered: 0,
|
|
7761
|
+
statuses: {},
|
|
7762
|
+
failed: {
|
|
7763
|
+
connect: 0,
|
|
7764
|
+
request: 0,
|
|
7765
|
+
response: 0
|
|
7766
|
+
},
|
|
7767
|
+
aborted: 0,
|
|
7768
|
+
headersMs: [],
|
|
7769
|
+
connects: {
|
|
7770
|
+
ok: 0,
|
|
7771
|
+
afterFallback: 0,
|
|
7772
|
+
ipv6: 0
|
|
7773
|
+
}
|
|
7774
|
+
};
|
|
7775
|
+
stats.set(host, entry);
|
|
7776
|
+
}
|
|
7777
|
+
return entry;
|
|
7778
|
+
};
|
|
7779
|
+
const requests = /* @__PURE__ */ new WeakMap();
|
|
7780
|
+
const connectErrors = /* @__PURE__ */ new WeakSet();
|
|
7781
|
+
const failures = [];
|
|
7782
|
+
let since;
|
|
7783
|
+
let listening;
|
|
7784
|
+
const guard = (handler) => (message) => {
|
|
7785
|
+
try {
|
|
7786
|
+
handler(message);
|
|
7787
|
+
} catch {}
|
|
7788
|
+
};
|
|
7789
|
+
const handlers = {
|
|
7790
|
+
[CHANNELS.create]: ({ request }) => {
|
|
7791
|
+
const host = hostnameOf(request?.origin);
|
|
7792
|
+
if (host === void 0 || !watched().includes(host)) return;
|
|
7793
|
+
requests.set(request, {
|
|
7794
|
+
host,
|
|
7795
|
+
path: pathOf(request.path),
|
|
7796
|
+
startedAt: clock(),
|
|
7797
|
+
answered: false
|
|
7798
|
+
});
|
|
7799
|
+
statsFor(host).requests += 1;
|
|
7800
|
+
},
|
|
7801
|
+
[CHANNELS.headers]: ({ request, response }) => {
|
|
7802
|
+
const entry = requests.get(request);
|
|
7803
|
+
if (entry === void 0) return;
|
|
7804
|
+
entry.answered = true;
|
|
7805
|
+
const stat = statsFor(entry.host);
|
|
7806
|
+
stat.answered += 1;
|
|
7807
|
+
const status = response?.statusCode;
|
|
7808
|
+
if (Number.isInteger(status)) stat.statuses[status] = (stat.statuses[status] ?? 0) + 1;
|
|
7809
|
+
stat.headersMs.push(Math.round(clock() - entry.startedAt));
|
|
7810
|
+
if (stat.headersMs.length > NETWORK_LIMITS.samples) stat.headersMs.shift();
|
|
7811
|
+
},
|
|
7812
|
+
[CHANNELS.connectError]: ({ error }) => {
|
|
7813
|
+
if (error !== null && typeof error === "object") connectErrors.add(error);
|
|
7814
|
+
},
|
|
7815
|
+
[CHANNELS.connected]: ({ connectParams, socket }) => {
|
|
7816
|
+
const host = typeof connectParams?.hostname === "string" ? connectParams.hostname.toLowerCase() : void 0;
|
|
7817
|
+
if (host === void 0 || !watched().includes(host)) return;
|
|
7818
|
+
const connects = statsFor(host).connects;
|
|
7819
|
+
connects.ok += 1;
|
|
7820
|
+
if (Array.isArray(socket?.autoSelectFamilyAttemptedAddresses) && socket.autoSelectFamilyAttemptedAddresses.length > 1) connects.afterFallback += 1;
|
|
7821
|
+
if (socket?.remoteFamily === "IPv6") connects.ipv6 += 1;
|
|
7822
|
+
},
|
|
7823
|
+
[CHANNELS.error]: ({ request, error }) => {
|
|
7824
|
+
const entry = requests.get(request);
|
|
7825
|
+
if (entry === void 0) return;
|
|
7826
|
+
requests.delete(request);
|
|
7827
|
+
const stat = statsFor(entry.host);
|
|
7828
|
+
if (isAbort$1(error)) {
|
|
7829
|
+
stat.aborted += 1;
|
|
7830
|
+
return;
|
|
7831
|
+
}
|
|
7832
|
+
const stage = connectErrors.has(error) ? "connect" : entry.answered ? "response" : "request";
|
|
7833
|
+
stat.failed[stage] += 1;
|
|
7834
|
+
failures.push(compact({
|
|
7835
|
+
time: new Date(now()).toISOString(),
|
|
7836
|
+
host: entry.host,
|
|
7837
|
+
path: entry.path,
|
|
7838
|
+
stage,
|
|
7839
|
+
afterMs: Math.round(clock() - entry.startedAt),
|
|
7840
|
+
error: chainOf(error),
|
|
7841
|
+
attempts: attemptsOf(error),
|
|
7842
|
+
message: typeof error?.message === "string" ? error.message.slice(0, 1e3) : void 0
|
|
7843
|
+
}));
|
|
7844
|
+
if (failures.length > NETWORK_LIMITS.failures) failures.shift();
|
|
7845
|
+
}
|
|
7846
|
+
};
|
|
7847
|
+
const subscribed = Object.entries(handlers).map(([name, handler]) => [name, guard(handler)]);
|
|
7848
|
+
const stop = () => {
|
|
7849
|
+
if (!listening) return;
|
|
7850
|
+
listening = false;
|
|
7851
|
+
for (const [name, handler] of subscribed) unsubscribe(name, handler);
|
|
7852
|
+
};
|
|
7853
|
+
/** One host's addresses, or why there are none; never longer than `lookupMs`. */
|
|
7854
|
+
const resolve = async (hostname) => {
|
|
7855
|
+
let timer;
|
|
7856
|
+
try {
|
|
7857
|
+
const list = await Promise.race([lookupAll(hostname), new Promise((_, reject) => {
|
|
7858
|
+
timer = setTimeout(() => reject(Object.assign(/* @__PURE__ */ new Error("lookup timed out"), { code: "LOOKUP_TIMEOUT" })), lookupMs);
|
|
7859
|
+
timer.unref?.();
|
|
7860
|
+
})]);
|
|
7861
|
+
return { resolved: (Array.isArray(list) ? list : []).map((item) => item?.address).filter((address) => net.isIP(address ?? "") !== 0) };
|
|
7862
|
+
} catch (error) {
|
|
7863
|
+
return { lookupError: word(error?.code) ?? "LOOKUP_FAILED" };
|
|
7864
|
+
} finally {
|
|
7865
|
+
clearTimeout(timer);
|
|
7866
|
+
}
|
|
7867
|
+
};
|
|
7868
|
+
const countsOf = (stat) => {
|
|
7869
|
+
if (stat === void 0) return {};
|
|
7870
|
+
const nonZero = (counts) => Object.fromEntries(Object.entries(counts).filter(([, count]) => count > 0));
|
|
7871
|
+
const failed = nonZero(stat.failed);
|
|
7872
|
+
return compact({
|
|
7873
|
+
requests: stat.requests,
|
|
7874
|
+
answered: stat.answered,
|
|
7875
|
+
statuses: Object.keys(stat.statuses).length === 0 ? void 0 : { ...stat.statuses },
|
|
7876
|
+
failed: Object.keys(failed).length === 0 ? void 0 : failed,
|
|
7877
|
+
aborted: stat.aborted === 0 ? void 0 : stat.aborted,
|
|
7878
|
+
headersMs: summary(stat.headersMs),
|
|
7879
|
+
connects: stat.connects.ok === 0 ? void 0 : nonZero(stat.connects)
|
|
7880
|
+
});
|
|
7881
|
+
};
|
|
7882
|
+
return Object.freeze({
|
|
7883
|
+
/**
|
|
7884
|
+
* Start listening; returns the function that stops it.
|
|
7885
|
+
* @returns the disposer the plugin's effect runs on unload.
|
|
7886
|
+
*/
|
|
7887
|
+
start() {
|
|
7888
|
+
if (!listening) {
|
|
7889
|
+
listening = true;
|
|
7890
|
+
since = now();
|
|
7891
|
+
for (const [name, handler] of subscribed) subscribe(name, handler);
|
|
7892
|
+
}
|
|
7893
|
+
return stop;
|
|
7894
|
+
},
|
|
7895
|
+
stop,
|
|
7896
|
+
/**
|
|
7897
|
+
* What the log saw since it started, the runtime settings, and how each
|
|
7898
|
+
* host resolves now.
|
|
7899
|
+
* @param options - stored secrets to redact from error messages.
|
|
7900
|
+
*/
|
|
7901
|
+
async snapshot({ secrets = [] } = {}) {
|
|
7902
|
+
const names = watched();
|
|
7903
|
+
const lookups = await Promise.all(names.map(resolve));
|
|
7904
|
+
return compact({
|
|
7905
|
+
since: since === void 0 ? void 0 : new Date(since).toISOString(),
|
|
7906
|
+
runtime: runtimeOf(env),
|
|
7907
|
+
hosts: names.map((host, index) => ({
|
|
7908
|
+
host,
|
|
7909
|
+
...lookups[index],
|
|
7910
|
+
...countsOf(stats.get(host))
|
|
7911
|
+
})),
|
|
7912
|
+
recentFailures: failures.map((failure) => compact({
|
|
7913
|
+
...failure,
|
|
7914
|
+
message: safeMessage$1(failure.message, secrets)
|
|
7915
|
+
}))
|
|
7916
|
+
});
|
|
7917
|
+
}
|
|
7918
|
+
});
|
|
7919
|
+
}
|
|
7920
|
+
//#endregion
|
|
7561
7921
|
//#region src/claude-user-text.js
|
|
7562
7922
|
/**
|
|
7563
7923
|
* Which of DSH's messages Claude Code receives on the cc route.
|
|
@@ -7669,7 +8029,7 @@ function guardGeminiReplay(open, { model, context, signal, wait = waitForRetry }
|
|
|
7669
8029
|
//#endregion
|
|
7670
8030
|
//#region src/gemini-api.js
|
|
7671
8031
|
/** Gemini's native API, including level-based thinking on older pi-ai hosts. */
|
|
7672
|
-
const GEMINI_MODELS = ["gemini-3.8-flash"];
|
|
8032
|
+
const GEMINI_MODELS = ["gemini-4-argon", "gemini-3.8-flash"];
|
|
7673
8033
|
/**
|
|
7674
8034
|
* Google's documented thought signature for a function call whose real one
|
|
7675
8035
|
* cannot be sent: the API skips signature validation for it.
|
|
@@ -7723,10 +8083,10 @@ const skipped = (payload) => ({
|
|
|
7723
8083
|
contents: skipThoughtSignatures(payload.contents)
|
|
7724
8084
|
});
|
|
7725
8085
|
/**
|
|
7726
|
-
* pi-ai 0.85
|
|
7727
|
-
*
|
|
7728
|
-
*
|
|
7729
|
-
* send no real signature.
|
|
8086
|
+
* pi-ai 0.85 treats Argon as a budget-based model, and defaults Flash to
|
|
8087
|
+
* MINIMAL when reasoning is omitted. Both models support LOW/MEDIUM/HIGH,
|
|
8088
|
+
* default to MEDIUM and cannot disable thinking. Keep the native SDK stream,
|
|
8089
|
+
* credentials and tools; correct that config and send no real signature.
|
|
7730
8090
|
*/
|
|
7731
8091
|
function createGeminiApi() {
|
|
7732
8092
|
const api = googleGenerativeAIApi();
|
|
@@ -7757,6 +8117,49 @@ function createGeminiApi() {
|
|
|
7757
8117
|
};
|
|
7758
8118
|
}
|
|
7759
8119
|
//#endregion
|
|
8120
|
+
//#region src/hy4-identity.js
|
|
8121
|
+
/** The sentence appended to Hy4's instructions; checked live as worded. */
|
|
8122
|
+
const HY4_IDENTITY_NOTE = "About yourself: the hy4 model is Hy4 from Tencent Hunyuan (腾讯混元), reached through PackyCode. DeepSeek Harness is the agent app you run in, not your model or its maker.";
|
|
8123
|
+
const INSTRUCTION_ROLES = /* @__PURE__ */ new Set(["developer", "system"]);
|
|
8124
|
+
const carriesNote = (item) => typeof item?.content === "string" && item.content.includes("About yourself: the hy4 model is Hy4 from Tencent Hunyuan (腾讯混元), reached through PackyCode. DeepSeek Harness is the agent app you run in, not your model or its maker.");
|
|
8125
|
+
/**
|
|
8126
|
+
* Add the note to a Responses payload: after the leading instructions, or as
|
|
8127
|
+
* the only ones when the request has none. A payload that already carries it
|
|
8128
|
+
* is returned unchanged, and the given payload is never modified.
|
|
8129
|
+
* @param payload - the Responses request body pi-ai built.
|
|
8130
|
+
* @returns the body to send.
|
|
8131
|
+
*/
|
|
8132
|
+
function withIdentityNote(payload) {
|
|
8133
|
+
if (!Array.isArray(payload?.input) || payload.input.some(carriesNote)) return payload;
|
|
8134
|
+
const [first, ...rest] = payload.input;
|
|
8135
|
+
const input = INSTRUCTION_ROLES.has(first?.role) && typeof first.content === "string" ? [{
|
|
8136
|
+
...first,
|
|
8137
|
+
content: `${first.content}\n\n${HY4_IDENTITY_NOTE}`
|
|
8138
|
+
}, ...rest] : [{
|
|
8139
|
+
role: "developer",
|
|
8140
|
+
content: HY4_IDENTITY_NOTE
|
|
8141
|
+
}, ...payload.input];
|
|
8142
|
+
return {
|
|
8143
|
+
...payload,
|
|
8144
|
+
input
|
|
8145
|
+
};
|
|
8146
|
+
}
|
|
8147
|
+
/**
|
|
8148
|
+
* pi-ai options whose payload carries the note before any caller hook sees
|
|
8149
|
+
* it, so a caller's own `onPayload` still has the last word.
|
|
8150
|
+
* @param options - the request's pi-ai options.
|
|
8151
|
+
* @returns the same options with the note added on the way out.
|
|
8152
|
+
*/
|
|
8153
|
+
function withHy4Identity(options) {
|
|
8154
|
+
return {
|
|
8155
|
+
...options,
|
|
8156
|
+
onPayload: async (payload, model) => {
|
|
8157
|
+
const noted = withIdentityNote(payload);
|
|
8158
|
+
return await options?.onPayload?.(noted, model) ?? noted;
|
|
8159
|
+
}
|
|
8160
|
+
};
|
|
8161
|
+
}
|
|
8162
|
+
//#endregion
|
|
7760
8163
|
//#region src/provider.js
|
|
7761
8164
|
/**
|
|
7762
8165
|
* The PackyCode pi-ai provider, built in code so the user configures no route.
|
|
@@ -7812,10 +8215,13 @@ function createPackyCodeProvider({ claudeCli } = {}) {
|
|
|
7812
8215
|
"google-generative-ai": createGeminiApi()
|
|
7813
8216
|
}
|
|
7814
8217
|
});
|
|
7815
|
-
const requestOptions = (model, options) =>
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
8218
|
+
const requestOptions = (model, options) => {
|
|
8219
|
+
const cached = model.api === "openai-responses" && !model.id.startsWith("gpt-") ? {
|
|
8220
|
+
...options,
|
|
8221
|
+
cacheRetention: "none"
|
|
8222
|
+
} : options;
|
|
8223
|
+
return model.id === "hy4" ? withHy4Identity(cached) : cached;
|
|
8224
|
+
};
|
|
7819
8225
|
return Object.freeze({
|
|
7820
8226
|
...provider,
|
|
7821
8227
|
stream: (model, context, options) => provider.stream(model, context, requestOptions(model, options)),
|
|
@@ -9460,6 +9866,17 @@ function apply(ctx, config) {
|
|
|
9460
9866
|
statusHost: normalizeHost(current.statusHost, DEFAULT_STATUS_HOST)
|
|
9461
9867
|
};
|
|
9462
9868
|
};
|
|
9869
|
+
/**
|
|
9870
|
+
* How requests to PackyCode's own hosts end at the transport level, for
|
|
9871
|
+
* the diagnostics card. It listens from load, so the first group reading
|
|
9872
|
+
* is in it too.
|
|
9873
|
+
*/
|
|
9874
|
+
const network = createNetworkLog({ hosts: () => [
|
|
9875
|
+
hosts().apiHost,
|
|
9876
|
+
DEFAULT_API_HOST,
|
|
9877
|
+
...statusHostOrder(hosts())
|
|
9878
|
+
] });
|
|
9879
|
+
ctx.effect(() => network.start(), "packycode-api: network diagnostics");
|
|
9463
9880
|
const balance = createBalanceReader({
|
|
9464
9881
|
listKeys: () => ring.secrets(),
|
|
9465
9882
|
getHosts: hosts
|
|
@@ -9839,7 +10256,8 @@ function apply(ctx, config) {
|
|
|
9839
10256
|
ring,
|
|
9840
10257
|
groups,
|
|
9841
10258
|
secrets: async () => (await Promise.all([ring.secrets(), imageRing.secrets()])).flat().map((entry) => entry.key),
|
|
9842
|
-
ownPackageJsonUrl: new URL("../package.json", import.meta.url)
|
|
10259
|
+
ownPackageJsonUrl: new URL("../package.json", import.meta.url),
|
|
10260
|
+
network
|
|
9843
10261
|
})
|
|
9844
10262
|
});
|
|
9845
10263
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-packycode-api",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.6",
|
|
4
4
|
"description": "Use PackyCode API keys in DeepSeek Harness with multi-protocol models, image generation and editing, separate key rings, and balance display",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|