dsh-opencode-free-tier 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +13 -8
  2. package/lib/index.js +37 -22
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -29,14 +29,19 @@ A zero-dependency Cordis plugin that fixes all three at the fetch transport laye
29
29
  `AsyncLocalStorage` across each adapter stream, so the fetch layer knows which DSH
30
30
  conversation a request belongs to (stable session → optimal upstream prompt-cache routing).
31
31
  - **Fetch middleware, scoped strictly to `opencode.ai`** (+ subdomains) — every other
32
- host passes through byte-for-byte untouched:
33
- - missing/non-CLI `User-Agent` `opencode/<cli-version> (platform arch; node...)`
34
- - missing/malformed `x-opencode-session` → canonicalized (`ses_` + 26) from the DSH
35
- conversation id; an already-canonical id passes through (cache affinity preserved)
36
- - fills `x-opencode-client: cli`, `x-session-affinity`, `X-Session-Id`,
37
- `x-opencode-request`, `x-opencode-project` when absent
38
- - chat-completions bodies missing `bash`/`read` tools get the stubs appended
39
- (`tool_choice: "none"` when the caller had no tools, so the model never calls them)
32
+ host passes through byte-for-byte untouched. It distinguishes the two lanes:
33
+ - **Anonymous free lane** (`/zen/...`) the full CLI disguise: missing/non-CLI
34
+ `User-Agent` `opencode/<cli-version> (platform arch; node...)`;
35
+ missing/malformed `x-opencode-session` canonicalized (`ses_` + 26) from the DSH
36
+ conversation id (an already-canonical id passes through, preserving cache affinity);
37
+ `x-opencode-client: cli`, `x-session-affinity`, `X-Session-Id`,
38
+ `x-opencode-request`, `x-opencode-project` filled when absent; chat-completions
39
+ bodies missing `bash`/`read` tools get the stubs appended (`tool_choice: "none"`
40
+ when the caller had no tools, so the model never calls them).
41
+ - **Paid OpenCode Go lane** (`/zen/go/...`) — only the stable canonical
42
+ `x-opencode-session` the Go docs require; the harness's real `User-Agent` and the
43
+ request body stay untouched. This also fixes DSH's missing session header on the
44
+ Go lane ([discussion #5495](https://github.com/deepseek-ai/deepseek-harness/discussions/5495)).
40
45
 
41
46
  Already-correct requests pass through untouched (idempotent) — e.g. it coexists with
42
47
  `opencode2dsh` instead of breaking it.
package/lib/index.js CHANGED
@@ -257,6 +257,13 @@ async function readBodyText(input, init) {
257
257
  * Options:
258
258
  * - hosts: allowlist (default opencode.ai)
259
259
  * - isEnabled: () => boolean, consulted per matching request
260
+ *
261
+ * Two lanes share the host:
262
+ * - the anonymous free lane (`/zen/...`) needs the full CLI disguise (UA,
263
+ * canonical session, `bash`/`read` tools) or it answers 403 FreeTierError;
264
+ * - the paid OpenCode Go lane (`/zen/go/...`) only needs the stable
265
+ * `x-opencode-session` header the Go docs ask for, and should keep the
266
+ * harness's real User-Agent, so it is never disguised and gets no tools.
260
267
  */
261
268
  export function createFreeTierMiddleware(options) {
262
269
  const hosts = options?.hosts?.length ? options.hosts : DEFAULT_HOSTS
@@ -265,6 +272,7 @@ export function createFreeTierMiddleware(options) {
265
272
  const url = requestUrlOf(input)
266
273
  if (!url || !hostMatches(url.hostname, hosts)) return next(input, init)
267
274
  if (!isEnabled()) return next(input, init)
275
+ const freeLane = !url.pathname.startsWith('/zen/go/')
268
276
 
269
277
  // Merge whichever header source would actually reach the wire (fetch spec:
270
278
  // init.headers replaces Request headers when present).
@@ -276,10 +284,13 @@ export function createFreeTierMiddleware(options) {
276
284
  : undefined
277
285
  const headers = new Headers(source ?? undefined)
278
286
 
279
- // 1. User-Agent must start with opencode/
280
- const ua = headers.get('user-agent')
281
- if (!ua || !ua.toLowerCase().startsWith('opencode/')) {
282
- headers.set('user-agent', opencodeUserAgent())
287
+ // 1. User-Agent must start with opencode/ (free lane only; Go keeps the
288
+ // harness identity the Go docs ask clients to send).
289
+ if (freeLane) {
290
+ const ua = headers.get('user-agent')
291
+ if (!ua || !ua.toLowerCase().startsWith('opencode/')) {
292
+ headers.set('user-agent', opencodeUserAgent())
293
+ }
283
294
  }
284
295
 
285
296
  // 2. x-opencode-session must be canonical ses_+26; preserve a correct one
@@ -295,27 +306,31 @@ export function createFreeTierMiddleware(options) {
295
306
  session = canonicalSessionID(signal)
296
307
  headers.set(HEADER_SESSION, session)
297
308
  }
298
- if (!headers.get('x-opencode-client')) headers.set('x-opencode-client', 'cli')
299
- if (!headers.get('x-session-affinity')) headers.set('x-session-affinity', session)
300
- if (!headers.get('x-session-id') && !headers.get('X-Session-Id')) headers.set('X-Session-Id', session)
301
- if (!headers.get('x-opencode-request')) headers.set('x-opencode-request', randomID('req', 16))
302
- if (!headers.get('x-opencode-project')) headers.set('x-opencode-project', defaultProjectID())
309
+ if (freeLane) {
310
+ if (!headers.get('x-opencode-client')) headers.set('x-opencode-client', 'cli')
311
+ if (!headers.get('x-session-affinity')) headers.set('x-session-affinity', session)
312
+ if (!headers.get('x-session-id') && !headers.get('X-Session-Id')) headers.set('X-Session-Id', session)
313
+ if (!headers.get('x-opencode-request')) headers.set('x-opencode-request', randomID('req', 16))
314
+ if (!headers.get('x-opencode-project')) headers.set('x-opencode-project', defaultProjectID())
315
+ }
303
316
 
304
- // 3. Body must carry bash+read tools (chat-completions shape only).
317
+ // 3. Free lane only: body must carry bash+read tools (chat-completions shape).
305
318
  let newBodyText
306
- try {
307
- const bodyText = await readBodyText(input, init)
308
- if (typeof bodyText === 'string' && bodyText.length > 0) {
309
- let parsed
310
- try {
311
- parsed = JSON.parse(bodyText)
312
- } catch {
313
- parsed = undefined
319
+ if (freeLane) {
320
+ try {
321
+ const bodyText = await readBodyText(input, init)
322
+ if (typeof bodyText === 'string' && bodyText.length > 0) {
323
+ let parsed
324
+ try {
325
+ parsed = JSON.parse(bodyText)
326
+ } catch {
327
+ parsed = undefined
328
+ }
329
+ const fixed = parsed === undefined ? undefined : ensureFreeLaneShape(parsed)
330
+ if (fixed !== undefined) newBodyText = JSON.stringify(fixed)
314
331
  }
315
- const fixed = parsed === undefined ? undefined : ensureFreeLaneShape(parsed)
316
- if (fixed !== undefined) newBodyText = JSON.stringify(fixed)
317
- }
318
- } catch {}
332
+ } catch {}
333
+ }
319
334
 
320
335
  if (newBodyText === undefined) {
321
336
  return next(input, { ...init, headers })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-opencode-free-tier",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Make DSH llm-pi-ai opencode routes pass Zen free-tier gate: opencode User-Agent + canonical ses_ session + bash/read tools. Scoped to opencode.ai only.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",