@dan-ai-studio/dshopencodego 0.1.63 → 0.1.64

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.
@@ -18,6 +18,7 @@ import type { AttachmentStore, ImageAttachmentRef } from '@deepseek-ai/dsh-attac
18
18
  import { OpencodeGoCatalog } from './catalog/index.ts';
19
19
  import type { GoDoc } from './catalog/go-doc.ts';
20
20
  import type { OpencodeGoConfig } from './config.ts';
21
+ import type { RequestRecorder } from './request-log/store.ts';
21
22
  import type { MeteredUsage } from './usage/meter.ts';
22
23
  /** Image machinery the adapter reads once per request. */
23
24
  export interface OpencodeGoImageAccess {
@@ -78,6 +79,12 @@ export interface OpencodeGoAdapterOptions {
78
79
  model: string;
79
80
  usage: MeteredUsage;
80
81
  }) => void;
82
+ /**
83
+ * Record one request's span. The recorder returns undefined unless the
84
+ * profile enabled the log, so a disabled log costs one call and one
85
+ * boolean read per request and constructs nothing.
86
+ */
87
+ readonly requestLog?: RequestRecorder;
81
88
  }
82
89
  /**
83
90
  * The single `opencode-go` route's adapter.
@@ -14,7 +14,9 @@
14
14
  import type { RetryPolicyConfig } from '@deepseek-ai/dsh-llm';
15
15
  import z from '@deepseek-ai/schemastery';
16
16
  import { DEFAULT_KEEP_ALIVE_TIMEOUT_MS, DEFAULT_KEEP_ALIVE_TIMEOUT_SECONDS, MAX_KEEP_ALIVE_TIMEOUT_MS, MAX_KEEP_ALIVE_TIMEOUT_SECONDS, TIMER_CEILING_MS } from './network/keep-alive.ts';
17
+ import { DEFAULT_REQUEST_LOG_MAX_ENTRIES, MAX_REQUEST_LOG_MAX_ENTRIES, MIN_REQUEST_LOG_MAX_ENTRIES } from './request-log/limits.ts';
17
18
  export { DEFAULT_KEEP_ALIVE_TIMEOUT_MS, DEFAULT_KEEP_ALIVE_TIMEOUT_SECONDS, MAX_KEEP_ALIVE_TIMEOUT_MS, MAX_KEEP_ALIVE_TIMEOUT_SECONDS, TIMER_CEILING_MS, };
19
+ export { DEFAULT_REQUEST_LOG_MAX_ENTRIES, MAX_REQUEST_LOG_MAX_ENTRIES, MIN_REQUEST_LOG_MAX_ENTRIES, };
18
20
  /** Credential reference resolving the OpenCode Go API key. */
19
21
  export declare const DEFAULT_API_KEY_ENV = "OPENCODE_GO_API_KEY";
20
22
  /** Requests and pickers share this cache lifetime; explicit discovery revalidates immediately. */
@@ -73,6 +75,23 @@ export interface OpencodeGoConfig {
73
75
  * The escape hatch for a model whose family rule guessed wrong.
74
76
  */
75
77
  modelProtocols: Record<string, string>;
78
+ /**
79
+ * Whether the per-request log records anything at all. Off by default: a
80
+ * disabled log changes nothing on the request path — not one extra
81
+ * allocation — because the recorder is never constructed.
82
+ */
83
+ requestLogEnabled: boolean;
84
+ /**
85
+ * Whether the request log also records pool lifecycle and full session
86
+ * identity. Debug only: the common log carries a truncated session and no
87
+ * pool events, which is enough to follow one conversation.
88
+ */
89
+ requestLogDebug: boolean;
90
+ /**
91
+ * How many request-log entries the Host keeps, oldest dropped first. Bounded
92
+ * so the log can never grow with the session.
93
+ */
94
+ requestLogMaxEntries: number;
76
95
  /**
77
96
  * Addresses a name should be sent to instead of the one the system
78
97
  * resolves, by host name. A name that is not listed keeps the system's
@@ -63,6 +63,21 @@ export declare function gatewayHostOf(baseURL: string): string;
63
63
  * address it is pinned to.
64
64
  */
65
65
  export declare function resolveGatewayPin(pins: PinnedAddresses, baseURL: string): GatewayPin;
66
+ /**
67
+ * Pool lifecycle observer. Pools live for minutes, so these events are rare
68
+ * by construction — observing them costs nothing on the request path.
69
+ */
70
+ export interface PoolObserver {
71
+ /** A new pool was built for this key. */
72
+ create(key: string): void;
73
+ /** The pool for this key was closed. */
74
+ close(key: string): void;
75
+ }
76
+ /**
77
+ * Watch pool lifecycle. The request log sets this when its debug mode is on;
78
+ * unset it (or never set it) and the pool code reads exactly as before.
79
+ */
80
+ export declare function observePool(next: PoolObserver | undefined): void;
66
81
  /**
67
82
  * Close the dispatcher, if one is open.
68
83
  *
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Wire contract for the request-log Remote.
3
+ *
4
+ * One reading: the bounded per-request log the Host keeps while the profile
5
+ * enables it. The viewer opens it on demand; nothing polls, so a closed
6
+ * viewer costs no wire traffic at all.
7
+ *
8
+ * @module @dan-ai-studio/dshopencodego/request-log/contract
9
+ */
10
+ import type { RemoteResult, TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol';
11
+ import type { RequestLogSnapshot } from './store.ts';
12
+ export type { PoolLogEvent, RequestLogEntry, RequestLogSnapshot } from './store.ts';
13
+ /** Validate a snapshot crossing the wire. */
14
+ export declare function parseRequestLogSnapshot(value: unknown): RequestLogSnapshot;
15
+ declare module '@deepseek-ai/dsh-typert-protocol' {
16
+ interface TypertRemoteNamespaceMap {
17
+ opencodeGoRequestLog: {
18
+ readLog(): Promise<RemoteResult<RequestLogSnapshot>>;
19
+ };
20
+ }
21
+ }
22
+ /** Remote methods this package owns, mounted together by the plugin. */
23
+ export declare const requestLogRemote: TypertRemoteContribution;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The per-request log: public surface of the request-log module.
3
+ *
4
+ * @module @dan-ai-studio/dshopencodego/request-log
5
+ */
6
+ export { OpencodeGoRequestLogService } from './service.ts';
7
+ export type { RequestLogServiceOptions } from './service.ts';
8
+ export { RequestLog } from './store.ts';
9
+ export type { PoolLogEvent, RequestLogEntry, RequestLogGates, RequestLogSnapshot, RequestRecorder, RequestSpan, RequestStart, } from './store.ts';
10
+ export { parseRequestLogSnapshot, requestLogRemote } from './contract.ts';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Request-log bounds, shared by the schema and the settings page.
3
+ *
4
+ * These live apart from `config.ts` on purpose: the settings page must not
5
+ * drag the configuration module (and its Harness-only imports) into the
6
+ * browser bundle. Import from here on both sides instead.
7
+ *
8
+ * @module @dan-ai-studio/dshopencodego/request-log/limits
9
+ */
10
+ /** Default request-log retention: enough to debug a session, small enough to never matter. */
11
+ export declare const DEFAULT_REQUEST_LOG_MAX_ENTRIES = 200;
12
+ /** Fewest request-log entries a profile may keep. */
13
+ export declare const MIN_REQUEST_LOG_MAX_ENTRIES = 50;
14
+ /** Most request-log entries a profile may keep; the buffer is bounded by design. */
15
+ export declare const MAX_REQUEST_LOG_MAX_ENTRIES = 500;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Host half of the request-log Remote.
3
+ *
4
+ * The viewer opens the snapshot on demand and refreshes it when asked; the
5
+ * Host never pushes. A closed viewer therefore costs no wire traffic at all,
6
+ * and an open one costs one read per refresh.
7
+ *
8
+ * @module @dan-ai-studio/dshopencodego/request-log/service
9
+ */
10
+ import type { Context } from '@deepseek-ai/cordis';
11
+ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
12
+ import type { RequestLog, RequestLogSnapshot } from './store.ts';
13
+ /** Host inputs for the request-log service. */
14
+ export interface RequestLogServiceOptions {
15
+ /** The bounded ring the adapter appends to. */
16
+ readonly log: RequestLog;
17
+ /** How many entries to keep; the ring trims to it on every read. */
18
+ readonly maxEntries: () => number;
19
+ }
20
+ /** Request-log Remote: one on-demand snapshot of the ring. */
21
+ export declare class OpencodeGoRequestLogService extends TypertRemoteService {
22
+ private readonly options;
23
+ constructor(ctx: Context, options: RequestLogServiceOptions);
24
+ /** The current reading, trimmed to the configured bound first. */
25
+ readLog(): RequestLogSnapshot;
26
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * The per-request log: a bounded ring buffer owned by the Host.
3
+ *
4
+ * Everything here is inert unless the profile enables it. The recorder is
5
+ * constructed only when `requestLogEnabled` reads true, so a disabled log
6
+ * changes nothing on the request path — not one extra allocation. When
7
+ * enabled, one entry per completed or failed request is appended; the oldest
8
+ * entries are dropped past the configured bound, so the log can never grow
9
+ * with the session.
10
+ *
11
+ * Two facts are structural rather than conventional:
12
+ *
13
+ * - Authorization never enters a record. The recorder takes no headers
14
+ * argument at all; the only header-derived value it stores is the session
15
+ * id it is handed, already separated from the credential by the caller.
16
+ * - The session id is stored truncated (first 8 characters) unless debug
17
+ * mode is on at record time. A truncated id groups one conversation's
18
+ * requests without writing the full identity to the log.
19
+ *
20
+ * @module @dan-ai-studio/dshopencodego/request-log/store
21
+ */
22
+ /** How many session-id characters the common (non-debug) log keeps. */
23
+ export declare const SESSION_PREFIX_LENGTH = 8;
24
+ /** One recorded request. Counts are provider-reported; timings are Host-measured. */
25
+ export interface RequestLogEntry {
26
+ /** Monotonic sequence within this process; gaps mean nothing was recorded. */
27
+ readonly seq: number;
28
+ /** When the request started, in epoch milliseconds. */
29
+ readonly atMs: number;
30
+ /** The model id the request was made with. */
31
+ readonly model: string;
32
+ /** The session id, full in debug mode, truncated to {@link SESSION_PREFIX_LENGTH} otherwise. */
33
+ readonly session: string;
34
+ /** HTTP method used. */
35
+ readonly method: string;
36
+ /** Request URL without any credential (the key travels in a header). */
37
+ readonly url: string;
38
+ /** Response status when a response arrived; absent when the request never got one. */
39
+ readonly status?: number;
40
+ /** Provider-reported input tokens, when the terminal usage arrived. */
41
+ readonly inputTokens?: number;
42
+ /** Provider-reported output tokens, when the terminal usage arrived. */
43
+ readonly outputTokens?: number;
44
+ /** Provider-reported cache-read tokens, when positive. */
45
+ readonly cacheReadTokens?: number;
46
+ /** Wall time from request start to terminal event, in milliseconds. */
47
+ readonly durationMs?: number;
48
+ /** Wall time from request start to the first token delta, in milliseconds. */
49
+ readonly firstTokenMs?: number;
50
+ /** Whether the request completed. */
51
+ readonly ok: boolean;
52
+ /** Failure code (`TIMEOUT`, `TRANSPORT`, …) for a request that did not complete. */
53
+ readonly code?: string;
54
+ /** Connection-pool key the request ran under, when the pool is enabled. */
55
+ readonly poolKey?: string;
56
+ /** Request shape summary, so the log stays informative without bodies. */
57
+ readonly shape?: {
58
+ /** Number of conversation messages sent. */
59
+ readonly messages: number;
60
+ /** Whether any message carried image content. */
61
+ readonly hasImage: boolean;
62
+ /** Number of tool schemas sent. */
63
+ readonly tools: number;
64
+ };
65
+ /**
66
+ * Debug-only headers, allowlisted at record time: the session id and the
67
+ * user agent. Never the credential — the recorder takes no headers
68
+ * argument, so there is no path by which an `authorization` value could
69
+ * reach a record.
70
+ */
71
+ readonly headers?: {
72
+ readonly session?: string;
73
+ readonly userAgent?: string;
74
+ };
75
+ }
76
+ /** One pool lifecycle event. Rare by construction: pools live for minutes. */
77
+ export interface PoolLogEvent {
78
+ readonly atMs: number;
79
+ readonly type: 'create' | 'close';
80
+ /** The pool key (`host@address@timeoutMs`). */
81
+ readonly key: string;
82
+ }
83
+ /** What the viewer reads: the entries plus the counters a rate needs. */
84
+ export interface RequestLogSnapshot {
85
+ /** Newest first, at most the configured bound. */
86
+ readonly entries: readonly RequestLogEntry[];
87
+ /** Recent pool lifecycle, newest first, capped. */
88
+ readonly poolEvents: readonly PoolLogEvent[];
89
+ /** Requests recorded since boot, including ones since dropped. */
90
+ readonly totalRecords: number;
91
+ /** Requests dropped for exceeding the bound. */
92
+ readonly droppedRecords: number;
93
+ /** Sockets the gateway accepted since boot. */
94
+ readonly newConnections: number;
95
+ /** Pool key currently in force, if any request has run. */
96
+ readonly poolKey?: string;
97
+ }
98
+ /** Live gates the recorder reads at record time. */
99
+ export interface RequestLogGates {
100
+ /** Whether any record is kept at all. */
101
+ readonly enabled: () => boolean;
102
+ /** Whether debug detail (full session, pool events) is kept. */
103
+ readonly debug: () => boolean;
104
+ /** How many entries to keep; the ring trims to it on every append. */
105
+ readonly maxEntries: () => number;
106
+ }
107
+ /**
108
+ * An in-flight request, handed back only when the log is enabled.
109
+ *
110
+ * Every method is cheap by construction: two `Date.now` reads and small
111
+ * integer arithmetic across the whole span. No serialization, no hashing,
112
+ * no body handling — the counts ride the usage chunk the adapter already
113
+ * holds, and the timings bracket work that already happened.
114
+ */
115
+ export interface RequestSpan {
116
+ /** Mark the first token delta's arrival. Idempotent: only the first call counts. */
117
+ firstToken(): void;
118
+ /**
119
+ * Record the wire request line, from the fetch wrapper. Called at most
120
+ * once per span; a second call keeps the first, so retries never rewrite
121
+ * the original line.
122
+ */
123
+ request(method: string, url: string): void;
124
+ /** Mark the response status line's arrival. */
125
+ status(status: number): void;
126
+ /**
127
+ * Close the span and append its entry.
128
+ * @param ok - whether the request completed.
129
+ * @param code - failure code for a request that did not complete.
130
+ * @param usage - provider-reported counts, when the terminal usage arrived.
131
+ */
132
+ finish(ok: boolean, code?: string, usage?: {
133
+ readonly inputTokens: number;
134
+ readonly outputTokens: number;
135
+ readonly cacheReadTokens?: number;
136
+ }): void;
137
+ }
138
+ /** Detail the adapter hands the recorder for one request. */
139
+ export interface RequestStart {
140
+ readonly model: string;
141
+ readonly sessionId: string | undefined;
142
+ /** Fallback line, used only when the fetch wrapper never reported the real one. */
143
+ readonly method?: string;
144
+ readonly url?: string;
145
+ readonly messageCount: number;
146
+ readonly hasImage: boolean;
147
+ readonly toolCount: number;
148
+ /** Allowlisted request headers; the recorder never sees the credential. */
149
+ readonly headers: {
150
+ readonly session?: string;
151
+ readonly userAgent?: string;
152
+ };
153
+ }
154
+ /**
155
+ * One request's recorder. The adapter calls `start` once per stream; the
156
+ * span it returns (or undefined when the log is disabled) carries the rest.
157
+ */
158
+ export interface RequestRecorder {
159
+ start(detail: RequestStart): RequestSpan | undefined;
160
+ }
161
+ /** Appends bounded per-request records for the lifetime of one Host process. */
162
+ export declare class RequestLog {
163
+ private readonly gates;
164
+ private entries;
165
+ private poolEvents;
166
+ private seq;
167
+ private totalRecords;
168
+ private droppedRecords;
169
+ private newConnections;
170
+ private poolKey;
171
+ constructor(gates: RequestLogGates);
172
+ /**
173
+ * Count one accepted socket. Called from the diagnostics channel, not the
174
+ * request path, so its cost never lands on a request.
175
+ */
176
+ connectionOpened(): void;
177
+ /**
178
+ * Record one pool lifecycle event. Pools live for minutes, so this is rare
179
+ * by construction; still gated on debug so the common log stays request-only.
180
+ */
181
+ poolEvent(type: 'create' | 'close', key: string): void;
182
+ /**
183
+ * Begin one request's span, or nothing when the log is disabled.
184
+ *
185
+ * This is the single gate on the request path: a disabled log costs one
186
+ * method call and one boolean read, and constructs nothing.
187
+ */
188
+ start(detail: RequestStart): RequestSpan | undefined;
189
+ private append;
190
+ /** Shrink the ring to a new bound immediately; growing backfills nothing. */
191
+ setMaxEntries(bound: number): void;
192
+ /** The current reading, newest first. Computed lazily: nothing on the hot path. */
193
+ snapshot(): RequestLogSnapshot;
194
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dan-ai-studio/dshopencodego",
3
- "version": "0.1.63",
3
+ "version": "0.1.64",
4
4
  "description": "OpenCode Go provider for DeepSeek Harness: live gateway catalog, per-conversation session header, usage",
5
5
  "type": "module",
6
6
  "license": "MIT",